Skip to content

Releases: rasonyang/ai-native-callcenter

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 16 Oct 03:09
1b405aa

Upgrade notes

  • Upgrade both images together. The switch image changes aicc_outbound.lua
    (it stamps and exports aicc_did on 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's campaign_id, the DID's
    max_outbound_channels, and a data correction that clears has_recording on
    CDR rows no stored recording backs (#196). Nothing is dropped or narrowed.
  • Breaking: the Gemini provider is removed. A deployment with
    AICC_PROVIDER=gemini no longer starts; choose openai, grok, qwen,
    gateway or doubao and 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 with aicc flowadd before moving traffic (see
    Changed).
  • The server now refuses to start when AICC_OUTBOUND_ENDPOINT is set to
    anything but a sofia/ dial string holding exactly one %s, when
    AICC_TRANSCRIBE_MODEL names 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 its start, pause, resume, cancel,
    contacts and attempts), the do-not-call list (/dnc-numbers) and
    post-call transcription (/calls/{callId}/transcription), under the new
    scopes campaigns:read, campaigns:write, dnc:read and dnc:write. The
    CDR gains campaignId, the DID gains maxOutboundChannels and
    isCampaignReady, POST /calls gains the refusals NUMBER_ON_DNC,
    DID_NOT_OUTBOUND and DID_CHANNELS_EXHAUSTED, and the event stream gains
    CAMPAIGN_UPDATED. Nothing was removed. A call that ends without a stored
    recording no longer reports hasRecording: 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, the AICC_POSTCALL_TRANSCRIBE_* family and
    XAI_API_KEY. .env.example lists each with its default. A deployment that
    keeps a do-not-call list and dials national numbers should set
    AICC_DEFAULT_COUNTRY_CODE; aicc doctor warns 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 can transfer_to_agent is 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:
    /campaigns with campaigns:read and campaigns: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 of AICC_BOT_INBOUND_RESERVE
    slots (unset keeps a small default share free). An answered outbound call
    that finds no free bot slot is held for AICC_OVERFLOW_HOLD (default
    1500 ms) and then hung up as OVERFLOW; a campaign whose answered calls
    overflow beyond AICC_OVERFLOW_TIGHTEN_RATE plans more cautiously, and above
    AICC_OVERFLOW_STOP_RATE stops over-dialing until the rate falls
    (AICC_OUTBOUND_MAX_RINGING bounds the legs ringing at once) (#158, #165).
  • Do-not-call list: GET/POST /dnc-numbers and DELETE /dnc-numbers/{number}
    (scopes dnc:read, dnc:write). A destination on the list is refused with
    422 NUMBER_ON_DNC on 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 with AICC_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_lead with DO_NOT_CALL) (#162).
  • Flow tool record_lead on campaign calls: the bot records the call's
    conclusion (intent HIGH, MEDIUM, LOW, NONE or DO_NOT_CALL, and
    notes) on the call's attempt. At the end of the call DO_NOT_CALL adds the
    number to the do-not-call list and HIGH files one callback on the
    campaign's follow-up queue. A campaign call is offered neither
    transfer_to_agent nor take_message. The seed gains the
    campaign_lead_followup flow on 95061 (English) and 95062 (Chinese); no
    campaign is seeded (#166).
  • Campaign reporting: the CDR carries campaignId and GET /cdrs filters on
    it, a webhook subscription can filter on campaignId, and CAMPAIGN_UPDATED
    events on the stream tell supervisors when a campaign moved (#167).
  • maxOutboundChannels on 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_EXHAUSTED with 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, and go test ./internal/flowsim runs
    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.md section 8 (#85).
  • AICC_OUTBOUND_RING_TIMEOUT (default 45s, at least 5s and 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_PLUS or NATIONAL, for a trunk that
    refuses the default. Campaigns store and compare E.164 whatever it is
    (#164).
  • AICC_DEFAULT_COUNTRY_CODE completes a national number for the do-not-call
    check and contact import. The compose stack sets it to 86; the
    application's own default is empty. aicc doctor gains a WARN status and a
    dnc_country_code check that uses it; a WARN does not change the exit
    status (#162).
  • AICC_PROVIDER=grok: xAI Grok Voice (grok-voice-3.0, key in XAI_API_KEY)
    as a profile on the Realtime client, with G.711 passed through untouched. The
    client now ignores the ping event, treats cumulative
    ...input_audio_transcription.updated as partial text, and counts a
    ...transcription.completed as final only when its status is absent or
    completed, so each caller utterance is one transcript line. A refused
    response.cancel with 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 trait IgnoresOverrideOnceTalking).
    aicc doctor and the installer (--provider grok) know the provider. Findings:
    docs/design/grok-findings.md (#226).
  • Flow tools can keep a list across their calls: collect on an HTTP tool
    appends a rendered value ({args.x}, {slots.y}) to a list slot after each
    successful call for which its optional when condition holds, without
    duplicates and at most 20 values. {slots.<slot>} renders the list and
    <slot>.count its size, so a transition can branch on it. Checked at load
    (docs/flow-authoring.md 2.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...
Read more

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 10 Oct 09:36
9e88146

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 optional PSTN_GATEWAY_PROXY setting.
    There are no new migrations; the database schema is unchanged.
  • API contract: POST /agent/login no longer accepts extensionNumber. 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 with CONFLICT; the agent signs out first. Presence gains the
    optional sipDomain. Nothing else was removed.
  • New settings, all optional: AICC_BOT_RTP_DEAD_TIMEOUT (default 5s),
    AICC_BOT_FIRST_MEDIA_TIMEOUT (default 30s),
    AICC_BOT_GREETING_MEDIA_WAIT (unset keeps the old behaviour), and the
    switch's PSTN_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_WAIT holds 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; 0 waits for
    the first inbound RTP however long; a positive value waits for it or that
    long. The logs now record first media and first bot audio
    (isBeforeMedia), and the metrics aicc_bot_first_media_ms and
    aicc_bot_first_audio_ms{before_media}, whether or not the setting is
    used (#98).
  • Deploy: AICC_BOT_RTP_DEAD_TIMEOUT and AICC_BOT_FIRST_MEDIA_TIMEOUT
    make the bot's two media timeouts configurable; 0 turns one off (#97).
  • Switch: PSTN_GATEWAY_PROXY sets the host and port the outbound
    Request-URI names, separately from PSTN_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.sipDomain is 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: the AGENT_* and
    DEVICE_* events, GET /agent/presence, GET /auth/me and the GET /agents roster. An agent who is signed out is told about their phone, and
    a DEVICE_* event is published only when the state changed. Sign-in is
    only at the bound extension (#106).
  • Switch: mod_audio_stream is 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
    fetches libwsc from our fork too. The image gains mod_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 no X-FSBC-Out marker (#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 to AICC_BOT_FIRST_MEDIA_TIMEOUT (30
    s) and logs media never started (#97).
  • AI calls: speech at the moment playback ends cancels the dead-air watch.
    NO_INPUT could 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 second repair_status lookup, 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's botSummary and botReason in
    the row's user data (#57).
  • CDR: an agent's call to a bot number is the agent's call. The row is
    OUTBOUND from 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_sec is 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: isDeviceRegistered was 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.0

Both 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

Choose a tag to compare

@github-actions github-actions released this 04 Oct 07:03
8166202

Upgrade notes

  • Existing flows without maxDurationSec now end AI calls after 15 minutes.
    Set "maxDurationSec": 0 in a flow's global to keep the old behaviour.
  • Upgrade both images together. The switch image adds a dialplan route and
    aicc_did_route.lua for an agent's call to a platform number, and sends the
    call's type to the bot as X-AICC-Call-Type, which the application now
    reads. The application adds migration 00034 (unbacked_claims on the CDR);
    migrations run at application startup.
  • API contract: the CDR gains the optional unbackedClaims array and the
    UnbackedClaim enum. 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 (or null) means 900 seconds, 0 turns 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
    reason SESSION_LIMIT when 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
    cause SESSION_LIMIT and 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 metric aicc_bot_unbacked_claims_total{claim,provider}
    and recorded on the CDR as unbackedClaims (TRANSFER, LOOKUP,
    MESSAGE, FAREWELL; database column unbacked_claims, migration 00034).
    It changes no call behaviour and not isContained; 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 is INTERNAL, any other number (a DID or an external
    number) is OUTBOUND. 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-demo the installer's closing message says how to create
    the first administrator with aicc 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 --wait gives the switch's aicc_bot gateway up to 45
    seconds to come back UP after the application is recreated. A provider
    change followed by a rerun could print FAIL BOT_GATEWAY_DOWN for a stack
    that was healthy by the switch's next OPTIONS probe.
  • Docs: deploy/one-line-installer.md now 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_agent or hangup again 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 a hangup
    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's queueId.
    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 with NO_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 is OUTBOUND from 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).
  • 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.0

Both 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

Choose a tag to compare

@github-actions github-actions released this 28 Sep 05:00
c3c5c4e

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_PASSWORD must 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 with FS_EXTERNAL_IP in the SDP, including phones on
    the switch's own LAN. On a Linux host, check that FS_EXTERNAL_IP is 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-plus and the
    profile's fallback voice longanqian_v3.1. A deployment that sets
    AICC_PROVIDER_MODEL keeps its model. There is no 3.1 flash model. Live
    transcription still uses qwen-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), without sudo on macOS (into ~/.aicc). It
    runs preflight checks that fail with a stable PREFLIGHT_* 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
    runs aicc doctor. Reruns repair without regenerating secrets;
    --external-ip, --upgrade (with a pg_dump backup first),
    --uninstall, --purge, --check and --no-demo cover the rest.
  • Release assets: install.sh, aicc-deploy-<tag>.tar.gz and
    checksums.txt on the GitHub release, built by scripts/release-bundle.sh.
  • Compose overlays: deploy/compose.linux.yml runs the application and the
    switch on the host network; deploy/compose.macos.yml runs them in one
    shared network namespace. deploy/compose.release.yml.in is the release
    overlay that pins the published images.
  • aicc doctor checks 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.
  • /readyz reports 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-acl aicc_sip_local, with
    aggressive-nat-detection on) 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 the aicc_bot
    gateway, is unchanged. Two-way audio verified live on Docker Desktop on an
    Intel Mac, on Colima with the grpc port forwarder (macOS overlay) and on
    a Linux host with the host-network overlay and a LAN WebRTC agent.

Fixed

  • lua-role could exit with "tuple concurrently deleted" on a fresh start,
    when its grants raced a migration replacing a luacc view. 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.0

Both 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

Choose a tag to compare

@github-actions github-actions released this 24 Sep 01:28
1362048

Upgrade notes

  • Upgrade both images together. The switch image changes its Lua scripts and
    callcenter.conf, and the application adds migration 00033, which recreates
    the luacc.directory view. Migrations run at application startup.
  • An unrecognised AICC_PROVIDER now stops the application at startup even
    when AICC_BOT_ENABLED is off. Previously it was only noticed once the bot
    was enabled.
  • API contract: the ErrorCode enum gains TERMINAL_ANNOUNCE_REQUIRED
    (returned with 422 when a flow is published). createCall declares the
    extra scope AI_OUTBOUND needs as x-body-scopes; the rule itself is
    unchanged. Clients that treat ErrorCode as 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) and AICC_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.closingTarget and global.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.count now counts consecutive silences.
  • Publishing a flow whose terminal phases have no announce is refused with
    TERMINAL_ANNOUNCE_REQUIRED on 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
    cause PROVIDER_SESSION_EXPIRED instead of MEDIA_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.md and deploy/production-checklist.md, with a
    troubleshooting section.

Changed

  • The six seeded flows carry their greetings and closing lines as announce,
    set closingTarget, and hang up on the first decline at wrap-up.
    novanet_support announces 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 named support@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.1

Both 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

Choose a tag to compare

@github-actions github-actions released this 14 Sep 08:09

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.0

Both 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

v0.1.0-rc.4 Pre-release
Pre-release

Choose a tag to compare

@rasonyang rasonyang released this 13 Sep 09:08

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.4

Both 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

v0.1.0-rc.2 Pre-release
Pre-release

Choose a tag to compare

@rasonyang rasonyang released this 13 Sep 06:47

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.2

linux/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

v0.1.0-rc.1 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 11 Sep 09:51

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...
Read more