All notable changes to @freewaretools/outercom.
This project adheres to Semantic Versioning. While the
major version is 0, breaking changes ship in minor releases.
- A truncated AI reply could leak the raw JSON envelope to the visitor.
Providers ask the model for a
{"reply", "escalate", "reason"}envelope andparseAIReplyextractsreplyfrom it. If the response was cut off (e.g. by the token limit) before the JSON object closed,extractJsonObjectcorrectly found nothing, but the leaked-control-text safety net only checked forescalate/reason— notreply— so the unclosed{"reply": "..."scaffold fell through and was shown to the visitor verbatim instead of triggering a clean escalation.looksLikeControlLeaknow also matches a leakedreplyfield.
Fixes four defects an adversarial review found in 0.5.0, each reproduced by a probe and now covered by a regression test. No API change.
- The AI could talk over a live human.
maybeResumeAi's "has an agent replied?" guard was session-scoped, but agents reply into a thread, and threads are deliberately shared across sessions (getThreadByEmailreuse). An inbound reply is recorded only against the session that owns the thread, so any other live session on that thread saw zero agent messages and would resume — posting AI replies into a topic a human was actively working. Resuming now additionally requires that the session still owns its thread. abandonSessionresurrected deleted topics. Its departure notice relayed throughmirror(), so a deleted topic was recreated, told "this chat continues here" (it doesn't — the next statements close it), then immediately closed. In the very scenario 0.5.0 targets (someone tidying up topics), every abandoned session spawned a fresh topic. Terminal notices now usemirror(..., {revive: false}).- The
startSessionintro was the one send that couldn't heal. It calledtransport.sendMessagedirectly and discarded theSendResult, so athreadGonethere was dropped and the session was persisted onto the dead thread. Harmless forTelegramTransport(whosereopenTopiccatches a deleted topic first, leaving only a narrow race), but systematic for any transport that returnstruefromreopenTopicbecause it has no reopen concept — which the interface explicitly permits. It now routes throughmirror(). - A synchronous storage throw permanently disabled revival. The in-flight
dedupe entry was installed after the async IIFE was invoked, and an IIFE runs
synchronously up to its first
await. A storage adapter throwing synchronously (an ordinary guard-clause pattern) therefore ranfinally'sdeletebefore theset, leaving a resolved-nullpromise cached forever — so every later revive for that session returnednulland dropped the message, resurrecting the original bug. The task is now deferred by a microtask so the entry is installed first. Latent for both Prisma-backed consumers (async, so they reject rather than throw).
-
A deleted transport topic no longer kills the conversation silently. Topics are reused per visitor email indefinitely, so once a topic was deleted upstream every relay failed with
Bad Request: message thread not found— and because sends are best-effort, nothing surfaced. The chat looked alive while every message to the team vanished. The engine now recognises a dead thread, recreates it, persists the new id, and retries the message. A returning visitor whose remembered topic was deleted gets a fresh one instead of being pinned to the dead id. Concurrent messages share a single revival, so a burst can't mint duplicate topics. -
Escalating no longer strands a visitor when nobody is watching.
livewas a one-way door: if no agent ever replied — because the transport was misconfigured, or nobody was looking — the AI stayed silent forever and the visitor got nothing. SeeaiResumeAfterMsbelow.
- AI prompt: don't escalate on gibberish. An unclear message, stray keystroke, or empty input now gets a warm "could you rephrase?" instead of burning a human handoff. Escalation is reserved for real questions.
ChatEngineOptions.aiResumeAfterMs(default10 * 60_000,0disables) — how long an escalated chat may sit unanswered before the AI picks it back up. Only ever fires when no agent has replied in that session, so it cannot cut in on a human who is simply typing slowly.TelegramApiErrorandisThreadGone(err)— exported so hosts can classify Bot API failures.isThreadGoneis deliberately narrow:TOPIC_NOT_MODIFIED,chat not foundand auth errors all leave the topic intact.- A test suite (
npm test), using Node's built-in runner viatsx.
Only relevant if you implement ChatTransport yourself; TelegramTransport
users are unaffected beyond the version bump.
ChatTransport.sendMessagereturnsPromise<SendResult>(wasPromise<void>). Return{ ok: true }on success. Return{ ok: false, threadGone: true }when the thread no longer exists to opt into recreate-and-retry; leavethreadGoneunset to keep the old behaviour.ChatTransport.reopenTopicreturnsPromise<boolean>(wasPromise<void>). Returnfalseonly when the thread is gone —trueif it reopened, was already open, or the concept doesn't apply. Returningfalseon a transient error will spawn duplicate threads.
In practice an un-migrated JavaScript transport keeps working: the engine only
acts on an explicit threadGone: true / reopenTopic() === false, so the
undefined a void-returning transport yields is read as "delivered" and
"alive" — exactly the pre-0.5 behaviour. TypeScript users get a compile error
and should adopt the new returns to gain self-healing.
- Relicensed MIT and moved to the public
@freewaretools/outercomon npm (formerly the private@lowvoltage/outercom). No functional changes.
- Menu-launched mode + server-trusted identity (
resolveVisitor).
- Offline-reply email + config-driven theming.
- Initial release.