Repository navigation
Releases: rasonyang/ai-native-callcenter
Release list
v0.5.0
Upgrade notes
- Upgrade both images together. The switch image changes
aicc_outbound.lua
(it stamps and exportsaicc_didon the outbound trunk leg, which the DID
line limit counts). The application adds migrations 00035 to 00041, which run
at startup: post-call transcription, the do-not-call list, campaigns and
their contacts and attempts, the CDR'scampaign_id, the DID's
max_outbound_channels, and a data correction that clearshas_recordingon
CDR rows no stored recording backs (#196). Nothing is dropped or narrowed. - Breaking: the Gemini provider is removed. A deployment with
AICC_PROVIDER=geminino longer starts; chooseopenai,grok,qwen,
gatewayordoubaoand set that provider's key before upgrading (see
Removed). - Flows are re-validated at the start of every call. A flow that names a slot
nothing in it writes no longer loads, so its calls are refused until it is
fixed; check each custom flow withaicc flowaddbefore moving traffic (see
Changed). - The server now refuses to start when
AICC_OUTBOUND_ENDPOINTis set to
anything but asofia/dial string holding exactly one%s, when
AICC_TRANSCRIBE_MODELnames a model the qwen client does not accept, and
when any of the new settings below is malformed or out of range. - API contract: new operations for campaigns (
/campaigns,/campaigns/settings,
/campaigns/{campaignId}and itsstart,pause,resume,cancel,
contactsandattempts), the do-not-call list (/dnc-numbers) and
post-call transcription (/calls/{callId}/transcription), under the new
scopescampaigns:read,campaigns:write,dnc:readanddnc:write. The
CDR gainscampaignId, the DID gainsmaxOutboundChannelsand
isCampaignReady,POST /callsgains the refusalsNUMBER_ON_DNC,
DID_NOT_OUTBOUNDandDID_CHANNELS_EXHAUSTED, and the event stream gains
CAMPAIGN_UPDATED. Nothing was removed. A call that ends without a stored
recording no longer reportshasRecording: true. - New settings, all optional:
AICC_BOT_NO_INPUT_TIMEOUT,
AICC_OUTBOUND_RING_TIMEOUT,AICC_OUTBOUND_SIP_HEADERS,
AICC_DEFAULT_COUNTRY_CODE,AICC_OUTBOUND_DIAL_FORM,
AICC_OUTBOUND_HOURS,AICC_OUTBOUND_MAX_RINGING,
AICC_CAMPAIGN_FREQUENCY_WINDOW,AICC_TRUNK_BREAKER_COUNT,
AICC_PROVIDER_BREAKER_COUNT,AICC_BOT_INBOUND_RESERVE,
AICC_OVERFLOW_HOLD,AICC_OVERFLOW_TIGHTEN_RATE,
AICC_OVERFLOW_STOP_RATE, theAICC_POSTCALL_TRANSCRIBE_*family and
XAI_API_KEY..env.examplelists each with its default. A deployment that
keeps a do-not-call list and dials national numbers should set
AICC_DEFAULT_COUNTRY_CODE;aicc doctorwarns when it is empty and the
list is not. - An outbound call that rings past the 60 second answer window is now hung up
when it answers late, and every outbound leg rings for at most
AICC_OUTBOUND_RING_TIMEOUT(default 45 s).
Added
- Campaigns: AI outbound campaigns. A campaign dials an imported contact list
from one DID, every answered call talks to that DID's flow, and each contact
yields a structured lead. The customer is dialled first and the bot leg
exists only once they answer, so an unanswered call costs no bot session.
Campaigns are AI-only: a flow that cantransfer_to_agentis refused on
create and start, and while a running or paused campaign dials from its DID
(CAMPAIGN_FLOW_HAS_TRANSFER,CAMPAIGN_DID_NOT_OUTBOUND). A campaign has a
calling window checked against the callee's time zone (per contact, else the
campaign's) and the deployment's outer hours, a ring timeout, a retry policy,
and a state machine of DRAFT, RUNNING, PAUSED, COMPLETED and CANCELLED, with
racing transitions decided by the database. The dialer writes each attempt
before it dials, classifies the outcome (busy, no answer, answered,
transient and originate failures, bot failures) and schedules retries.
Consecutive trunk failures or bot failures pause a campaign
(AICC_TRUNK_BREAKER_COUNT,AICC_PROVIDER_BREAKER_COUNT). After one
campaign reaches a number, no other campaign dials it for
AICC_CAMPAIGN_FREQUENCY_WINDOW. Contacts are imported up to 5000 per
request with a result per row, numbers canonicalised to E.164. API:
/campaignswithcampaigns:readandcampaigns:write; the SPA has the
campaign, do-not-call and line-limit screens. Design:
docs/design/10-campaigns.md(#89, #154, #163, #164). - Campaigns keep bot capacity for inbound and bound the over-dial: admission
counts the answers a campaign's ringing legs are expected to bring instead
of one bot slot per leg, and stops short ofAICC_BOT_INBOUND_RESERVE
slots (unset keeps a small default share free). An answered outbound call
that finds no free bot slot is held forAICC_OVERFLOW_HOLD(default
1500 ms) and then hung up asOVERFLOW; a campaign whose answered calls
overflow beyondAICC_OVERFLOW_TIGHTEN_RATEplans more cautiously, and above
AICC_OVERFLOW_STOP_RATEstops over-dialing until the rate falls
(AICC_OUTBOUND_MAX_RINGINGbounds the legs ringing at once) (#158, #165). - Do-not-call list:
GET/POST /dnc-numbersandDELETE /dnc-numbers/{number}
(scopesdnc:read,dnc:write). A destination on the list is refused with
422 NUMBER_ON_DNCon every outbound path, click-to-dial included, and no
CDR is written; the campaign dialer checks it before every call. A national
number is compared after completing it withAICC_DEFAULT_COUNTRY_CODE;
with that empty it cannot be matched and the call goes through, so set it on
a deployment that keeps a list. A customer who asks not to be called is added
by the flow (record_leadwithDO_NOT_CALL) (#162). - Flow tool
record_leadon campaign calls: the bot records the call's
conclusion (intentHIGH,MEDIUM,LOW,NONEorDO_NOT_CALL, and
notes) on the call's attempt. At the end of the callDO_NOT_CALLadds the
number to the do-not-call list andHIGHfiles one callback on the
campaign's follow-up queue. A campaign call is offered neither
transfer_to_agentnortake_message. The seed gains the
campaign_lead_followupflow on 95061 (English) and 95062 (Chinese); no
campaign is seeded (#166). - Campaign reporting: the CDR carries
campaignIdandGET /cdrsfilters on
it, a webhook subscription can filter oncampaignId, andCAMPAIGN_UPDATED
events on the stream tell supervisors when a campaign moved (#167). maxOutboundChannelson a DID (/dids): how many outbound calls the number
may carry at once, as the carrier sells lines per number. Unset is no limit.
A single call or click-to-dial over the limit answers
409 DID_CHANNELS_EXHAUSTEDwith no CDR, and a campaign waits. The switch
stamps the DID on the trunk leg (aicc_did) so hand-dialled legs count too
(#160, #165).- The campaign contact importer in the web app reads Excel workbooks (
.xlsx)
as well as CSV, with the same mapping, preview and rejected-rows download
(#192; thanks @javayou13131). aicc flowsim: replays a scripted scenario against a flow on the real flow
engine with no telephony and no model, andgo test ./internal/flowsimruns
it for every seed flow. A second tier lets text models play the bot and the
caller. Every seed flow ships with scenarios. Guide:
docs/flow-authoring.mdsection 8 (#85).AICC_OUTBOUND_RING_TIMEOUT(default45s, at least5sand below the
60 second answer window): how long an outbound leg, AI outbound or
click-to-dial, rings before the switch gives up. A single call used to ring
for as long as the carrier allowed, and an answer after the window was
bridged to nobody and left parked; it is now hung up (ALLOTTED_TIMEOUT)
(#155).AICC_OUTBOUND_DIAL_FORM: how the campaign dialer writes a number to the
trunk,E164(default),E164_NO_PLUSorNATIONAL, for a trunk that
refuses the default. Campaigns store and compare E.164 whatever it is
(#164).AICC_DEFAULT_COUNTRY_CODEcompletes a national number for the do-not-call
check and contact import. The compose stack sets it to86; the
application's own default is empty.aicc doctorgains aWARNstatus and a
dnc_country_codecheck that uses it; aWARNdoes not change the exit
status (#162).AICC_PROVIDER=grok: xAI Grok Voice (grok-voice-3.0, key inXAI_API_KEY)
as a profile on the Realtime client, with G.711 passed through untouched. The
client now ignores thepingevent, treats cumulative
...input_audio_transcription.updatedas partial text, and counts a
...transcription.completedas final only when itsstatusis absent or
completed, so each caller utterance is one transcript line. A refused
response.cancelwith nothing active is no longer reported as an error.
Grok drops a per-response instruction override once the conversation has a
turn, so on that profile every mid-call spoken line is first put in the
conversation as a caller message (profile traitIgnoresOverrideOnceTalking).
aicc doctorand the installer (--provider grok) know the provider. Findings:
docs/design/grok-findings.md(#226).- Flow tools can keep a list across their calls:
collecton an HTTP tool
appends a rendered value ({args.x},{slots.y}) to a list slot after each
successful call for which its optionalwhencondition holds, without
duplicates and at most 20 values.{slots.<slot>}renders the list and
<slot>.countits size, so a transition can branch on it. Checked at load
(docs/flow-authoring.md2.6.1) (#116). - Post-call transcription of recordings with
qwen-audio-3.1-asr-flash, off by
default (AICC_POSTCALL_TRANSCRIBE_ENABLED, plus an HTTPS endpoint and
ALIYUN_API_KEY). A finished call's stereo recording is split into its two
channels, cut into clips of at most 4.5 minutes and sent to the model; the
segments are stored apart from the live tran...
v0.4.0
Upgrade notes
- Upgrade both images together. The switch image builds
mod_audio_stream
from our fork (see Changed), forwards the call type on a bot overflow in
aicc_queue.lua, and takes the new optionalPSTN_GATEWAY_PROXYsetting.
There are no new migrations; the database schema is unchanged. - API contract:
POST /agent/loginno longer acceptsextensionNumber. The
body is optional and a body carrying any property is refused with
VALIDATION_FAILED(422); the agent is always signed in at the extension
bound to them. The web client never sent the field.PUT /agents/{agentId}
that changes or clears the bound extension of a signed-in agent is now
refused withCONFLICT; the agent signs out first.Presencegains the
optionalsipDomain. Nothing else was removed. - New settings, all optional:
AICC_BOT_RTP_DEAD_TIMEOUT(default5s),
AICC_BOT_FIRST_MEDIA_TIMEOUT(default30s),
AICC_BOT_GREETING_MEDIA_WAIT(unset keeps the old behaviour), and the
switch'sPSTN_GATEWAY_PROXY. A negative or malformed duration is refused
at startup. - A bot call that receives no inbound media at all is now ended after 30
seconds, not 5. A call whose media was flowing and stopped still ends after
5 seconds.
Added
- Deploy:
AICC_BOT_GREETING_MEDIA_WAITholds the bot's first turn until the
caller's inbound media has started. Behind a carrier that opens its media
path seconds after the answer, the greeting was spoken into a path that did
not exist yet and the caller never heard it. The model session still opens
at call start. Unset greets as soon as the session is ready;0waits for
the first inbound RTP however long; a positive value waits for it or that
long. The logs now recordfirst mediaandfirst bot audio
(isBeforeMedia), and the metricsaicc_bot_first_media_msand
aicc_bot_first_audio_ms{before_media}, whether or not the setting is
used (#98). - Deploy:
AICC_BOT_RTP_DEAD_TIMEOUTandAICC_BOT_FIRST_MEDIA_TIMEOUT
make the bot's two media timeouts configurable;0turns one off (#97). - Switch:
PSTN_GATEWAY_PROXYsets the host and port the outbound
Request-URI names, separately fromPSTN_GATEWAY_HOST/PORT, the next hop
the datagram goes to. A carrier behind FreeSBC v2 needs the two to differ.
Unset, it follows the host and port, so existing deployments are
unchanged (#61). - API:
Presence.sipDomainis the SIP domain this deployment's phones
register at, readable without minting a session (#119).
Changed
- Agents: the phone (
isDeviceRegistered,deviceAccount) is read from the
switch at the agent's bound extension on every surface: theAGENT_*and
DEVICE_*events,GET /agent/presence,GET /auth/meand theGET /agentsroster. An agent who is signed out is told about their phone, and
aDEVICE_*event is published only when the state changed. Sign-in is
only at the bound extension (#106). - Switch:
mod_audio_streamis built from our fork,
rasonyang/mod_audio_stream, pinned at a commit whose code is the MIT
release we already shipped; only the submodule URL changed, so it now
fetcheslibwscfrom our fork too. The image gainsmod_audio_stream.repo
as a label. Upstream relicensed the module to AGPLv3 after that commit and
the fork takes nothing from it. - Docs: the product has no carrier-line configuration screen or API. The
switch holds one gateway, the SBC's, set once at install; everything about
a carrier lives in the SBC's own management. The reference dialplan notes
that an AI outbound leg carries noX-FSBC-Outmarker (#62). - Docs: the installer's "not yet verified" caveats for Docker Desktop and
Linux distributions other than Ubuntu 24.04 are gone.
Fixed
- AI calls: a carrier that opens its media path late no longer sends every
inbound bot call to the fallback queue. The dead-media watch used to count
from the start of the call, so a call that had received no packet yet was
ended at 5 seconds; it now waits up toAICC_BOT_FIRST_MEDIA_TIMEOUT(30
s) and logsmedia never started(#97). - AI calls: speech at the moment playback ends cancels the dead-air watch.
NO_INPUTcould be reported over a caller who had just spoken (#95). - AI calls: when a tool result changes a value the phase's instruction
quotes without moving the call (a secondrepair_statuslookup, say), the
instruction is pinned again before the tool answer, so the next turn is not
worded from the previous record (#48). - CDR: a transfer the bot accepted but never handed over (the caller hung up
during the closing line) keeps the bot'sbotSummaryandbotReasonin
the row's user data (#57). - CDR: an agent's call to a bot number is the agent's call. The row is
OUTBOUNDfrom the agent's extension whether the bot hangs up, transfers
or the agent hangs up first, with the agent as primary and legs
AGENT, BOT;bill_secis 0 because no carrier answered. It was written
by the bot ledger with no agent, counted as bot containment and billed from
the bot's answer (#76, #80, #104). The call type also travels on a bot
overflow from a queue. - CDR: a carrier call whose caller id equals a signed-in agent's extension
is no longer written as the agent's call, nor written twice. Who placed a
call is read from the originating leg's own extension, not from a number
match (#105). - Agents:
isDeviceRegisteredwas false for an agent whose phone was
registered but who had not signed in, and a stale true survived a later
unregister (#37, #106). - Web: the phone setup card ticks "install the extension" as soon as the
extension is installed, before the site is allowed, instead of telling the
agent to install what they already have (#81). - Web: a browser that moved to another deployment, with the agent on the
same extension number there, mints a phone session for the new deployment
instead of staying registered at the old one with a chip saying Voice ready
(#119).
The images
docker pull rasonyang/ai-native-callcenter:v0.4.0
docker pull rasonyang/freeswitch-aicc:v0.4.0Both linux/amd64 and linux/arm64, built from the same commit. The
application image is one executable with the SPA inside it and nothing else;
the switch image is FreeSWITCH with this release's configuration and Lua
scripts. Run them together: they share the luacc.* contract, and a switch
from another release breaks phone registration. Only exact tags are published —
there is no latest, so a deployment names the build it runs.
Point AICC_IMAGE and AICC_FS_IMAGE at them and the stack pulls instead
of building; deploy/README.md is the whole procedure.
v0.3.0
Upgrade notes
- Existing flows without
maxDurationSecnow end AI calls after 15 minutes.
Set"maxDurationSec": 0in a flow'sglobalto keep the old behaviour. - Upgrade both images together. The switch image adds a dialplan route and
aicc_did_route.luafor an agent's call to a platform number, and sends the
call's type to the bot asX-AICC-Call-Type, which the application now
reads. The application adds migration 00034 (unbacked_claimson the CDR);
migrations run at application startup. - API contract: the CDR gains the optional
unbackedClaimsarray and the
UnbackedClaimenum. Nothing was removed. - A queue call now rings the agent's phone for 30 seconds instead of 15
before it counts as missed.
Added
- Deploy: the one-line installer accepts
--provider gemini, the fifth
engine the application already answers on. Its key,GEMINI_API_KEY, is
read from the environment like the others'. - Flow DSL:
global.maxDurationSec, the call's time limit counted from the
bot answering. Unset (ornull) means 900 seconds,0turns it off, and any
other value must be between 60 and 3600. Gemini's own provider session cap
(about ten minutes) comes before the 900 s default, so a gemini flow should set
a lower limit (docs/provider-extension.md). At 80% of the limit the bot is told to
wrap up. At the limit the call is transferred to the number's queue with
reasonSESSION_LIMITwhen that queue is enabled; otherwise it moves to
global.closingTarget, or hangs up after a goodbye (at once, with no
goodbye asked for, when the bot never gave up the floor). The wrap-up steer
counts the time left when it is rendered, and offers a transfer to a person
only when that queue is open. The CDR records hangup
causeSESSION_LIMITand does not count the call as contained. New metric
aicc_bot_session_limits_total{outcome}. - AI calls: the bot is told, as a platform rule beside the confidentiality
rule, never to say it is transferring the caller, looking something up,
saving a message or ending the call unless it calls that tool in the same
reply. A claim made anyway is detected from the bot's transcript, logged,
counted in the new metricaicc_bot_unbacked_claims_total{claim,provider}
and recorded on the CDR asunbackedClaims(TRANSFER,LOOKUP,
MESSAGE,FAREWELL; database columnunbacked_claims, migration 00034).
It changes no call behaviour and notisContained; a containment figure
that should not count a caller told something that never happened can
leave those calls out (#44).
Changed
- Telephony: a call the queue delivers rings the agent for 30 seconds (five
rings on the browser phone), up from 15. Agents were missing calls they
were reaching for, and one miss benches them. Benching after one missed
call is unchanged. - Click-to-dial types a call by its callee: a provisioned extension or a
queue's number isINTERNAL, any other number (a DID or an external
number) isOUTBOUND. This replaces a four-digit length rule. When the
lookup fails the dial is refused rather than typed by a guess. - Demo flows: the repair-status flow looks up a clearly spoken repair number
at once instead of asking the caller to confirm it in the same turn; an
unclear number is still read back first.
Fixed
- Deploy: with
--no-demothe installer's closing message says how to create
the first administrator withaicc useradd(as root on Linux) instead of
printing an admin password that was never seeded, and drops the demo-only
"dial 95001" hint (#72). - Deploy:
aicc doctor --waitgives the switch'saicc_botgateway up to 45
seconds to come back UP after the application is recreated. A provider
change followed by a rerun could printFAIL BOT_GATEWAY_DOWNfor a stack
that was healthy by the switch's next OPTIONS probe. - Docs:
deploy/one-line-installer.mdnow says where the generated admin
password lives and how the voice provider's key is passed, kept and changed
(#69, #70). - AI calls: when the model calls
transfer_to_agentorhangupagain while
the call's ending is already armed, the first ending stands. A repeat used
to replace it, wait for a turn after the latest call and restart the 10 s
cap, so the transfer ran ten seconds after the last repeat, and ahangup
after a transfer could hang up a caller who had been told they were being
put through. The repeat is answered as done and changes nothing (#25). - CDR: a call the bot decided to transfer but never handed over (the caller
hung up during the closing line) no longer carries the queue'squeueId.
The row was counted in the queue reports as a queue call answered inside
the SLA, with a wait of 0 s, for a caller who never reached the queue (#44). - Telephony: an agent who dialled a platform number (95001, say) from the
browser phone was hung up withNO_ROUTE_DESTINATION. The call now reaches
that number's flow, the same one a carrier's caller reaches, as one call
with one id; the CDR row isOUTBOUNDfrom the agent's extension (#74). - Agents: an agent benched for a missed call who chooses READY is offered the
next call at once. The switch kept them idle for up to a minute after the
miss while callers waited (#51). - AI calls: when the model asks for several tools in one response, each is
judged in the phase the response was made in. The second tool used to be
refused when the first one's result moved the phase, and the new phase's
line pre-empted the answers still owed (#12). - AI calls: an armed transfer or hangup whose closing line is queued behind
other audio waits for the line to finish playing (plus 2 s) instead of
cutting it off at the 10 s cap (#24). - AI calls, interruptions and keypresses:
- The tail of a response cut off by a barge-in is no longer played; a call
used to hear 40-80 ms of the interrupted answer after every keypress. - A keypress while an ending is armed is recorded and leaves the closing
line alone. It used to cut the line into fragments and delay the action. - A keypress takes the floor from a turn that is being generated but has
not spoken yet, and on openai and gateway it now cancels the turn at the
provider too (#55). - A text cue sent while a response holds the floor is answered when that
response ends, instead of being refused and left unanswered (#55). - A function call cut off by a cancel is removed from the conversation, so
qwen no longer says it is transferring without calling the tool. - The 800 ms echo guard runs from when the caller starts hearing the bot,
so real speech over the start of a back-to-back answer is no longer
swallowed (#58). - A turn flushed before any of it played is trimmed from the model's
history. qwen accepts the trim but still behaves as if the line was
heard (#53 stays open for qwen).
- The tail of a response cut off by a barge-in is no longer played; a call
- Web: an agent without the web-sip-phone extension on the page sees the
install wizard at once, not "Phone lost contact · reload" for several
seconds (#75).
The images
docker pull rasonyang/ai-native-callcenter:v0.3.0
docker pull rasonyang/freeswitch-aicc:v0.3.0Both linux/amd64 and linux/arm64, built from the same commit. The
application image is one executable with the SPA inside it and nothing else;
the switch image is FreeSWITCH with this release's configuration and Lua
scripts. Run them together: they share the luacc.* contract, and a switch
from another release breaks phone registration. Only exact tags are published —
there is no latest, so a deployment names the build it runs.
Point AICC_IMAGE and AICC_FS_IMAGE at them and the stack pulls instead
of building; deploy/README.md is the whole procedure.
v0.2.0
Upgrade notes
- The demo seed no longer resets existing accounts: an account or extension
that already exists keeps its password and role on every boot. AICC_SEED_PASSWORDmust not contain& < > " ', whitespace or control
characters; the application refuses to start with one, because the switch
writes the value into XML.- Upgrade both images together. The v0.2.0 switch image changes the internal
SIP profile's NAT handling (see Changed): every phone that is not on
loopback is answered withFS_EXTERNAL_IPin the SDP, including phones on
the switch's own LAN. On a Linux host, check thatFS_EXTERNAL_IPis an
address LAN phones can send RTP to. - The macOS installer needs the v0.2.0 switch image or later. Do not pair it
with the v0.1.1 switch image (AICC_FS_IMAGE=rasonyang/freeswitch-aicc:v0.1.1):
that image answers phones behind Docker Desktop's or Colima's port
forwarding with the container's address, and the calls have no audio. - qwen: the default model is now
qwen-audio-3.1-realtime-plusand the
profile's fallback voicelonganqian_v3.1. A deployment that sets
AICC_PROVIDER_MODELkeeps its model. There is no 3.1 flash model. Live
transcription still usesqwen-audio-3.0-asr-flash-streaming. - Building from source needs Go 1.27.1 or later.
Added
- A one-line installer,
deploy/install.sh:
curl -fsSL https://github.com/rasonyang/ai-native-callcenter/releases/latest/download/install.sh | sudo sh
on Linux (into/opt/aicc), withoutsudoon macOS (into~/.aicc). It
runs preflight checks that fail with a stablePREFLIGHT_*tag and a fix
before anything is written, verifies the deploy bundle's checksum, writes
.env(mode 0600) with generated secrets, starts the published images and
runsaicc doctor. Reruns repair without regenerating secrets;
--external-ip,--upgrade(with apg_dumpbackup first),
--uninstall,--purge,--checkand--no-democover the rest. - Release assets:
install.sh,aicc-deploy-<tag>.tar.gzand
checksums.txton the GitHub release, built byscripts/release-bundle.sh. - Compose overlays:
deploy/compose.linux.ymlruns the application and the
switch on the host network;deploy/compose.macos.ymlruns them in one
shared network namespace.deploy/compose.release.yml.inis the release
overlay that pins the published images. aicc doctorchecks a running deployment from inside it and changes
nothing: readiness, the switch link, the database and its migrations, the
event socket, the SIP profiles, the bot gateway, the external address and
the provider. Each failure carries a stable code and a fix.AICC_SEED_PASSWORD: the password the demo seed gives the accounts and
extensions it creates, and the simulated customers' SIP password.AICC_BOT_ALLOWED_PEERS: the addresses allowed to send SIP to the bot and
RTP/RTCP to its calls; anything else is dropped before it is parsed. Empty
(the default) allows every peer, as before; the Linux overlay sets it./readyzreports the database, the switch link and the migration version
in stable body lines.- The agent's My calls list shows the whole call's duration in a Total
column beside the agent's talk time, which is now labelled "Agent talk".
Changed
- The switch's internal SIP profile treats only loopback as its local
network (local-network-aclaicc_sip_local, with
aggressive-nat-detectionon) and answers every other phone with
FS_EXTERNAL_IP, letting RTP auto-adjust follow the address the audio
arrives from. Before, a phone reaching the switch through Docker Desktop's
or Colima's port forwarding arrived from the compose gateway's private
address, was taken to be local, and was told to send its audio to the
container's address. The external profile, which carries theaicc_bot
gateway, is unchanged. Two-way audio verified live on Docker Desktop on an
Intel Mac, on Colima with thegrpcport forwarder (macOS overlay) and on
a Linux host with the host-network overlay and a LAN WebRTC agent.
Fixed
lua-rolecould exit with "tuple concurrently deleted" on a fresh start,
when its grants raced a migration replacing aluaccview. It now waits
for the migrations to finish and checks every grant.- Gemini: the local speech detector no longer drops a caller's speech start
or stop while the bot's audio is being relayed, so the dead-air timer is
cancelled when the caller talks over the bot.
Docs and CI
deploy/README.md: the git checkout names the release tag, and the guide
covers the prerequisites, where provider keys must be set, first-start
switch messages, a no-phone test call, the bot gateway's NOREG state,
transcription settings and upgrading. A section covers running the stack
on macOS, with troubleshooting for one-way and no audio.- CI skips Markdown-only changes and builds container images only at
release. Redundant tests were removed and the test harness consolidated.
The images
docker pull rasonyang/ai-native-callcenter:v0.2.0
docker pull rasonyang/freeswitch-aicc:v0.2.0Both linux/amd64 and linux/arm64, built from the same commit. The
application image is one executable with the SPA inside it and nothing else;
the switch image is FreeSWITCH with this release's configuration and Lua
scripts. Run them together: they share the luacc.* contract, and a switch
from another release breaks phone registration. Only exact tags are published —
there is no latest, so a deployment names the build it runs.
Point AICC_IMAGE and AICC_FS_IMAGE at them and the stack pulls instead
of building; deploy/README.md is the whole procedure.
v0.1.1
Upgrade notes
- Upgrade both images together. The switch image changes its Lua scripts and
callcenter.conf, and the application adds migration 00033, which recreates
theluacc.directoryview. Migrations run at application startup. - An unrecognised
AICC_PROVIDERnow stops the application at startup even
whenAICC_BOT_ENABLEDis off. Previously it was only noticed once the bot
was enabled. - API contract: the
ErrorCodeenum gainsTERMINAL_ANNOUNCE_REQUIRED
(returned with 422 when a flow is published).createCalldeclares the
extra scopeAI_OUTBOUNDneeds asx-body-scopes; the rule itself is
unchanged. Clients that treatErrorCodeas a closed set need the new value. - Phone registration: an extension with no live SIP session now registers
with its static password (see Changed). While an agent is signed in to
the panel, the static password for that extension does not register.
Added
- Two voice providers:
AICC_PROVIDER=doubao(Doubao full-duplex, key in
DOUBAO_API_KEY) andAICC_PROVIDER=gemini(Gemini Live, key in
GEMINI_API_KEY). One provider per deployment, as before. - Flow DSL: a phase may carry
announce, a bilingual line the bot says as
written when the call enters that phase. The flow designer shows it on the
phase card, and the starter flow includes one. - Flow DSL:
global.closingTargetandglobal.maxTurnsWithoutTool. A call
moves to the closing phase after three consecutive silences where no
authored rule handles NO_INPUT, or after too many bot replies without a tool
call.noInput.countnow counts consecutive silences. - Publishing a flow whose terminal phases have no
announceis refused with
TERMINAL_ANNOUNCE_REQUIREDon a provider that cannot be prompted by text
(doubao). Other providers are unaffected. - A call whose provider session ends because the provider's session lifetime
ran out (currently reported by the gemini client) is released with hangup
causePROVIDER_SESSION_EXPIREDinstead ofMEDIA_OR_PROVIDER_FAILURE. The
caller still goes to the fallback queue. - Metrics:
aicc_provider_sessions_expired_total{provider}and a
provider session counter covering every provider. - Outbound (click-to-dial) calls are recorded, under the same per-DID
recording setting as inbound calls (freeswitch/scripts/aicc_outbound.lua,
called from the trunk dialplan). - Deployment guide split into
deploy/README.md,deploy/chrome-policy.md,
deploy/carrier.mdanddeploy/production-checklist.md, with a
troubleshooting section.
Changed
- The six seeded flows carry their greetings and closing lines as
announce,
setclosingTarget, and hang up on the first decline at wrap-up.
novanet_supportannounces its hand-over to an agent itself. - A phone configured by hand (for example "Configure a SIP account manually"
in web-sip-phone) registers with the extension's static password when the
agent has no live SIP session. The server-issued session credential still
takes precedence while one is live. - On openai, qwen and gateway, a tool result that ends the call carries the
closing line itself, so the line is said as written more reliably. - The bot no longer confirms or repeats its own instructions when a caller
recites them back. Phase labels no longer name internal node ids, and CDR
transcripts record a tool result without its hint and with the node the
call moved to (movedTo). - The agent page shows a lost-contact card with a reload action when the
browser phone extension stops answering, instead of the install card. - Gemini: the caller's language is passed as an input transcription hint;
uplink stalls are logged while the call is in progress, one line per
episode.
Fixed
- A missing provider API key is reported at startup with one ERROR naming the
variable. Previously the application started cleanly and callers were sent
to the fallback queue. - A call ends on its own when the caller declines or stays silent, instead of
depending on the model calling hangup. - An agent moved to NOT_READY with reason DEVICE_LOST returns to READY when
the phone registers again. Other NOT_READY reasons are left alone. - An armed transfer or hangup no longer waits behind the dead-air timer: it
runs when the last line has finished playing, not on the no-input timeout. - Human-only calls get a live transcript; previously the transcription tap
attached but the stream was refused. - If the switch cannot reach the database for
callcenter.conf, the fallback
configuration no longer installs a queue namedsupport@default. - qwen: a silence that moves the call into a phase with a line asks for one
turn, not two; a tool result sent while a response is still open waits for
that response to end; a cancel refused because the response had already
finished is no longer logged as an error. - Gemini: a tool result the model never answers is treated as a stalled turn.
- A provider session refused with a close code is recorded with that outcome
rather than as a clean close. - Migration 00006's Down step returns CLAIMED callbacks to OPEN before
narrowing the constraint, so rolling it back works on a database with
claimed callbacks.
Docs and CI
- README, CLAUDE.md and the design documents corrected to match the code;
AGENTS.md added for Codex. - Provider findings for doubao, gemini and qwen recorded under
docs/design/. - CI and release workflows moved off the Node 20 Actions runtime.
- The GitHub issue chooser points at Discussions.
The images
docker pull rasonyang/ai-native-callcenter:v0.1.1
docker pull rasonyang/freeswitch-aicc:v0.1.1Both linux/amd64 and linux/arm64, built from the same commit. The
application image is one executable with the SPA inside it and nothing else;
the switch image is FreeSWITCH with this release's configuration and Lua
scripts. Run them together: they share the luacc.* contract, and a switch
from another release breaks phone registration. Only exact tags are published —
there is no latest, so a deployment names the build it runs.
Point AICC_IMAGE and AICC_FS_IMAGE at them and the stack pulls instead
of building; deploy/README.md is the whole procedure.
v0.1.0
Changes since v0.1.0-rc.4
- release: v0.1.0 is the pair deploy names (ac64d35)
- docs(deploy): name the /api/v1 mount and link the contract (7bb37fc)
- docs: integrations with other products live outside the tree (a328610)
- fix(web): phone onboarding follows the extension without a refresh (6b1ef84)
The images
docker pull rasonyang/ai-native-callcenter:v0.1.0
docker pull rasonyang/freeswitch-aicc:v0.1.0Both linux/amd64 and linux/arm64, built from the same commit. The
application image is one executable with the SPA inside it and nothing else;
the switch image is FreeSWITCH with this release's configuration and Lua
scripts. Run them together: they share the luacc.* contract, and a switch
from another release breaks phone registration. Only exact tags are published —
there is no latest, so a deployment names the build it runs.
Point AICC_IMAGE and AICC_FS_IMAGE at them and the stack pulls instead
of building; deploy/README.md is the whole procedure.
v0.1.0-rc.4
Changes since v0.1.0-rc.2
- release: v0.1.0-rc.4 is the pair deploy names (0959196)
- build: cross-compile the application image instead of emulating arm64 (bb1d728)
- release: publish the switch image from the release workflow (3615b94)
- release: the switch image carries the application's release tag (6ed1ea6)
v0.1.0-rc.3 was tagged but never released: its build was cancelled before the application image was pushed.
The images
docker pull rasonyang/ai-native-callcenter:v0.1.0-rc.4
docker pull rasonyang/freeswitch-aicc:v0.1.0-rc.4Both linux/amd64 and linux/arm64, built from the same commit. The
application image is one executable with the SPA inside it and nothing else;
the switch image is FreeSWITCH with this release's configuration and Lua
scripts. Run them together: they share the luacc.* contract, and a switch
from another release breaks phone registration. Only exact tags are published —
there is no latest, so a deployment names the build it runs.
Point AICC_IMAGE and AICC_FS_IMAGE at them and the stack pulls instead
of building; deploy/README.md is the whole procedure.
v0.1.0-rc.2
Changes since v0.1.0-rc.1
- release: v0.1.0-rc.2 is the image deploy names (f63c27d)
- fix(aicc): wait for the database at startup instead of exiting (ac482e5)
- docs(deploy): skip the Docker install where Docker is already there (b5ef845)
- build: make base image and GOPROXY overridable for mainland China (6d1a6de)
- release: the image is the release, published to Docker Hub only (61e2a8a)
- feat(phone): the published extension id is the build constant (70bbe80)
- chore(deps): go.mod as go mod tidy leaves it (1233a8d)
- fix(transcribe): a wait the test can see, not a race it can lose (552718a)
- fix(ci): keep the dist placeholder from being staged away by a build (e6766c3)
- fix(ci): restore the web/dist placeholder the go job builds against (1b4fe66)
- feat(phone): an agent's phone configures itself (859fb39)
- feat(deploy): one stack a newcomer can start with two lines in .env (da0f7f3)
- feat(seed): one password, and US numbers inside the 555-01XX block (5d057f9)
- feat(cli): flowadd -did creates the number when it does not exist (5612fe8)
- docs(deploy): what a clean-host install of v0.1.0-rc.1 actually needs (b7107df)
- docs(deploy): correct what a first install on a clean host actually hits (ad56500)
The image
docker pull rasonyang/ai-native-callcenter:v0.1.0-rc.2linux/amd64 and linux/arm64, one executable with the SPA inside it and
nothing else. Only exact tags are published — there is no latest, so a
deployment names the build it runs.
Point AICC_IMAGE at it and the stack pulls instead of building; the switch
that goes with it is rasonyang/freeswitch-aicc, and
deploy/README.md is the whole procedure.
v0.1.0-rc.1
Changes in v0.1.0-rc.1
- ci(release): publish the image under the exact tag only, mark pre-releases, write notes from history (f459753)
- docs(design): 09 describes the webhooks as built (04123c0)
- fix(webhooks): cap deliveries in flight per subscription (957b709)
- fix(webhooks): an unknown filter key is refused when the subscription is written (63468ac)
- fix(seed): the demo flow tells the model to hang up, not to talk past it (2766169)
- feat(deploy): run this deployment on Qwen, and pass both keys through (f1d3c64)
- feat(seed): publish every bundled flow in the demo, each behind two numbers (f3c2108)
- fix(dev): restart.sh creates logs/ before touching its marker (bf0c47e)
- chore: stop tracking Playwright MCP console logs (b2309e1)
- docs: show the agent desktop answering a call in the README (33faa39)
- fix(web): give component tests a five-second async budget (b072c63)
- fix(ci): commit the web/dist placeholder that go:embed all:dist depends on (599b305)
- chore: ignore the eslprobe build output (9308ce2)
- docs: commit messages, comments and docs are written in English (eb8028b)
- feat(calls): kind on POST /calls is optional and defaults to AGENT_OUTBOUND (52610d7)
- docs: the switch's config and image are in the repo, docs updated to match (9e98d82)
- feat(dev): this machine's simulated PSTN trunk goes under version control (6c09a8e)
- feat(demo): the demo stack switches to the switch image built here (f2f1992)
- feat(freeswitch): build the switch image ourselves, mod_audio_stream baked in (16d7172)
- feat(freeswitch): the whole configuration tree, baselined on v1.11.3 vanilla (ad1956a)
- fix(build): docs/ is an input to the binary; it cannot be excluded wholesale (427bf26)
- docs: translate the narrative Chinese; the Chinese in the product stays (4a11e02)
- chore: remove the internal verification records and a mis-committed key backup (4d01d97)
- docs: how to bring this stack up locally, and why mockbackend goes missing (35d807f)
- fix(aicall): leave a trace when the provider interrupts itself too (8d70630)
- feat(seed): factory flows stop choosing a voice for the deployment (49ff514)
- fix(aicall): what the caller never heard must leave the provider's history (a32a9f9)
- feat(provider): the gateway needs no first-turn cue, and this was measured (7851774)
- feat(provider): the Realtime gateway gets its own profile, no longer posing as openai (14c6fe0)
- fix(provider): truncate the model's speech on barge-in, not its tool call (1c89e77)
- docs: delete unreferenced docs and 764K of raw evidence, keep the verdicts (1f83e5f)
- docs(design): align the design set with the code — scopes, and drift found on the way (f6a3350)
- docs(auth): the scope derivation now reads in the repository's language (3429b72)
- docs(auth): REVOKED terminal-state gate passes (owner 2026-08-31), task done (6499a41)
- fix(api): a revoked key cannot be edited — the server never enforced it (b2ca5f0)
- fix(web): a revoked row drops to secondary text; the pill no longer wraps (7d90fe8)
- feat(web): capability groups ordered by what a key is for, calls first (a0a8a76)
- feat(web): capability picker grouped by resource, select-all per group only (b1359ca)
- test(api): ⑦(b)'s errors.go leg now parses instead of transcribing by hand (bc18b14)
- docs: ①–⑦ done, only manual gates left; api-first-audit's table settled (82b6c5f)
- docs(auth): the ledger and cursor for ⑥⑦, only the manual gate is left (3690532)
- test(api): the contract gate — three assertions, each first proved to bite (6e1c517)
- i18n: add the 4 missing error-code translations (ruling 4) (6934149)
- feat(web): an API Keys page — capabilities come from the generated vocabulary (cd0dfad)
- docs(auth): run it on the real dev stack, with evidence for the REVOKED state (1a39c56)
- feat(auth): a 20th scope calls:create:ai — AI outbound is not click-to-dial (a2346cd)
- docs(auth): the ledger and cursor for ④⑤; the build is green again (8396a15)
- feat(audit): an audit row says who is acting, and as whom (135c06c)
- feat(auth): API keys become a first-class credential, build back green (71c7480)
- docs(auth): the ledger and cursor for ③, and an undecided AI outbound capability (5f613f0)
- refactor(auth): the contract decides — AuthContext + scope middleware, no role guards (30fd79d)
- docs(auth): the cursor advances to ③; ②b's verification shape goes in the ledger (ffff937)
- feat(api): generate the scope vocabulary from the contract too, Go and TS (2a874e5)
- docs(auth): §4.1 lays down N1, worded to P3's intent rather than its letter (21f89c2)
- docs(auth): the top of the cursor states how to carry on (13f3f74)
- docs(auth): check the cursor, four drifts fixed — one of them a false tick (305b393)
- docs(auth): tick off §2/§3, move the cursor to ③ (298aa20)
- test(auth): §3's nine cases; eight fail on the baseline as expected (c723a9f)
- docs(auth): §2 done, ledger notes 91/19/26 and the escalation the self-check caught (a074dce)
- chore(api): regenerate the Go and TS contract code (1d541f3)
- feat(api): scope authorization, API key endpoints, and the contract serving itself (bbf7bf8)
- docs(auth): ruling 8 corrected to 18 scopes, and config:read's deliberate widening (f43a666)
- docs(auth): 16 scopes final, users:write on its own, act-as header X-AICC-Agent-ID (bd50916)
- docs: fold V3 (a deployment serves its own contract) into §2 as method 2.2f (f2fb358)
- docs: the review ledger records the commits that landed V1/V2 and O1-O4 (e332d76)
- fix(api): the API answers its own misses; the SPA no longer fakes success (085a40e)
- docs(auth): cut four complexities §1 prepaid for problems that do not exist (e2fe8ad)
- docs: "UI is optional. API is the product" in CLAUDE.md, product audited against it (605cea6)
- docs(auth): unified auth model §0 — source baseline, rulings, positioning review (7f9578b)
- feat(supervisor): supervisors can LISTEN/WHISPER/BARGE; their leg is not a party (6a3870c)
- feat(contacts): numbers accepted liberally, stored strictly — +?2-32 digits (4251346)
- feat(forms): field-level errors on every form, each one verified by clicking (231af79)
- feat(catalog): validation failures name the field and rule, shown under the input (2b2306a)
- docs(walkthrough): the stray CDR is fixed and verified on a real call; todo struck (106ac3c)
- fix(outbound): export aicc_call_id on originate; the trunk leg stops going solo (4414816)
- feat(designer): the Tools page expands parameters per their schema (0e03c64)
- fix(cdr): the BOT leg gets the flow name too on a call transferred to an agent (de478d8)
- docs(walkthrough): 41 and 81 pass — 98 items done; todo: the BOT leg's on-off label (c72294e)
- feat(callbacks): dialling from a callback row records on it, never completes it (ef8ea97)
- feat(callbacks): icon actions on a callback row; dial back or release it (a80d4e5)
- feat(seed): mobile_support may take_message rather than push to an agent (fc7f7f3)
- docs(walkthrough): 73-75 publication history, all pass (92c6e90)
- fix(designer): built-in tools listed, persona follows the UI, hangup_call renamed (dc4169a)
- fix(designer): unoverlapped labels, a graph that fits, dashed global edges (bc41c4f)
- docs(walkthrough): 61, 62, 64 pass; 60 records three defects, 63 left to the eye (e1e2ee0)
- docs(walkthrough): 76 and 77 pass (f07e574)
- docs(walkthrough): 5.3 push, 93–97 all pass (f65af89)
- docs(walkthrough): 5.2, outbound and hang-up, all pass (8e4bea0)
- fix(api): stop telling a machine with an API key to refresh a session it never had (e97a658)
- docs(walkthrough): 54–59 load-time validation all passes, with a script for all six (8b5a598)
- docs(walkthrough): 52 and 53 written up, each with a pitfall in the query (2ce77fa)
- feat(supervisor): the wallboard gains "in queue", and queue names stay put (51d3c8c)
- docs(walkthrough): 43 and 44 pass; 44 gets the two commands that tell them apart (dbd383e)
- docs(walkthrough): 49-51 pass; the transcription item rewritten to what was measured (45f36e0)
- fix(supervisor): drop the always-empty Consoles live card; 0 means "unlimited" (0079e71)
- feat(agent): the contact card shows the call ID, short on screen, copied whole (8ef18d5)
- docs(walkthrough): 37–40 pass (my calls, recording playback, contacts and notes) (bd9f37b)
- docs(walkthrough): 36 walked through, and where to look up the caller id (10e28c9)
- docs(walkthrough): 34 and 35 walked through (a2983e2)
- docs(walkthrough): the DTMF step gets a way to verify at the far end (8a8fb0a)
- feat(agent): cut the conference button (6119c99)
- fix(transcript): tool rows show the tool name, not "requested" and "answered" (9e4b5c8)
- docs(walkthrough): 29 and 30 pass (0db46ae)
- fix(flows): the transfer script says "transferring you now", not "hang up" (f934c81)
- fix(bot): a transfer to an agent may only enter this number's own queue (8c50de5)
- fix(bot): a terminal phase no longer overrides an already-armed transfer (9ef2dbf)
- fix(bot): transfer to an agent no longer makes the model guess a queue name (f826284)
- docs(walkthrough): number the items from 27 on so progress lines up (1b797b9)
- feat(agent): "My queues" lists queues, and an empty queue shows 0 (28c3c04)
- docs(walkthrough): all 26 items in the first section walked through (759bca9)
- refactor(numbers): layer the direction group by what it means to the business (7fbd31d)
- fix(numbers): the number form sends what it shows; default outbound moves on claim (8d80f40)
- feat(admin): staff a queue with agents; row actions become icons (66d7915)
- docs(walkthrough): calibrate section 1 against the real screens, 1-10 pass (6cbd6d3)
- fix(admin): a reset extension password must be visible, and copying must say so (2b4bedd)
- docs(walkthrough): close four gaps so the document can start on its own (451ce5c)
- docs(bot): five classic use cases and a backend bring the bot into the walkthrough (360a536)
- docs: a feature walkthrough for the three roles (567f634)
- fix(ci): give the build gate a database, because a third of the s...