Liaison is a self-hosted AI phone agent that calls customer support on your behalf and reports back entirely in text. It runs against your own Twilio and OpenAI accounts, and its core invariant is that nothing important ever exists only in audio.
This release closes out a full read-only audit of the codebase and makes the v1 protocol real rather than aspirational.
Try it without credentials
git clone https://github.com/kaushika05/liaison.git && cd liaison
npm ci
npm run setup -- --defaults --output=.env
npm run devNo Twilio account, no OpenAI key, no phone call. The deterministic simulator runs the whole product — intake, planning, one-time call authorization, live approvals, and an evidence-grounded outcome report.
The protocol is now served, not just published
The Universal Support Protocol schemas used to be generated and tested but unused by the running application. They are now returned by the API and validated at the boundary:
GET /api/cases/:caseId/execution-plan ExecutionPlan
GET /api/attention/:id AttentionRequest
GET /api/cases/:caseId/commitments Commitment[]
A new projection module is the single place where internal records become protocol documents, and every projection ends in a schema.parse, so a document that no longer satisfies the contract throws instead of shipping. An approved disclosure now also writes a metadata-only DisclosureEvent to the audit log, which survives the restart that clears the in-memory ledger.
Fixed
- A hang-up could silently fail to end a real call. After an ambiguous call start, the database learned the Twilio call SID from a signed callback but the relay adapter did not, so
endCallskipped termination and still reported success. The SID is now bound to the adapter, andendCallrefuses to claim success when it has neither a SID nor an open relay socket. - Disclosure release used a weak purpose check. It ran term overlap against the representative's own words, and both the account-number and order-number policies contain the word "identification", so they were not category-discriminating. Release now requires a closed, disjoint classifier to identify that card's own category; unrecognised requests fail closed.
- Tests could contact a real provider. The suite read the developer's local
.envand issued a billable OpenAI request. Provider-free defaults are now forced before any test imports configuration. CallService.shutdownno longer throws on an active call row the process never owned in memory.- Two unhandled promises in the client polling loop, found by newly enabled type-aware linting.
busy_timeoutso the retention job and backup tooling wait instead of failing withSQLITE_BUSY.- The session store is scoped to the app instance and cleared on close.
Removed
MAX_CONCURRENT_CALLS and UNAUTHORIZED_SENDER_RESPONSE are gone. Neither was read anywhere in the codebase. The first looked like a safety limit but concurrency has always been enforced by the single-active-call check, and the second implied that replies to unauthorized senders were configurable when they are always silent by design. Configuration that does nothing is worse than no configuration in a product like this.
ESTIMATED_SMS_COST_PER_SEGMENT_USD was in the same category and has been wired up instead: it now drives a running estimate over segments actually submitted to a carrier. Rejected inbound provider requests, previously recorded but never shown, are surfaced as a count.
Housekeeping
- The whole tree is Prettier-formatted, gated in CI.
- ESLint runs type-aware rules including
no-floating-promises. db.tssplit intoschema.ts,records.ts, and the query layer.defaultAuthoritymoved to a schema-free module, so the browser bundle no longer pulls in Zod through a value import — 590 kB to 517 kB.- Coverage tooling added (
npm run test:coverage).
Verification
Format, lint, typecheck, 152 tests across 24 files, the deterministic demo, and the production build all pass.
As always: passing tests are not live-provider validation. Nothing in CI registers a sender, sends a carrier SMS, places a PSTN call, or spends OpenAI credits.
Full changelog: v1.0.0...v1.1.0