Subtitles now work everywhere speech does — including the engine we run ourselves, and including speech the platform generates for you.
The local speech engine can make subtitles now. It was the only one that could not, and the reason was almost funny: it was already asking Whisper to time every word, and then reading only the start and end of each sentence and dropping the rest. Nothing above it asked for the detail either. Both ends are fixed, so the cheapest engine produces SRT and VTT like the rest. Placing every word takes a little longer, so it is done for transcription and skipped on live phone calls, where nothing would read it.
Captions on the speech you generate. A Voiceover node now carries captions, and a talking avatar inherits them from the voiceover driving it. On a video the player gets a CC button and the captions start on. On audio you get the subtitle files, since there is no picture to put words over.
Only ElevenLabs reports exactly when it said each word. For every other voice the words are spread across the real length of the clip, which is close but not exact — and those captions are labelled estimated timing, so you always know which kind you have. Sound effects and music are left alone; there are no words to caption.
The local speech engine produces subtitles — it was already timing every word and throwing the detail away.
Voiceovers carry captions, and a talking avatar inherits them from the voiceover that drives it.
Video players get a CC button, with captions on by default; audio nodes offer the SRT and VTT files instead.
Estimated captions are labelled as estimated. Only ElevenLabs reports real timings; every other voice is spread across the clip length and says so.
Generated audio gets a real duration — most speech engines never report one, which left the stored clip with no length at all.
Both changes in this release are the platform surfacing something it already had and was quietly throwing away. Subtitles from your transcripts. Speech-to-text engines have always returned word-level timings — exactly when each word starts and ends — and every layer above the engine discarded them, so it was impossible to build subtitles from a transcript the platform had timed perfectly well. A Transcribe node in the Studio now offers its text as TXT, and as SRT and VTT subtitle files ready for a video editor or the web. Lines are grouped to be readable, breaking at pauses and sentence ends rather than flashing one word at a time, and they are kept with the transcript so they are still there when you come back to the flow. Not every engine reports timings. Deepgram, ElevenLabs and the Whisper adapter do; the local Whisper engine does not yet, so a transcript from it offers plain text and says so, instead of handing you subtitles with made-up timecodes. A stopped speech engine now says it stopped. In the Studio's engine picker, a local engine that was down was filtered out of the list — indistinguishable from one that had never been set up, with nothing on screen to explain why a node would not run. Offline engines are now listed and greyed out, marked offline. Engines you have deliberately switched off are still hidden: switched off is a choice, offline is a fault.
Transcripts download as subtitles — TXT always, plus SRT and VTT whenever the engine reported word timings, with lines grouped for reading rather than one word at a time.
Subtitles are kept with the transcript, so they survive coming back to the flow later.
A local speech engine that is down is shown as offline instead of disappearing from the Studio's engine picker, where a fault used to look identical to an engine that was never configured.
An engine that reports no timings says so rather than producing subtitles with invented timecodes.
A customer who calls a busy team should wait with music and be connected the moment somebody frees up. Every part of that turned out to be broken, and this release fixes the lot. Waiting now works from anywhere. Reaching a team through the receptionist could not put you in the queue at all — the receptionist would say nobody was available and, if you offered to hold, had no way to let you. It now asks whether you would rather wait with music, and puts you in line when you say yes. The music plays continuously. It used to stop the instant the system began ringing the team, so callers heard about three seconds of music and then twenty of silence, over and over. It now plays right through and stops only when someone actually answers. Unreachable phones stop costing you time. A desk phone that is switched off was still rung for a full twenty seconds before anyone else was tried. That dead time is gone: the system moves on as soon as it sees there is nothing ringing. Operators stop being invisible. Hanging up used to leave you shown as 'wrapping up' for about a minute even though the real window is fifteen seconds, and a Do-not-disturb set hours earlier would silently exclude you from your teams with nothing on screen to explain it. Wrap-up now clears on time, and the web phone warns you when your own status is hiding you from team calls.
The receptionist can offer to put you on hold for a team — if the team is busy and you say you will wait, you get hold music and a place in line, and are connected as soon as someone frees up.
Hold music no longer cuts out every few seconds — it used to stop as soon as the system started ringing the team, up to twenty seconds before anyone could answer. It now plays through the ring.
A switched-off desk phone no longer costs the caller twenty seconds — the system moves on to the next person as soon as it can see nothing is actually ringing.
'Wrapping up' clears on time — the window is fifteen seconds, but its end was never announced, so colleagues kept seeing you as busy for about a minute after you hung up.
The web phone warns you when your own status is hiding you — Busy, Do not disturb and Away quietly remove you from every team queue; being signed in is not the same as being reachable, and now it says so.
Two proposals close in this release, and they are the same idea applied twice: the platform should offer exactly what it can actually do, and admit what it could not read. What an agent offers. Last release the console chat learned to decide attachments from everything the agent has — its model, its sub-agents and its tools — instead of the model alone. Every other surface was still guessing. Now the embedded agent, the chat bubble and chat inside your apps ask the same question: an agent whose model cannot see, but which has a vision or image-editing sub-agent, accepts images on those surfaces too; and chat inside an app, which never offered an attachment at all, offers one whenever something can read the file. Nothing to configure — attach the sub-agent and the surfaces follow. What the platform read. Scanned pages inside an otherwise digital document were being skipped. One average over the whole file decided whether any page was read with OCR, so a 200-page contract with ten scanned inserts — a signed page, a stamped annex — was treated as fully digital and those ten pages contributed nothing, with no warning anywhere. Every page is now judged on its own. Very large documents are capped and the agent is told plainly when pages were left unread, poorly scanned pages are flagged so figures and names from them are treated as uncertain, and large files are processed in bounded batches instead of being loaded whole.
Embedded agents and app chats offer what the agent can really do — an agent whose model cannot see, but which has a vision or image-editing sub-agent (or an image tool), now accepts images on the embedded widget, the chat bubble and inside your apps, exactly as in the console.
Chat inside an app can accept files — it previously had no attachment control at all; it now shows one whenever something attached to the agent can read a document or an image.
Callers are held in line when every operator in a group is busy — the call queue (hold music, your place in line, a maximum wait, and the group re-rung when it is your turn) never actually engaged: callers were dropped straight to the overflow action instead of waiting. Switch Queue on for a group and it now works.
Ring groups no longer ring someone who is already on a call — busy, away and do-not-disturb were always respected, but a member genuinely on a call could still be rung, wasting a full ring window per busy member before reaching a free one. The person who receives an internal call is now seen as busy too, and returns to Available properly after hanging up.
Agents on Claude, GPT, Gemini or Grok accept images again — a capability check added in the previous release did not know about commercial models, so the chat refused image uploads for them (documents were unaffected).
Scanned pages inside a digital document are no longer skipped — a mostly-digital file with a few scanned inserts had those pages silently dropped, and the agent answered as if the document were complete. Each page is now judged on its own.
You are told when a document was only partly read — very large documents are capped at 300 scanned pages and the agent is told which pages were not read, instead of answering from a document with holes in it.
Poor scans are flagged — each scanned page carries a confidence score, and the agent is told to treat figures and names from badly scanned pages as uncertain.
Large scanned files are processed in bounded batches rather than loaded whole, and the OCR on/off setting is finally honoured (it was ignored before).
A research request on a production agent came back as text from memory: no web search, no document, no email. The agent had asked the scraper to open a Google results page, which is refused instantly; it retried the same call twice, and a five-month-old safety guard answered the third attempt by taking every tool away for the rest of the turn. None of that showed in the list of what the agent was doing. This release changes each link of that chain. A tool that repeats itself now loses only itself — the search, the document generator and email stay available. A scraper pointed at a search results page runs the search it meant and hands back the results. A request to investigate or research something runs the web search before the model starts writing. And the guard's interventions appear in the activity list, so a turn that changes course explains itself. Documents: the old PDF Report Generator, deprecated for months, is gone; the Word Document Generator produces PDF and Word, and when a tenant has no default template of its own it now applies the built-in ArcanFlows brand format. The chat also offers exactly what the agent can do — attach, image, video, audio, documents — computed from its model, sub-agents and tools together (PROPOSAL-086).
Research requests search first — asking an agent with web search to investigate or research something (investigar, pesquisar, recherche, deep dive) runs the web search before the model writes, even when the same request also names a document or an email to produce.
Built-in ArcanFlows brand format — when a document request names no template and your tenant has no default format template, the Word Document Generator applies the ArcanFlows brand format, so a brand-new tenant still gets a branded PDF or Word document.
The chat offers what the agent can actually do — the attach control and the image/video/audio/document options now follow what the agent's model, sub-agents and tools can really do, per action, with a reason when something is unavailable (PROPOSAL-086).
An agent whose model cannot see can still read an image by delegating to a vision-capable sub-agent.
A looping tool no longer takes every other tool down with it — when the same tool is called three times with the same input, only that tool is blocked for the rest of the turn; the search, document and email tools stay available so the task can finish.
A scraper pointed at a search results page runs the search instead — Google, Bing, DuckDuckGo, Yandex, Baidu, Yahoo, Ecosia, Brave and Startpage result pages given to the Web Scraper are answered with the agent's web search results for that query, not a refusal.
The activity list shows the guard's interventions — Repeated call to X skipped and X blocked for the rest of this turn appear as steps, so a turn that stops using a tool says why.
Internal housekeeping prompts (conversation titles, call summaries) never trigger a web search.
The list of steps is drawn once — while a reply was streaming, the same list could appear twice, one copy flickering in and out; the standalone Working on your request bubble now hands over to the message as soon as it exists.
Guard notices look like notices — Repeated call skipped and blocked for the rest of this turn appear in amber with a shield instead of reading as failures.
A tool call written twice by the model — once without its input, once with it — now runs with the input instead of being rejected as empty.
Long turns are bounded — an agent gets a time budget for tool calls in one reply (7 minutes in chat, 12 in the background) and at most 8 calls to the same tool; when either is reached it answers with what it has and says what it could not finish, and the list of steps shows why.
A step that never reported its end is now marked as not finished when the reply ends, instead of spinning forever.
A failed background job is retried only when the failure was quick and transient — a run that already took more than ten minutes, or that ended on a model-provider timeout, is not run again from scratch; it is marked failed with the reason.
Both people on a call show In a call — the person who dialled and the person who answered, for extension-to-extension and external calls alike, from ring to hang-up; the receiver of an internal call used to stay Available, so colleagues rang them and added them to conferences.
The transfer / Add-to-the-call picker shows everyone's status and will not let you pick someone who is in a call, busy, on do-not-disturb or offline.
Presence updates live on every call event, whichever way the call arrived.
People added to a conference show In a call — invitees and consult targets count from the moment they are rung until they leave, are removed, or never answer.
Removing someone from a conference is final — they leave and stay out until you invite them again; before, their browser reconnected on its own and put them back in the call.
Documents that were never generated no longer come with a link. A document too long for a single generation is retried shorter, then reported honestly instead of as raw text; a download link the agent made up is removed and flagged, and the agent is pushed to really generate the file.
PDF Report Generator retired — deprecated months ago and disabled platform-wide, it is removed from the catalog and from the tool lists of the 35 agents that still listed it. PDF output is the Word Document Generator with output format PDF.
An agent that needs several steps no longer leaves you staring at a still window. From the moment you send a request you see the Arcanflows mark turning, "Working on your request" and a counter — and as the agent works, a short list of what it is doing: the tool it is calling, the sub-agent it is asking, the search it is running, each with a tick and how long it took. When the answer arrives the list folds into a single line you can open again, and a step that failed stays open, because that is usually why an answer disappointed. The list is kept with the conversation, so coming back tomorrow — or to something you sent to the background — still shows what happened.
On a phone call the opposite is true: a caller hears that someone is still there, and never hears any of the detail. The agent fills a long pause in its own language, stops the instant the answer is ready, and never says it more than twice. Tool names, sub-agent names and search terms stay on the screen where they belong.
Also in this release: an agent's safety rules can be edited, not only created and deleted. Correcting a single wrong keyword used to mean deleting the guardrail and building it again from memory; the pencil now opens the rule in the same form used to create it, and saving keeps the rule's on/off state.
See that the agent is working — the Arcanflows mark, Working on your request and a counter appear the moment you send, and stay while the agent is busy. It replaces a spinner that disappeared as soon as the first word arrived, leaving the rest of a long request looking dead.
See what it is doing — a short list of steps as they happen: the tool being called, the sub-agent being asked, the search being run, each with a tick or a cross and how long it took. It folds to one line when the answer lands, and a failed step stays visible.
It is kept with the conversation — reopen a chat later, or come back to something you sent to the background, and the list of what the agent did is still there.
Names and outcomes only — the list says Searching the web, never what was searched; Odoo search records, never which record. Anyone who can open the conversation can read it, so it carries no customer detail.
On a call, presence instead of silence — when a question takes a moment the agent says so, in its own language (Spanish, English, Portuguese, French), stops the instant the real answer is ready, and never says it more than twice in one turn.
Callers no longer hear the internal Sub-Agent label read aloud before a delegated answer; it belongs to the screen.
Your own messages in Team Chat no longer appear twice — the message you sent and the copy that came back over the live connection could both be drawn while the send was still completing.
Safety rules can be edited — the pencil on a rule in an agent's Safety tab opens it for changes (keywords, action, scope, priority) instead of forcing you to delete it and start again. Saving keeps the rule switched on or off exactly as it was.
The list of what an agent is doing now includes its tools and web searches, not only its sub-agents — a document being generated no longer looks like nothing happening.
Agents follow the Arcanflows brand rules again: the name, the ™ and the palette reach the model instead of only the brand tools being available.
Reopening a conversation no longer shows each step of the agent's work twice.
Texting moves into the phone itself: a Messages tab beside the dialpad in the console, in the docked phone on an app page and in the widget a partner CRM embeds — your conversations, the shared inbox with Claim, a reply box and New. Pictures are real attachments now: send an image, a short video, an audio clip, a PDF or a contact card up to 5 MB, and pictures a contact sends appear inline in the conversation, stored by ArcanFlows rather than left on the carrier. Every outbound message identifies the sender — a header such as ARCANFLOWS, with the opt-out line on the first message of a conversation — configurable on the Messaging tab, and the compose box shows exactly what the contact will receive before you send. Integrators get a click-to-text command, two host events, the seat's session messaging routes, three server-key scopes that send only as a mapped seat, and six webhook events carrying the seat's own CRM id.
Team Chat is a place now, not a pop-up. Operations → Team Chat opens the full workspace: conversations on the left, the conversation in the middle, details on the right — and the header and dock buttons show the same thing in a smaller frame. A message is something you can act on: reply in a thread, react, edit or delete your own, pin it, copy a link to it. Text is formatted, the composer takes several lines, and typing @ picks a teammate who is then told, even if they have the conversation set to quiet. You choose how loud each conversation is (everything, mentions only, or nothing), mute it, pin it to the top, and optionally have unread mentions emailed to you once a day. History no longer stops at 200 messages.
The phone's directory now tells you who is reachable before you ring them: every person carries a coloured dot and a label — available, busy, in a call, wrapping up, do not disturb, away or offline — and it follows changes live. Three fixes for anyone embedding the phone in their own system: transferring a call and adding a participant work from an embedded phone, the panel collapses to one column in a narrow card instead of painting the directory over the dial pad, and a workspace with no webhook subscriptions answers with an empty list rather than nothing at all.
Messages tab in the phone — beside the dialpad in the console modal, the docked app phone and the CRM widget: own conversations, the shared inbox with Claim, reply and New, refusals inline, allowance and suspension notices, an unread badge.
Pictures and files (MMS) — Attach sends an image, short video, audio clip, PDF or contact card up to 5 MB; incoming pictures render inline in the conversation. Attachments are stored under your workspace (and counted in its storage), fetched by the carrier through a short-lived signed link, and removed from Twilio. Numbers outside the US and Canada receive a link inside the text instead.
Every message says who sent it — a brand header (ARCANFLOWS: …) on outbound messages and Reply STOP to opt out on the first message of a conversation, both configurable on the Messaging tab; the default is your workspace name, so a new workspace is compliant from the start. The compose box previews the exact text and its segment count.
Click-to-text — the widget accepts a text command from the host page (the twin of dial) that opens a prefilled compose; the host receives message_received and message_updated events.
Session API for the seat — messages/me, threads, thread detail (+ read, claim), send, preview and media upload, all scoped to the seat behind the widget.
Server API scopes — messages:read (tenant-wide conversations, a seat's usage), messages:send (send as a mapped seat, never as the key, through that seat's own gate), messages:manage (assign).
Six webhook events — phone.message.received (with the caller context your lookup returned), sent, delivered, failed with the carrier code, opted_out, and phone.user.suspended; every payload carries the seat's CRM id, and stored pictures arrive as signed links.
App pages — onMessageReceived / onMessageSent on the webphone component, with event.message_body, event.message_from, event.thread_id and the phone_message_* state keys.
Team Chat has a page — Operations → Team Chat: conversations on the left, the conversation in the middle, details on the right. The header icon and the floating button open the same thing in a smaller frame, so the two never disagree.
Messages you can act on — reply in a thread, react, edit your own (marked edited), delete your own, pin to the conversation, copy a link. Messages are formatted now: bold, lists, code blocks and clickable links, in a composer that takes several lines.
@ mentions — type @ and pick a teammate; they are notified even if the conversation is set to quiet, and @channel / @here work in groups. A conversation someone mentioned you in shows an @ instead of a count.
Choose how loud each conversation is — all messages, mentions only, or nothing; mute for an hour or a day; pin to the top of your list. Desktop alerts if you allow them, and an opt-in daily email of mentions you never read.
History you can reach — Load older messages walks back through the whole conversation (it used to stop at 200), with day dividers and a new messages line that stays where you left off.
See who is reachable — the web phone's directory shows each person's live status beside their extension: available, busy, in a call, wrapping up, do not disturb, away or offline, with AI agents marked separately. It updates as people change status or pick up calls. You can still dial anyone; the dot informs, it does not block.
A CRM session never reports management rights through the widget; a session is a seat credential.
The signed media route answers HEAD and serves correct partial content, which carriers require before they will fetch an attachment.
The phone API conformance suite sends the working-tenant pin the console uses; a header change had let it skip most of its checks silently.
Team chat voice notes are fetched through the same membership-checked route as files: a copied link is useless to anyone outside the conversation, and they now count toward the workspace's storage and its retention settings.
Team chat no longer loads everything twice on every page, and closing the panel no longer marks messages you never saw as read.
You can start a team chat conversation without the handoff permission — chat has its own teammate directory.
A conversation preview no longer stops updating when a message is cut mid-character (any accented text could trigger it).
Transferring a call, and adding someone to one, now work from an embedded phone — the widget was sending the target extension in a shape the API could not read.
The embedded phone fits a narrow card again: below about 520 pixels the directory moves under the dial pad instead of over the digits.
A workspace with no webhook subscriptions answers with an empty list instead of nothing, so integrations no longer break on their first run.
SMS and MMS join the phone system. Before anything can be sent, ArcanFlows verifies your own Twilio account — Messaging Service, sender pool, A2P campaign and brand — and keeps one readiness verdict. Every number you own is registered with the capabilities the carrier reports, and one SMS-capable number becomes the workspace's messaging number. Texts arrive on a signed, per-workspace webhook and land on the right person: the owner of a dedicated number, the owner of the open conversation, or the shared inbox as unclaimed; who a message was attributed to when it arrived never changes. The new Messages page shows Mine, Inbox and All with claim, assign, transfer, release and close. Sending goes through the Messaging Service with a fixed gate that names every refusal — suspension, workspace pause, readiness, permission, the extension's switch, your allowance, number capability, opt-out, quiet hours, plan quota. Billing meters SMS and MMS segments; admins get per-user counters, allowances, suspensions, a workspace pause and a full audit trail.
Messaging readiness — the platform verifies the workspace's Twilio Messaging Service, sender pool, A2P 10DLC campaign and brand through the workspace's own credential and keeps one verdict (ready / pending / blocked / not configured); Verify now re-checks.
Numbers with capabilities — every number is registered with voice / SMS / MMS as Twilio reports it, synced on demand; the number search shows capabilities and can ask for SMS-capable numbers only; one number is designated as the workspace's messaging number.
Receiving — a signed, per-workspace webhook (Twilio's signature validated with the workspace's own token; retries store one row); messages resolve to a dedicated number's owner, the open conversation's owner, or the shared inbox; STOP / START / HELP recorded.
Messages page (Operations) — Mine, Inbox and All; claim, assign, transfer with a note, release, close; attribution shown under every message; live updates.
Sending — reply box and New message, from the workspace number or your dedicated number; refusals name their reason; quiet hours in the contact's local time (default 08:00–21:00, per-workspace window and time zone; a manager may override once, on record); delivery status returns onto the message.
Metering — SMS and MMS billable segments, inbound and outbound, with plan allowances, overage rates, tenant overrides and add-on grants; visible in the usage view.
Per-user controls — counters per user from a ledger written with every message, daily and monthly allowances, manual and automatic suspensions (opt-out complaints, carrier filtering, repeated breaches), a workspace pause, and an audit log; the Who may send, and how much card on the Messaging tab.
The delivery-status webhook used by notification channels accepted unsigned requests; it now validates Twilio's signature before writing.
The billing period refresh had silently dropped the web-search limit since that bucket shipped; repaired.
A Messaging Service whose campaign had failed read as merely pending; blocked now wins.
The first real third-party CRM integration of the embedded phone and a conformance pass over all 53 phone API routes (286 checks, all green on this build). For integrators: call filters that mean what they say, CRM fields on call detail, stable response shapes, seat provisioning that attaches to an existing extension instead of creating a second one plus a lookup route, webhook secrets never echoed, recording links only for files that exist. For the embedded phone: the realtime socket production had refused since the widget shipped now connects (incoming calls ring, click-to-call reaches the seat), a console login in the same browser no longer mounts a second phone, and leaving the page delivers the hang-up. For every workspace: a far end left alone is hung up within 90 seconds, the attendant's "are you still there?" ends in a goodbye after 20 seconds and is asked at most three times, and dialing the workspace's own number is refused.
Phone API conformance suite — every one of the phone's 53 public routes, 286 checks; the gate for phone releases from now on.
Seat lookup — GET /server/seats/lookup?email=|external_user_id= says what provisioning will do; the directory lists each human extension's email.
Widget — host page guarded against reload/close during a live call; the phone renders docked inside its iframe; PROPOSAL-080 S9/D11 written up; reference CRM at github.com/windconsul/arcanflows-crm-demo.
external_user_id and extension call filters both mean "calls involving the seat"; call detail carries the CRM fields; documented fields are always present.
Provisioning a seat for someone who already has an extension attaches the id to it (adopted: true) — never a second seat; 409 when their extension carries another id.
Webhook secrets are never echoed to server keys; unknown subscription delete is 404; past expires_at rejected; status note over 200 chars is 400; dial abc is 400.
Recording links are minted only when the file exists (not_ready for two minutes after a call ends); a sweeper clears paths the egress never wrote.
The embedded phone's realtime socket connects from the console origin (production had refused every one); a console login in the same browser no longer mounts a second phone inside the widget; the page-leave hang-up goes out as a beacon.
Call supervisor hangs up a far end left alone within 90 seconds; the voice agent's idle prompt reaches its goodbye and is asked at most three times; calls to the workspace's own number are refused.
The served OpenAPI document reports the running version.
The workspace phone is now a component in the App builder: place it on a page and it docks there for any signed-in user who owns an extension — the same single phone the console already runs, so a live call survives moving between pages. The phone also drives the page: a call rings, the caller's number and the CRM's name land in app state, the form beside the phone fills its empty fields without touching what the agent already typed, and when the form is saved the call and the submission point at each other.
Web Phone app component: dock the workspace phone into any app page for signed-in users who own an extension. It is not a second phone — the one you already have moves into the page and floats again when you leave, so a call in progress is never interrupted. Viewers without an extension, or apps with their own accounts, see a short explanation instead.
The phone drives the page: every call raises events a page can act on (ringing, caller identified, answered, held, resumed, ended, missed) and publishes the live call into app state. Wire caller identified to a state update and the form beside the phone fills in the caller's number and CRM name, leaving anything the agent typed untouched.
Calls and form submissions linked both ways: save a form during a call and the submission carries the call id while the call carries the form reference and a form_submitted tag.
Click-to-call from any component: bind the phone's dial target to a table row or a button and a click places the call.
Phone desk template: a ready-made page with the docked phone, a customer form and a recent-calls table, already wired.
Form prefill inside apps now fills only empty fields instead of replacing the whole form whenever its bindings change — a latent way to wipe a half-typed form.
We spent this release trying to break the embedded phone on purpose. Six drills — a key revoked mid-call, a session expiring mid-call, spoofed origins, a CRM lookup that never answers, a webhook receiver down for ten minutes, and fifty embedded seats ringing at once — found four real defects, all fixed here. Phone webhooks now survive a ten-minute outage at your end, one dead receiver no longer holds back everyone else's events, and the Phone System page is organised into five tabs instead of nine stacked cards.
Phone webhooks survive a ten-minute outage at your receiver: a phone.* subscription now retries for about 17.5 minutes (7 attempts) instead of 2.5 minutes, so a maintenance window at your end no longer loses call events. An explicit retry policy always wins, and existing subscriptions keep their settings.
One dead webhook endpoint no longer stalls everyone else: deliveries used to run inside the event loop, so a receiver that stopped answering held back every event on the platform for its whole retry cycle. They now run in parallel, bounded.
Large ring groups ring all at once: the web members of a ring group were called one after another, so the last seat of a big group started ringing noticeably late. Measured after the fix: fifty embedded seats all ring within about 50 ms of each other.
An embedded phone whose session was revoked now ends its call instead of appearing to still work: ten seconds of grace for a renewed token, then a normal hangup.
Steadier database access under load: the connection pool was sized by CPU count while background maintenance jobs each hold a connection for the length of their run, so enough overlap could leave the platform unable to reach its own database. The pool is now sized explicitly and a job can no longer wait forever for a connection.
The Phone System page is now five tabs — Numbers, Extensions, Ring groups, Integrations and Settings — instead of nine cards in one long scroll. The CRM features (embed keys, caller lookup) sit together under Integrations, and every tab can be linked to directly.
The embedded phone gets its data half. A CRM or ERP backend now holds its own scoped server key: call history and statistics, write-back of its record ids, tags, notes and outcomes onto our calls, click-to-call through a seat's open phone, phone-event webhooks, and just-in-time seats for its own users. And before an inbound call is routed, the platform can ask the CRM who is calling — greeting by name, screen-pop context, and CRM-directed routing — falling back to today's routing whenever the CRM does not answer.
The phone is also a documented product for developers now: a phone-only OpenAPI specification served at a public URL, a Phone API quickstart and interactive reference on the documentation site, a Phone webhooks guide that states the delivery contract from the code that sends it, and TypeScript types generated from the specification.
Phone server keys (pbxs_): a scope-gated key for your backend under /api/v1/public/phone/server — tenant-wide call history with filters and CSV export, call detail, one-hour signed recording links, the console's statistics and per-ring-group queue figures, presence, groups and seat status. Managed on the Phone System card under Server keys; every call audited.
CRM write-back: stamp your own record id, tags, notes and outcome on any call, then filter history by them.
Click-to-call from the CRM: originate asks the seat's open web phone to dial; you get 409 seat_offline when nobody is there to place it.
Just-in-time seats: one idempotent call turns a CRM user ArcanFlows has never seen into a phone-only identity with a web extension from the workspace's reserved range (default from 300).
Phone webhooks from the server key: create and manage phone.* subscriptions with the webhooks:manage scope.
Caller lookup: point the workspace at a URL your CRM answers; before routing, the platform asks who is calling (signed, hard timeout, cached, fail-open), greets the caller by name in the attendant and voice agents, pops the record on the ringing agent's screen, carries caller_context on every phone event and — when you allow it — routes to the extension or ring group your CRM names. Live "Ask the CRM" test and miss/timeout counters on the Phone System card.
Phone API for developers: every phone route has OpenAPI coverage and a phone-only specification is served at /api/v1/public/phone/openapi.json; the documentation site gains a Phone API quickstart (/documentation/api/phone) and an interactive reference (/documentation/api/phone/reference) that renders the live specification.
Phone webhooks guide: headers, HMAC-SHA256 signature over the raw body, retry policy (3 retries at 5/30/120 s), timeouts and the payload of each of the seven phone events, with Node and Python verification snippets. TypeScript types for the phone API are generated from the specification.
The phone system now talks to the rest of your software, and the phone itself moves into it. Every call, every agent handoff and every seat status change is a platform event you can subscribe a webhook or a workflow to; and the web phone ships as a drop-in widget for any CRM or ERP page. Two long-standing event-router defects are fixed for everyone.
Phone events for webhooks and workflows: call ringing, answered, completed (with duration, summary and a one-hour signed recording link), missed, transferred, voice-agent handoff requested, and seat status changed are catalogued events — subscribe a webhook or use them as workflow triggers, no code.
Drop-in phone widget: one script and one line render the console's own web phone inside any CRM or ERP page on the ArcanFlows origin, with ring, call and status events streamed to the host page and click-to-call from it. A ready-made test page ships at /embed/phone-test.html.
Webhook-only event subscriptions (no workflow attached) now work.
Event subscriptions no longer receive other workspaces' events.
New event subscriptions are matched immediately instead of after a cache that could stay stale for minutes or indefinitely.
Embedded phone resolves the API at runtime and reopens its dialpad after a hangup or a failed dial.
The release that lets a customer's phone system live inside whatever software their team actually works in. A CRM or ERP backend holds one embed key; its users get short-lived, single-extension sessions; the browser rings, answers, holds, transfers and sees presence through the same code the console uses, with every action audited and recordings leaving only as short-lived signed links. Also: the whole-cluster view for administrators, pending handoffs that page someone instead of aging invisibly, and a Microsoft Graph fix for semicolon-separated cc/bcc lists.
Embed the webphone in your CRM or ERP: create a phone embed key on the Phone System page; the other system's backend exchanges it for 15-minute, single-extension sessions — by extension id or by that system's own user id — and its users ring, answer, hold, transfer, set status and see presence from inside their own tools.
Session ledger and activity trail: every embedded session is visible (host, IP, opened, expires) with a Close button, and every action an embedded seat performs is recorded under Show recent activity.
Recordings only as short-lived links: an embedded seat can fetch a call's recording through a signed 10-minute URL, never a permanent one.
Whole-cluster view for administrators: one page shows every server with CPU, memory, disk, load and containers, and says plainly when a server cannot be reached.
Pending handoffs page someone: a handoff waiting longer than the configured minutes alerts the workspace's owners and admins, once, instead of aging unseen.
Embed pages enforce their domain allowlist in the browser: agent, dashboard and phone embeds now send a frame policy derived from the key's allowed domains, so a restricted key cannot be framed from another site.
Microsoft Graph cc/bcc lists with semicolons no longer fail with HTTP 400; both comma and semicolon separators are accepted for to, cc and bcc.
Infrastructure page sections below the cluster cards are labelled with the server that answered instead of flipping silently between servers.
The release where the platform stopped depending on any single machine. The web edge is now active-active behind a network load balancer that removes a failed server from rotation in under thirty seconds with no human involved. Inbound phone calls fail over automatically through a second carrier route. The data layer is addressed by stable internal names, with the standby database one command away from taking over. And none of it is theory: the edge failover and the phone failover were both drilled the same day — the latter with a real phone call answered normally while the primary telephony node was deliberately offline.
Automatic edge failover: all public hostnames now route through a health-checked load balancer across both application servers — a server failure removes it from rotation in under 30 seconds, automatically.
Automatic phone failover: incoming calls reach a second, independently verified telephony stack if the primary does not answer — proven with a live call during a controlled drill.
Database failover in one command: the standby database (already replicating within milliseconds) can be promoted and traffic repointed in minutes, with the procedure documented and the dangerous edges spelled out.
Deploy safety: the pre-deploy gate now refuses to deploy while any phone call is live, on development and production alike.
Both application servers now serve traffic simultaneously — capacity as well as safety.
Failover procedures are executed drills with recorded timelines, not untested documents.
Architecture documentation updated to reflect the new topology end to end.
The release where things land where they belong: calls in the right tenant, browser tabs in the right workspace, models chosen on the node that uses them, and support calls given the hours they actually run. A report that one tenant's extension was being counted in another's workspace unraveled to a resolver that threw away the caller's tenant on shared trunks — fixed, proven against the live collision, and the mis-booked calls moved home with an audit trail. The 4-hour call ceiling (enforced, it turned out, in five different places) became a 4h default with per-tenant grants up to 8 hours, each tab now pins the tenant it works in, and the Studio gained the accountability it was promised: a run log that turns "several nodes fail" into queryable facts, honest translated errors, real downloads, resizable nodes, capability-driven controls, and a per-node model picker.
Per-tab workspace pin: each browser tab remembers which organization it is working in — switching tenants in one tab no longer switches every other tab mid-task.
Studio run log & honest errors (PROPOSAL-082 S1b): every node run — success or failure — is recorded with engine, parameters, latency and the full error; failures open a detail modal, recurring provider errors (content filter, quota, rate limit, bad API key) are translated into plain language, and Platform Health gained a Studio tab.
Studio capability-driven controls (P082 S1): duration selectors show what the selected engine can actually render — fixed-duration engines say so instead of silently ignoring the setting; downloads save the file with a real name; image and video nodes are resizable with a full-size lightbox.
Model picker on Studio nodes (P082 S2): pick the model within the engine (e.g. wan2.2-i2v vs wan3.0-video-prime on the same account) right on the node — the list comes from the backend''s own supported-models configuration.
Call duration limits by demand: the platform default is 4 hours, and individual tenants can be granted 5, 6, 7 or 8 hours — or tightened below — from the capacity dialog. Real support calls run past four hours; they no longer get cut off mid-conversation.
Builder guided goal cards: visual what-do-you-want-to-build cards replaced the example lists, and the Builder''s plan quality went under regression guard with app-level golden cases.
Calls book to the right tenant: on shared trunks, extensions that exist in more than one workspace resolved to whichever created them first — one tenant''s calls were counted (and billed) in another''s. Resolution is now scoped by the caller''s own workspace, and the mis-booked calls were re-assigned with a full audit trail.
Hold music is for the caller: operators and transfer targets no longer hear the hold-music bed during consultations; a declined or unanswered transfer now auto-cancels and notifies the operator instead of leaving the caller parked in silence; a mid-call page refresh recovers the call.
Backend edits apply immediately: changing a video/image/voice backend''s model, URL or key now takes effect on the next generation instead of after the next platform deploy.
Longer videos by chaining: connecting one video node into another continues the clip from its last frame instead of failing — and image-seeded generation works on models that support it regardless of naming.
The release where "sent" stopped being taken on faith. A week of silently dead email traced to one Microsoft-blocked mailbox taught the platform that acceptance is not delivery — so mail is now DKIM-signed, dev is structurally unable to spam, and outbound email governance has a proposal and its first stage live. The same honesty landed on the phone system: every call is classified by whether it actually cost money, historical prices were repriced to the carrier's own numbers with a full audit log, hold music finally dies with the hold, and the two mysterious 20-second delays QA and the billing data measured from opposite ends turned out to be the same bug — teardown listening to media timeouts instead of signaling. PROPOSAL-074 COMPLETE (all stages + all four decisions), PROPOSAL-076 COMPLETE (the QA acceptance sheet answered), PROPOSAL-077 COMPLETE (the Builder learns what it spends, shows it, and can be limited and sold), PROPOSAL-075 S0, plus P059 D5–D7 resilience work — on dev and production.
Call billability classification (P074 S0, migration 000432): every call
Outbound calls reconcile at last (P074 S1): the endpoint match was
Call legs become first-class (P074 S2, migration 000435): one row per
The call ceiling is a tenant setting, and it pages (P074 S4, migration
Tenants see where their minutes go (P074 S5): five scenario tiles on
Billability-from-legs, in shadow (P074 D4): a new gate under which a
Presence in the upper bar and meters in the call widget (P076 S4+S5):
Recording integrity watch (P076 S3): every five minutes, calls whose
The Builder starts measuring itself (P077, migrations 000436/000437):
Outbound email governance, stage 0 (P075): the two email tool contracts
Ringback for internal calls (P076 S2): an extension-to-extension caller
WAL archiving with proven point-in-time recovery (P059 D7): production
Qdrant RAG replication (P059 D5): every shard of both vector collections
Per-node companion guard (P059 D6): a post-deploy check that proves the
Workflow execution settings are editable — a workflow's timeout,
The email outage — Microsoft had placed the platform's single shared
57 calls claimed both "never touched a carrier" and a carrier charge
Hold music finally dies with the hold (P076 S1): stops are room-keyed
The two 20-second constants (P076 S2): a declined internal call took
Hold-music lifecycle, complete (P076 S1): room-keyed stop flags (a stop
Call waiting closed without building (P076 S6, owner decision): the
The web phone says how each recent call ended — Answered / Missed /
Every table on the super-admin phone page paginates — extensions,
The production edge configuration (Caddyfile) is under version control —
[email protected] email tasks on dev are paused and dev sends sink to
The public site stops needing a deployment to tell the truth. Privacy, terms, FAQ and a new About page are edited and published from the console; /release replaces a hard-coded changelog whose newest entry was nine months and 33 releases out of date, and imports the real one; and the footer finally points at the pages it had been advertising with dead links. PROPOSAL-073 S1, S2, S3, S3b and S5a — on dev and production.
Content-managed public pages (migration 000430): site_pages +
/release — the release history (migration 000431): public page with
/about, reusing the existing showcase page type rather than adding a
Super-admin navigation in five feature groups (Platform, Tenants & People,
Talk to an agent on the contact page: pick an agent, authenticate with an
Unpublishing a page now takes it down. Both public loaders cached their
The contact page rendered neither its form nor its assistant — both were
Selecting a channel kind now changes the row. Choosing "Phone" on a
The contact form is chosen from a list, not a UUID typed from memory, and
PUT /site-pages/:slug validates its slug. It required one in the body
The CHANGELOG importer stopped inventing facts. An unreadable date
Footer links that were never wired: "About" and "Careers" were href="#"
Dev drops the dev6 hostnames, which resolve to the production cluster.
Rolls up one theme found five ways in 24 hours of live onboarding: the platform failing QUIETLY. Now it pages, persists, explains, and shows the truth instead. P067 S1 (self-monitoring, verified live on prod), the background-chat loss fixes, the RAG pipeline integrity package, the phone/dock fixes, and the Outbound calling card.
The MultiGla receptionist showed two "loaded" documents and held ZERO vectors. Chain: invisible ingestion → user re-uploads → catalog dedupe skips re-ingestion but the new row still claims vector_stored → phantom rows share ONE set of vectors carrying the FIRST row's id → user deletes the "duplicates" → purge destroys the document's only knowledge → the catalog still says loaded, so re-uploads are skipped-as-unchanged forever. Platform audit: 160 of 253 catalog rows point at vanished vectors.
Live prod findings while onboarding MultiGla Services and testing extensions (all deployed to dev AND prod the same evening):
Stuck-pending chat jobs are reaped (CHAT_JOB_PENDING_STALE_SECONDS,
Dead-lettering tells the user (bulk move and per-job): notification +
Failed background chats keep the user's words: the message and an
Conversation rows stop lying: the background persist path updates
One row per (agent, filename): re-uploads REINDEX onto the existing row
Share-aware delete: vectors survive while sibling rows reference the
pip in the rag-service image survives a flaky PyPI (120s timeout, 10
An open dialer swallowed incoming rings: the web dock treated
The attendant denied ring groups existed: S6's extension_groups
GET /api/v1/documents/status (rag-service) + `GET /agents/:id/
Knowledge tab renders the truth, polled every 20s: "Ready · N chunks",
The edit page's Save→train flow — the path that actually minted the
Found while verifying live: the default min_relevance: 0.5 silently
Outbound calling card on the Phone System page: shows whether the
Failure-rate sweep (workflow_health_alerter.go, 5-min cadence, one
Kafka canary probe (60s, on EVERY app node deliberately — the Sep 1
Delivery: bell notification to every active super admin + the System
Config (all optional, defaults sane): WORKFLOW_HEALTH_ALERTS=off,
P063 COMPLETE. Rolls up: S7 call queue (FIFO with hold music in the caller's own room, depth/max-wait caps, ring-lease turn-taking), S4 announcements over the music, per-answer round-robin, overflow-attendant with the routing menu, and the server-truth answer-detection fix.
Announcements over hold music (P063 S4 — the final P063 stage): every
Call queue (P063 S7) — migration 000429 group_queue_entries. When
v1 edges closed: round-robin now advances PER ANSWER (the claim moves
Callers heard ringing long after the member answered: group-ring
Rolls up: P063 S8 (group DIDs), unavailability announcements, the Twilio A2P compliance bundle (signup opt-in + settings consent + terms §5.4 — already live on prod ahead of this tag), the web-phone modal fix, the dev LiveKit URL fix, and the Kafka self-hairpin hardening (incident Sep 2).
Group DIDs (P063 S8) — migration 000428: a purchased number can ring
SMS opt-in at signup (Twilio A2P campaign requirement): the register
Reconsider any time — Settings → Profile → SMS Notifications: each
Terms §5.4 — Voice, SMS/MMS & WhatsApp Communications: telephony
Unavailability announcements (owner request): calling a person whose
The S9 status picker blew the web-phone dock out of its modal (§5b
Rolls up every entry from here down to v1.1.10.5: P063 S5+S9+S10+S6 (ring groups, user status & presence, outbound caller-ID policy, live group ringing with claim/overflow), the group-call lifecycle fixes, the web-phone hooks crash fix, and the dev LiveKit URL fix (dev web calls had silently pointed at prod's LiveKit since the Aug 29 DNS cutover).
Group ringing (P063 S6) — dialing a ring-group extension now rings the
The attendant now routes to teams: groups appear in its directory and
The web phone can dial a group (webint room carries the group payload;
User status & presence (P063 S9) — migration 000425: user_phone_status
Outbound caller ID policy (P063 S10) — migration 000424. One tiered
Ring groups (P063 S5) — migration 000423 creates extension_groups +
Dial-space integrity — group numbers live in the same global dialplan
Axis B (key shape): a per-component contract registry of canonical
Axis D (wiring): a CI invariant that every cross-component ref
G-H: migration 000422 teaches auth_mode and allow_registration
The owner verified S6 in the UI (re-invite after delete + custom app-role
Closes the last places where the Builder failed quietly: drafts that vanished mid-pipeline, and three separate causes behind a single hallucinated conversation. Builds now prove what they produced, and data-table shares get a branded public page with per-share overrides.
P070 S4 — the Builder proves what it built: post-build checks EXECUTE components (probe rows through live validators, probe submissions through published forms with the table row asserted, one real agent turn, app-reference resolution); every delivery card reports verified / unverified / failed with the reason.
P070 S5 — nothing vanishes anymore: all five silent-drop sites report through user-visible channels (failure cards, proposal notes, build-summary notes); unknown proposal types fail loudly; app component types validated on write against the full renderer union; action_data_share taught to the Builder with a full node contract.
P071 kind-backfill EXECUTED on prod: 16,120 pre-S1 learned points classified at their own learned_at; the sweep retired 2,096 expired relics (learned share 20.8% → 16.1%).
POST /admin/learned/backfill (rag-service): classifies the ~19k pre-S1
Light/dark theme on the public share page: follows the visitor's system
Agent hallucination triad (the CDMX-marathon autopsy): Spanish queries now search in Spanish; a temporal-integrity rule stops a previous edition's results being presented as today's; reasoning-only turns get one plain-text nudge before the generic fallback.
Builder exam findings: agents finally have an Enable path (draft → active); an exam plan without its wizard form is a wrong plan (hard planner rule); a dropped stream no longer loses the turn (server-side persistence + UI keeps drafted cards).
P066 convergence: the retired searxng provider stopped hiring itself — migration 000421 converged all agents/templates on serpapi; create/update normalize; the form fallback fixed.
New agents created from templates regressed onto searxng — the
Search in the wrong language: the agent had no language configured,
No temporal integrity rule: the model presented a *previous
Reasoning-only turns surrendered instantly: qwen3.8 burned 1,882
Agents can finally be enabled. Builder-created agents are drafts, the
An exam plan without a form is a wrong plan. The planner swapped the
Not changed, by design: question banks ship EMPTY unless the user's own
A network blip mid-Builder-stream (mobile connections, proxies) canceled
With Require Registration disabled, share login returned an empty
Document sharing grows up. Share links carry row filters, workflows can share a table as a step, and the brand frame becomes the default for every system email rather than a handful of them.
action_data_share workflow node: the manual sharing procedure as one step — create a share + read-only password token, output share_url/password for a follow-up email/notification node. Full dialog parity: visible fields, row filters (customer = {{input.name}}; status != closed), require-registration/search/export/page-size. Migrations 000417–000419.
Row filters on data-table shares: share one customer's rows from a mixed table — enforced on listing, reads, writes, deletes and file downloads (fail closed). Filter editor in the share dialog's Settings tab.
The brand frame as the default for all system emails: brand.EmailFrameAuto at every send boundary (tenant chain, system SendGrid, legacy notifications, email channels); complete documents pass through, fragments framed, plain text escaped tag-aware. Router's reset/invitation/verification emails re-skinned onto the frame.
action_data_share now exposes the share dialog's settings: **Require
Run-wide secret scrubbing: sensitive values (share passwords) redacted from every persisted node output — including downstream nodes that interpolate them.
The legacy email channel marked notifications "sent" while sending nothing when SendGrid was unconfigured; it now rides the tenant chain and fails honestly.
Workflow node boolean switches rendered unset keys as OFF while the backend default was ON.
Deleted workflow node types cleanup (migration 000415) and dead app_component_type enum.
Workflow node boolean config switches rendered unset keys as OFF even
Secrets scrubbed run-wide. Redaction stopped at the producing node:
The legacy email channel stopped lying. With SENDGRID_API_KEY
The Data Share Link node rendered as an unstyled white box: it was
Published apps become usable products: a signed-in user menu, emails sent through the tenant's own mail configuration, and the four portal blockers fixed in one shape. Learned knowledge is labelled as learned where the model reads it, acts on whole documents, and gets a lifecycle sweep that is dry-run by default.
P071 RAG knowledge hygiene, S0–S5 complete: execution-origin learning gate (fail-closed — scheduled/workflow/background runs never learn), answer-content veto, per-agent conversation_learning modes (off/interactive/all), unlearn-on-delete for conversations, kind + valid_until classification (transactional/news vs durable knowledge, capped LLM judge), lifecycle sweep with real deletion — 19,530 stale points removed on day one (38.6k → 19.1k) and a nightly autonomous sweep, doc-level review + promote-to-document, negative eval probes (probe_type=negative_learned, migration 000414) and the Knowledge hygiene card.
P070 Builder, S3(C+E)/S6/S7 + G18/G19: Builder binds existing workspace tools by name instead of drafting duplicates; renderability coverage tests incl. the first apps parity check (axis E); capability caveats surfaced on the plan card before Build plus GET /builder/capabilities; and the four portal blockers fixed — app-defined roles assignable (create + update), a deleted email can be re-invited (migration 000416: live-rows-only uniqueness + SQLSTATE 23505 detection), invite responses carry a shareable invite_link, route permission is the single authority.
Apps get a door: signed-in user menu in every authenticated app (avatar, profile dialog with editable name + change-password, honest sign-out labeling per auth mode; PUT /auth/me, POST /auth/change-password).
App email delivery joins the tenant chain: portal invites/resets/forgot-password route tenant office365/OAuth → fallback channel → system SendGrid, same as platform emails; email_sent is honest.
Phone: each call is its own conversation (tel-<caller>-<room>); every call to any tenant number is logged and billed, unassigned numbers answered by the attendant; read-back rule for captured phone/email data.
App Users mode: profile is editable (first/last/display name) and
Platform mode: identity shown read-only; sign-out is labeled
Public mode (or the flag off): no menu, nothing changes.
an app_users app: the pages and auth screens build, but app-role
a platform (staff-only) app: existing staff sign in fine, inviting NEW
Axis C (renderer_coverage_test.go): every field type the Builder may
Axis E (app_coverage_test.go): the console's AppComponentType
Migration 000415 drops the dead app_component_type DB enum (a third,
Negative probes in the RAG golden set (migration 000414): a question
Platform metrics. rag-service POST /admin/learned/stats (indexed
Agent Health → Knowledge hygiene card: learned share vs the ≤ 10%
rag-service POST /admin/learned/sweep (admin key) evaluates every
POST /admin/learned/snapshot creates a Qdrant snapshot of both
agent-manager KnowledgeLifecycleSweeper: nightly
Super-admin routes POST /api/v1/admin/rag/lifecycle/sweep and
explicit dates in the question or answer → valid through the latest date
"hoy / today / esta semana / this week / este mes / this month", and the
prices, exchange rates, availability, "current version", "actualmente"
no signal → a small LLM judge on the agent's own model (owner decision
Post-P054 workflow node rows left running forever after a done-context write; fresh close-out context.
The stale "email delivery is not configured" message on tenants with working email configuration.
FRONTEND_URL unset now fails loudly (response warning + ERROR log) instead of silently mailing localhost links.
G9: apps define their own roles, but role assignment accepted only
G10: the uniqueness constraint counted soft-deleted rows while the
G11: the invite token lived only inside an email that might never
G12: a role gate duplicated inside InviteUser/ResendInvitation
It could not bind existing tools. A tenant asked Builder — twice,
Enable telemetry counted facts as failures. *"data_table X: type has
The inbound resolve now carries inbound=1, caller and room; when there is
The worker hangs up immediately (deletes the room) instead of leaving the
Order. InitDataTableServices now runs immediately after the workflow
Constructor. NewDataTableAdapter panics at boot on a nil record
Engine. executeNodeWithRetrySafe wraps every node executor call: a
One row per learned document; every action — approve, delete, and the new
Each row shows kind (durable / time-bound with its validity, or expired),
Filters: kind, never served, expired only, older than N days — the same
A composition strip: document chunks, attachment chunks, learned durable /
API: GET /agents/:id/learned-knowledge?kind=&never_served=1&expired_only=1&origin=&older_than_days=,
Retrieval hits are routed by their payload source: conversation-learned
SemanticMemoryKnowledgeSource removed and unwired.
rag-service: chat_attachment gets its own rank tier (+0.04, between
Execution origin. ExecutionRequest.Origin (interactive, channel,
Turn trace. Any tool call (recorded at the executeCustomTool choke
Content vetoes on the answer: the model's own question back to the
Per-agent mode configuration.conversation_learning = off |
Unlearn on delete. Deleting a conversation now deletes its training
Learned items now carry origin in their metadata for S1/S2.
Carrier truth and honest failures. Every agent data-table write had been crashing agent-manager since August 20; a workflow node that hung forever was recorded as nothing in particular; and a failed write left no trace anywhere. Phone agents now read numbers and email addresses back before recording them, and stop speaking formatting meant for a screen.
Four small Builder defects from the P070 audit, each costing something on every single build, each with a test that fails without its fix.
The previous entry repaired the stored sub_fields on MultiGla Services' carga-de-documentos-1787888034. It never checked that the platform can RENDER those sub-fields. The owner found the rest by using the form. Three gaps, all closed here.
MultiGla Services' form carga-de-documentos-1787888034 shipped a repeater ("Archivos a Subir") that rendered the placeholder "Configure repeater sub-fields in the properties panel" — to end users. Owner: "at least Builder should deliver a first functional version."
Owner: "if schemas don't have tables, we should be able to delete them, and avoid having empty schemas there."
Production regression fix (P054 S8f). MultiGla Services asked the Builder to extend their client-portal app; the Builder drafted the change and then delivered nothing, twice, with: Partial: could not update data table "Documentos Recibidos": field 9: reference field must have a reference table ID
PROPOSAL-068 S5, voice half. SMS/MMS do not exist yet, so this is scoped to voice — which is the larger exposure anyway, because it has been live and billing customers from a guess.
P068 S5 (voice): carrier-truth reconciliation — carrier_usage_records / carrier_usage_daily / carrier_reconcile_log (migrations 000409–000410), calls.cost_basis, a 6-hourly Twilio reconciler in diff mode, and the PBX→Carrier variance view. Measured drift on day one: platform said $77.91, Twilio $33.28.
P054 S7+S8: full visual schema canvas; per-app schema namespacing (data_schemas, (tenant, schema, name) uniqueness after duplicate cleanup), Builder awareness of schemas, empty-schema deletion.
P070 S0/S1/S2/G8: Builder component contracts — form→table mapping through repeaters, managed_by: builder, unified refScope for cross-component references, delivery notes for unresolved refs, and gated apps no longer disclose their definition to strangers.
Repeaters that actually run: canonical sub_fields, file/radio/select sub-fields rendered in rows, full sub-field editor, one table row per entry (_form_submission_id, _repeater_index).
Failure accounting extended: data_table_error, workflow_node_stuck, job_timeout, job_orphaned, tool_panic.
Background jobs: 3600 s in-worker deadline, heartbeats (last_heartbeat_at, migration 000413), heartbeat-based reaper.
Voice overlay: greeting in context, USE_TOOL exempt from voice rules, no copyable example sentences, ASR-tolerant guardrail, DATA CAPTURE read-back before recording phone numbers and emails.
DELETE /api/v1/data-schemas/:id (data_tables.delete) deletes an EMPTY
UI: a trash action on each Schemas-tab card and a "Delete schema" button
Migration 000409 — carrier_usage_records (per-resource carrier truth,
calls.cost_basis — estimated | reconciled | off_carrier |
Carrier reconciler (internal/services/carrier_reconciler.go), modelled
Handles the price lag: Twilio's price is NULL on the create response
calls has no Twilio SID column — nothing in the voice path ever
On a deployment that has never reconciled, the first run backfills 30 days.
Super Admin → PBX / Phone → Carrier variance tab: what we counted · what
Every agent data-table insert/update since Aug 20 crashed agent-manager (writer wired before the service existed — typed-nil). Boot-checked, setter guarded, and a tool panic can no longer take the node down.
The post-tool spoken confirmation was swallowed on phone calls when the model fenced its INPUT JSON (phantom fence in the worker filter).
The stale-job reaper killed healthy jobs older than 20 min by age — the "3 or 4 tasks" ceiling.
"factura" disarmed a CRM agent (document-intake hard stop); fenced blocks and tool directives spoken aloud; guardrail refusal loop on noisy lines; greeting repeated.
Select columns created with no options rejected every row (top-level fields too); Builder preview had no upload handler.
Workflow node that hangs was closed with a fixed sentence naming nothing; a form's failed table write left no trace.
Call lifecycle: registrar-outbound calls invisible to /calls and billing; phantom ring-time billing; Kamailio in-dialog ACK/BYE (handle_ruri_alias returns 2 = TRUE).
dev.arcanflows.com had no Caddy site block.
Order. The writer is wired immediately after InitDataTableServices,
Setter. SetDataRecordWriter refuses a typed nil (reflect) and leaves
Guard. executeCustomTool — the single choke point both tool loops
The leak. stripDocumentArtifacts only removed the document tools'
A picker for a capability the agent did not have. Every document tool was
Contaminated call summaries. summarizeCall runs phone transcripts
Whole-job deadline — CHAT_JOB_TIMEOUT_SECONDS (default 3600).
Heartbeat — last_heartbeat_at (migration 000413) + a 30s ticker and
Reaper — replaces the age-only loop; judges heartbeat staleness.
Accounting — new job_timeout / job_orphaned classes. This class was
Gated app, unidentified caller → a shell: name, icon, theme,
none and an unset mode: byte-for-byte unchanged. The client's own
Still gets the whole app: a platform session in the app's own tenant
Client: the runner re-fetches with the app JWT after sign-in, and a
the schedule contract named the cron key cron; the validator has always read
the loop contract taught bare {{item}}; the engine nests the iteration under
action_agent's output was described as the reply text; response is an
transform_aggregate was said to require source/operation/group_by for
A repeater file cell now uses the same public upload endpoint, storage
UploadPublicFormFile resolves repeater sub-field paths
Also added to the row renderer: radio, multi_select, checkbox_group,
Required sub-fields are validated per row before submit ("Row 2: Archivo is
materializeInlineDataURLs now walks repeater rows too — the Aug 24 base64
Repeater file cells are canonicalized server-side into the standard file
Prompt teaches sub_fields + min_rows/max_rows/default_rows,
Canonicalize on save (FormService.Create/Update — the one chokepoint
Publish refuses a repeater with genuinely no sub-fields, naming the field
Migration 000411 repairs stored forms in place (guarded, idempotent —
Renderer + TS types read the legacy aliases as a fallback, so a
New internal/service/builder_table_refs.go — one tableRefScope used by
Two passes. A relation whose target is created later in the same plan is
Isolated components. A failing proposal is logged, skipped and reported;
Nothing targetless escapes. A reference column with no resolvable target
Directly drafted reference_table_ref columns now create dependency edges,
calls.duration_s is the PLATFORM's wall clock (NOW() - answered_at), and
calls.cost_usd was that wall clock times a hardcoded $0.05/min whose env
Twilio's own price field was declared once in the codebase and never read.
That operator estimate was rendered to TENANTS as "Voice cost".
Nothing anywhere compared our numbers to the carrier's.
Off-carrier calls were charged carrier rates. Extension-to-extension
formTableColumns (new, shared by the creator and the reconciler) gives the
formRecordRows (pure, testable) fans out: each row carries that entry's
Backfill stays idempotent: every fanned-out row shares one
Mapping entries for sub-fields use a composite <repeaterID>::<subFieldID>
A second repeater keeps its entries in one JSON column. Fanning out two
Non-repeater forms write exactly the single row they always did.
Reconciliation now also runs when an already-published form is saved:
Trashed tables still carry schema_id, so a schema can look empty while
App bindings (apps.settings.default_schema_id) are CLEARED and counted.
The edit path never resolved table references. applyUpdate's
Refs could only name tables from the same run. The resolver looked at
Schema placement stopped at the first build. The app schema was only
One bad column aborted everything. Execute returned on the first error,
Tenant billing basis is now the CARRIER's duration where the carrier
The tenant-facing "Voice cost" KPI is gone (G11). /calls → Stats now
VOICE_COST_ESTIMATE_PER_MINUTE is now a fallback, used only in the
Operator cost was massively over-stated. The $0.05/min constant is
Tenant minutes were under-counted. A call we forward onward bills Twilio
Calls that stayed open after the caller hung up — and kept billing — now close. The streaming watchdog stopped killing self-hosted models before their first token, research reads pages that exist rather than pages the model remembered, and forms create and follow their own data tables.
P065 S1–S4: Stripe webhook retry sweeper + operator replay surface; honest usage percentages (unclamped); usage threshold notifications (tenant 80/100/overage + operator alerts); missed-webhook Stripe reconciler.
Promotions: tenant validation + Stripe pre-application of codes; duration semantics ("3 months = months 1–3"); catalog-aligned targeting (real plan list, per-item add-on/product/service pinning, Services category, billing-cycle rules with the yearly-coupon guard); store promo-code box.
P024 T4+T2: agent failure accounting (agent_failure_events + super-admin Agent Health page) and the 90-second streaming idle watchdog with model-fallback handoff.
P063 S1–S3: hold music (synthesized default + per-tenant uploaded track, PyAV-decoded), explicit Hold/Resume on the web phone.
P064 S1–S4: forms auto data-tables (auto-create, reconcile, backfill, opt-out).
notifyTenantAdmins role filter matched a role that never existed — every tenant billing notice had been delivered to nobody.
Attended transfer rebuilt subscription-based: MoveParticipant is LiveKit Cloud-only and had never worked self-hosted; recorded calls could never transfer (EG_ egress counted as a party).
Voice: symmetric RTP for port-rewriting NATs (one-way audio); Kamailio dual Record-Route (BYE delivery both directions); hangup tears down the surviving leg; webphone Decline is terminal (no forward chase); PyAV padding distortion in hold music.
Webphone: louder dual-tone ring, full-screen incoming-call modal, tab-title flash.
Overage cap lowered to 1.25×; closed billing periods backfilled from usage_logs (no under-billing found).
The production AWS cluster, and voice that survives more than two people. A conference no longer dies when one participant leaves, rosters show real names, recordings actually reach S3, and web phones ring across replicas instead of only the one holding your presence. You can also bring a Twilio number you already own, including regulated ones.
Every agent on the fleet moved to v2 embeddings — 181 of 181. Phones can ring the browser and the desk phone together, call statistics gain time ranges, outbound and live concurrency, and each agent gets an Answer Quality panel with hallucination metrics that were recalibrated to mean something.
Retrieval quality becomes measurable and multilingual: an eval harness, corrected retrieval, per-agent cutover to new embeddings, a knowledge catalog, and conversation learning that is governed rather than automatic. Identity catches up too — the tenant switcher works, admins are per-tenant, and a drift gate keeps new routes from shipping without permissions.
The workspace gets a real PBX: numbers and extensions you manage yourself, softphone extensions that ring, outbound calling through the platform, and transfers from a physical keypad. Data tables gain real database semantics, and the MultiGla CRM connector arrives read-only.
Team chat carries files and photos, with a control plane deciding what may be sent and kept.
Adds the Microsoft Dynamics 365 connector, and fixes handoff replies that never reached the bubble widget or API clients — a human answered and the visitor never saw it.
Forms learn to capture the physical world: a camera field for photos and video, geolocation, and a Location column so what a form collects maps cleanly into a data table. Also fixes embedded dropdowns that were invisible, and hardens scheduled-task operations.
Web search becomes a good citizen — query hygiene, PII scrubbing, pacing, and a super-admin console showing what was searched. The Builder renders diagrams inline and can produce architecture documents on request, and team chat gains group conversations alongside the AI launcher.
The service-delivery lifecycle becomes a complete, honest operation — purchase-gated requests, real partner assignment, calendar-agreed scheduling, timed sessions, reminders, and instant notifications — while the platform itself becomes safe to scale horizontally (cross-replica realtime, single-runner sweeps) and forms trade the old math captcha for Cloudflare Turnstile. Plus honest per-model conversation memory, the Odoo/streaming chat fixes, and a mobile-responsiveness sweep.
The Builder's evolution loop closes (PROPOSAL-046 complete): failure-gap clustering, eval-verified skill saves, and a confirm-gated self-review where the Builder proposes its own skill edits. And agents get honest conversation memory (PROPOSAL-047 S1): history budgets derived from each model's real context window — verified live going from 8,000 to 83,790 tokens — which also exposed and fixed a platform-wide pgx bug that had silently broken every model-capability lookup.
Apps gain their own role system (both auth modes), forms and tables gain draw-to-sign signature fields, the Builder eval corpus grows to 22 curated golden cases, and a platform governance rule that broke generated-media links is fixed — plus the sequenced-drafting fix that closes the parallel-draft correlation gap for good.
Builder quality becomes a number. An eval harness scores builds, the first turn of the evolution loop took a suite from 11/16 to 16/16, unparseable drafts retry themselves, and the drafter's compiled judgment becomes editable data rather than code. Apps join the types you can edit by conversation.
Builds are verified before they are handed over, and a failure arrives with a fix button rather than a shrug. Credentials gain health, an honest Test, and Reconnect. Also fixes a re-index that destroyed knowledge bases while status flags reported success.
Governance reaches the places data actually flows: documents are scanned before they are embedded, the safety log stops being a PII honeypot, and a PII floor applies platform-wide. Mobile gets usable cards and composers, and the store becomes findable.
Service delivery becomes an operating system rather than a spreadsheet: the hour ledger credits on purchase and logs on delivery, requests have an intake queue, partners have configurable levels and a fulfillment queue. Also fixes service purchases that the Stripe webhook never recorded.
The Integration Hub takes shape — a Shopify connector, ServiceNow folded in as a connector rather than a standalone backend, and a Builder that knows connectors exist and can attach them. Dashboards gain share links, print and PDF, API-key embeds, and caching that survives concurrent loads.
Web search now tells the truth about itself. The SerpAPI tool had zero lifetime executions (three independent causes, all fixed via a new per-tenant api_key_provider auth mode); the Web Search tab's engine routing was saved and read by nothing (now: six engines, keyword-routed per query, localized, recency-filtered); Google Flights and Hotels join as structured-parameter catalog tools with response distillers; search accounting covers workflows and APIs; quotas gate paid tools where they execute. Plus: the tools and apps audits closed — every PROPOSAL-027 component type is now done — and the two-cloud production architecture (AWS mx-central-1 + Vultr MEX in-country DR) is designed in deploy-env/production/. Details: RELEASES.md.
The Builder can now build the whole component, not a fraction of it. Nobody had asked how much of each product the Builder could actually express — only whether what it emitted got wired correctly. Measured against the product's own type definitions: form fields 16→41, table columns 15→23, workflow nodes 23→31, dashboard widgets 6→14, plus filters, ordering, currency formatting, agent capabilities and model choice, app pages that surface dashboards, and threshold alerts. Along the way: five bugs where a value was taught, drafted, saved and silently discarded; three separate defects that made dashboard alerts notify nobody while recording that they had; four charts that were in the palette but drew grey placeholders; and app pages that vanished without a word. Details: RELEASES.md.
The platform's guardrails became real, and a long list of quiet failures got fixed. Content safety was enforced on only one of two execution paths — streamed conversations (console chat, embed widget, voice) ran with no checks at all; that's closed, and above it sits a new platform-wide guardrail layer that updates without a deploy, plus tool-call SSRF governance in shadow mode. Human handoff went from half-built to a working support channel (deterministic escalation keywords, operators actually notified, live operator replies). Voice cleared its bench: speakable-only streaming, SIP toll-fraud lockdown, billing enforcement, graceful drain, and a VAD retune so callers don't have to shout. Studio gained the Alibaba model family with HappyHorse as the default avatar engine. Details: RELEASES.md.
Any softphone now works, and Twilio numbers are proven end-to-end. A Kamailio SIP registrar in front of the LiveKit trunk lets Zoiper/Linphone register and hold a full two-way call with the agent; the worker's echo guard stops the agent from hearing its own speakerphone echo and talking to itself; and a real US number purchased in-console took a live PSTN call through to the agent. Details: RELEASES.md.
The platform's first minor release since GA: agents answer real phone calls. PROPOSAL-033 delivered S1–S5 in two days, live-verified by real calls: self-hosted LiveKit voice stack, Phone tab (extension/number, greeting, language, voice picker, recording policy), Calls page with transcripts/recordings/summaries, super-admin PBX panel, per-tenant trunks + extension namespace (D8), Twilio number purchase in-console (D7), voice_minutes billing in shadow with per-plan concurrent caps enforced. Plus the Builder/forms hardening round, sidebar icon rail, diagram viewer repairs, and the platform-wide tool-noise cleanup. Details: RELEASES.md.
Studio Avatars: 12 curated presets or your own photo as the on-screen character, Presenter/Action flows, true lip-sync via Google Veo 3.1 Fast (DashScope wan2.2-s2v adapter shipped and armed for CN-region keys), Veo self-healing (model-id churn, completed-response handling); live Alibaba/DashScope model sync + provider-sync tenant fix; agent Web Search tab is now the single source of truth, with SearXNG fallback, explicit no-result/misconfiguration notices, and streaming anti-narration; Builder trash safety (live component status in history, bundle restore, double-confirm + per-component audit); themes reach embeds, editor previews, and the app editor. Details: RELEASES.md.
Professional design system (full-token rendering, 12 new themes incl. 2 premium, apps + Builder theme awareness, tenant brand kits with local logo uploads, premium gating — migrations 000315/000316); chat image normalization (HEIC/TIFF/BMP + oversized → JPEG); sub-agent rule routing bounded to ≤280-char requests; batched tool calls re-issued instead of dropped (daily-news email restored); system tools never attachable to agents. Details: RELEASES.md.
Image analyze-vs-edit routing; vision/OCR capability self-awareness; scheduled-task reliability (concurrent worker pool, stuck-run watchdog + 48h window, empty tool-call guard, per-tool failure cap); large attachments via DB row (70-page scanned PDFs work in background chat, ~4 min); extraction memoization; Builder seeding on-demand only (migration 000314); Teams bridge 60s. Details: RELEASES.md.
Everything below this line up to the project's beginning ships as ArcanFlows v1.0.0.0, the platform's first tagged release. Release notes: RELEASES.md. Platform versions are four-segment (major.minor.patch.hotfix); the version is surfaced in the console sidebar, on Settings → Licenses, and at the API /health endpoint.