Changelog¶
All notable changes to Gaby are documented in this file.
[Unreleased]¶
[0.12.1] - 2026-07-24: triage plan no longer truncates to nothing¶
Fixed¶
- The triage plan (ADR-0009) silently produced nothing on real tickets. The
emit_plancall was capped at 512 completion tokens; a 3-5 step plan with fallbacks needs ~520 on a verbose ticket, so it hitstop_reason=max_tokensmid-stepsand came back empty (confirmed live against Haiku; investigation 019f9136). The cap is now 1024, matching the other structured calls. Fail-soft behaviour was correct, so investigations still ran, just without the plan.
[0.12.0] - 2026-07-23: the investigation loop stops burning budget on dead ends¶
Root-caused from a prod investigation that cost $3.69 and escalated with nothing, having never looked at the customer's Keycloak logs. Five defenses, in depth, so a broken tool or a dead end costs cents and a useful escalation rather than dollars and silence.
Added¶
- Repeated-failing-call circuit breaker (#55). A per-tool attempts ledger blocks a tool after 2 deterministic server errors, 3 failures of one class, or 4 total, and drops it from the planner's tools. A prod-broken resolver is now dispatched at most twice, not 17 times, and the loop routes to the tools that still work. The ledger rides the planner envelope, so the planner can name a broken tool in its escalation.
- Triage plan at investigation start (ADR-0009). One cheap Haiku call classifies each support ticket and drafts a 3-5 step, ticket-specific plan (each step with a fallback), rendered in the planner envelope as advisory guidance. It gives the reactive loop a spine so it works the arc, logs included, instead of concluding after one lookup. Fail-soft like the namer; alerts keep their own structured flow.
- Progress watchdog. After 4 consecutive iterations with no new evidence, the loop forces a diagnostic verdict, synthesised from the attempts ledger so it names the broken tool, instead of thrashing on to budget exhaustion.
Changed¶
- The planner's own tool arguments are dispatched (ADR-0008). Collapsed the planner/tool-selector split: the planner's arguments are validated against the tool's JSON schema in code and dispatched directly, with no second LLM call on the happy path. The selector is demoted to a one-shot repairer invoked only when validation fails. Removes a whole LLM call per tool step and the 16.8k tokens a stuck run burned re-deriving arguments it was never given.
- Budget floats in the planner envelope are coarsened (cost to cents, time to a 10s bucket) so sub-cent / sub-bucket jitter no longer rewrites the envelope every turn.
Fixed¶
- A cross-tenant hole in the tool-call path. Safety checked the planner's arguments while the selector's, possibly different, arguments were dispatched. With the split collapsed, the arguments safety approves are the arguments dispatched, and any repair is re-checked against the tenant pin before it runs.
[0.11.0] - 2026-07-23: the investigation budget is editable in Settings¶
Added¶
- Per-investigation budget caps are configurable (Settings > Behavior),
no redeploy. Max tokens, cost, seconds and iterations, resolved store > env >
default like every ADR-0006 setting. Previously hardcoded at 150k tokens, so
a deep investigation halted with
failed_budgetand there was nowhere to raise it. Adds a generic non-secret config API (/api/settings/config/{group}) and afloatsetting type.
[0.10.2] - 2026-07-22: tenant pinning no longer denies a customer's own cluster¶
Fixed¶
- A ticket about your own cluster could be denied as cross-tenant. The pin
captured whichever identifier a call happened to name, so resolving a cluster
by name (
new-image) and then reading it by id (9a14c05f...) looked like two tenants and every follow-up call was denied, leaving the investigation to escalate with nothing.pg_resolve_customerreturns the whole customer, so its result now seeds the pin with every identifier at once: workspace id, and each cluster's id, name and domain, and each realm route. The two forms of one cluster are the same tenant again.
[0.10.1] - 2026-07-22: the thread stops claiming Gaby is still working¶
Fixed¶
- The working-on-it reaction never cleared. It was added when an
investigation started and only ever replaced on
auto_resolved, so on a proposed, escalated or halted verdict the reporter's message kept saying work was in progress indefinitely, which is worse than saying nothing. Every terminal verdict now moves the Thread signal: answered, or a person has it. Which internal verdict produced the latter is deliberately not distinguished, and the draft itself is never posted to the customer. Sources with nothing to signal on (email, Zoho) no-op.
[0.10.0] - 2026-07-22: a customer follow-up can ask for another look (ADR-0007)¶
Added¶
- One automatic re-investigation per ticket. A customer replying "that
didn't fix it" used to be classified, learned from, and then dropped: the
ticket reopened and nothing ran until somebody pressed Re-investigate. A
non-resolved customer follow-up now queues the ticket (
requeued) for one more run that reads the thread as it now stands. The second non-resolved follow-up deliberately does not run again; it leaves the ticket for a human. - Rejection costs autonomy. A machine-sent reply that draws a non-resolved
follow-up pins that ticket to
propose(tickets.autonomy_level), so the re-run is reviewed by a person before it reaches the customer. Gaby never sends a second unreviewed answer to someone who rejected the first.
Changed¶
resolve_autonomytakes the minimum of the ticket and source levels rather than the first match. A per-ticket level must only ever restrict, or the downgrade above would become a way to escalate autonomy. Stricter for every caller: it can only deny autonomy first-match would have granted.
[0.9.8] - 2026-07-22: an account lookup must be for the person who asked¶
Added¶
- Cross-customer lookup guard. Tenant pinning only started once a tenant was named, so the call that establishes it was unconstrained: an investigation could look up any account by email. A lookup now has to use the address the ticket came from. Combined with pinning, a support investigation is bound to its reporter from the first call rather than the second.
[0.9.7] - 2026-07-22: an investigation stays inside one customer¶
Added¶
- Tenant pinning. The scope DSL gated which tools may run and never
looked at their arguments, so nothing stopped an investigation resolving one
customer and then reading another into the same reply. The first tool call
that names a tenant (
workspace_id,cluster,cluster_id,namespace) now pins it; a later call naming a different one is denied and audited. Calls that name no tenant are unaffected, andalertsinvestigations are never pinned since reading across the platform is their job.
[0.9.6] - 2026-07-22: the planner can see who is asking¶
Fixed¶
- Investigations escalated with "customer cannot be identified" even when the
ticket had an email. The envelope carried only
customer(a display name), so the planner passedemail="Guilliano"to the resolver and matched nothing. The envelope now carriesrequester_email, which is the actual account lookup key.
[0.9.5] - 2026-07-22: a source added from the UI actually polls, and Slack says "on it"¶
Fixed¶
- A ticket source added through the UI never polled. Creating a source
registered its MCP connector (so the card said "Connected") but started no
poller, and
TicketPollerRuntimeonly knew how to start every source at boot. The source sat there connected and ingesting nothing until the next restart, which is indistinguishable from a broken integration. Creating or deleting a source now starts/stops its poller directly. - Slack tickets carried no requester identity. The mapper hardcoded
requester_email: nulland stored the raw Slack user id (U06DWNLAP6Y) as the customer, despite the app holdingusers:readfor exactly this. The server now resolves the asker's real name and email (cached, on fetch rather than the 3s socket ack path). The email matters beyond display: it is what lets an investigation resolve which customer/workspace is asking.
Added¶
- "Working on it" acknowledgement. An investigation now signals on the
source as soon as it picks a ticket up, before any of the slow work: the
Slack server gained an
acknowledgetool (a reaction, default:eyes:) and adapters advertise it viacan_acknowledge. Previously a thread showed nothing at all until the reply landed, so a multi-minute investigation looked like it had been ignored. Cosmetic by design: failures never touch the run.
[0.9.4] - 2026-07-22: ticket sources can be deleted again¶
Fixed¶
- Removing a ticket source returned 500.
replay_sessionsandticket_source_cursorsboth foreign-keyticket_sourceswith no delete rule, so the row could not go while either child existed. The poller writes a cursor for every source it polls, which made any source that had ever run undeletable from the UI. The delete now clears the dependents (replay events, replay sessions, cursor) in the same transaction. Replay history is scoped to the source it replayed and what it produced already lives in the memory graph, so nothing learned is lost.
[0.9.3] - 2026-07-22: MCP ticket sources get their conversation back¶
Fixed¶
- Slack (and any MCP-backed source) investigated blind. The investigation
worker built its thread-fetch adapter without the MCP host or cursor store, so
build_adapter_from_sourcereturned None formcp_*kinds and hydration silently no-opped: the loop never saw the channel conversation and attachments were never ingested. Same failure mode as the earliersettingsomission it sits beside. The consumer now threadsmcp_hostthrough to the worker.
[0.9.2] - 2026-07-22: dark-mode fix for the ticket-source picker¶
Fixed¶
- Registry badges were unreadable in dark mode: the AVAILABLE / CUSTOM / COMING SOON badges set a light background with no dark variant, so light text landed on a light background and the labels washed out during onboarding. They now use the same dark treatment as the connector status badges.
[0.9.1] - 2026-07-22: Postgres fixes for the live deploy + admin config registry¶
Two Postgres-only failures that shipped in 0.9.0 are fixed. Both were latent in the prod deploy (they only fire when the affected page is opened), and both were surfaced by finally running the DB suite against real Postgres. Admin config also starts moving out of env vars and into the product.
Fixed: Postgres-only breakage in 0.9.0¶
- Costs rollup: the by-connector query selected a bare column that was not in
the
GROUP BY. SQLite tolerates that; Postgres rejects it, so Settings → Costs returned a 500 on a Postgres deploy. Now aggregates correctly on both. - Support-lead metrics: the investigation-duration query used SQLite's
julianday(), which does not exist on Postgres. Now dialect-portable (extract(epoch ...)on Postgres,juliandayon SQLite).
Added: admin config registry (ADR-0006)¶
- A setting registry plus a generic per-workspace store with
store > env > defaultprecedence. Secret values are envelope-encrypted at rest and never returned by the API, which exposes onlyis_setandsource. - First slice: the LLM provider key is settable in the bootstrap wizard and
in Settings → LLM instead of being env-only, and setting it hot-swaps the live
provider without a redeploy. Adds migration
0023_workspace_settings.
Added: test infrastructure¶
- The DB suite runs against real Postgres in CI (
test-postgresjob, one throwaway database per test). This is what caught both fixes above.
[0.9.0] - 2026-07-16 — self-observability, attachments, and Postgres-ready¶
Gaby's own health becomes first-class, investigations finally see ticket
attachments, and (found by taking the deploy live) the backend actually runs on
Postgres now, not just SQLite. Grilled scope in docs/plans/v0.9-observability.md
+ ADR-0005.
Added — observability (Tracks A + B)¶
- Error metrics:
gaby_llm_retries_total(by provider + status),llm_provider_errors_total,llm_fallbacks_total,alert_webhook_events_total(ingested/suppressed/skipped/resolved/rejected),verdict_reinvestigations_total,environment_corrections_total, andinvestigations_failed_total(by reason). A Gaby Grafana dashboard ships via the infra repo. - System-status page: admin
GET /api/system/status(providers + fallback state, worker heartbeats, connector states, queue depth, alert-webhook state, recent LLM errors + failed investigations) and a Settings → System health tab.
Added — ticket attachments (Track E)¶
- Investigations finally see files. Attachment bytes live in a content-addressed
disk store behind an
AttachmentStoreinterface; each attachment gets a bounded, secrets-redacted ingest summary (migration 0022) that the envelope lists (cache-stable). Aread_attachmentbuiltin tool serves bounded redacted slices on demand. Zoho fetch ships; PDFs are text-extracted (pypdf), images stored-not-analysed.redact_secretsstrips tokens/keys/passwords/PEM while keeping identifiers (emails/IPs) — the working material of an investigation.
Added — resilience + control¶
- Cross-provider fallback (ADR-0005): when a routed provider exhausts its
transient retries and the other provider is configured, one call falls back to
the tier-mapped equivalent (opus↔sol, haiku↔gpt-4o-mini), audited as a
model_fallbackevent; disable withGABY_LLM_FALLBACK_ENABLED=false. - LLM route persistence (migration 0021): Settings → LLM model routes survive a restart (they were in-memory) and republish to the router at boot.
- Alert-suppressions panel: view active rules + hit counts, add/remove, and confirm learned proposals (ADR-0004). EN + FR.
Fixed¶
- Runs on Postgres. Two bugs SQLite hid, found by taking the deploy live:
a bare
postgresql://URL resolved to psycopg2 (not bundled) —to_async_urlnormalizes it to asyncpg; andUTCDateTimebound timezone-aware values intoTIMESTAMP WITHOUT TIME ZONEcolumns — it now stores naive-UTC and reads back aware-UTC. Verified: the full app boots clean against Postgres 16. - Grace verdict no longer double-produces when the budget blows at the writeback boundary (a verdict already landed).
- Corrupt
sessions.operator_notesreads as unauthenticated instead of 500ing the auth path.
Docs¶
- ADR-0005 (cross-provider fallback), the grilled v0.9 plan, and the deployment
path in
docs/deployment/prod.md.
[0.8.0] - 2026-07-16 — investigation quality + alert sources¶
Two waves in one release. First, the investigation loop got measurably better at actually investigating (found by dogfooding two real failures). Second, internal infrastructure alerts became a first-class source category with their own end-to-end pipeline (ADR-0003/0004). Version note: no 0.7.0 tag exists; the v0.7-numbered UI work shipped inside 0.6.0 and this release matches the v0.8 plan docs.
Added — alert sources (v0.8 Tracks A-D)¶
- Source category (
support|alerts, migration 0019). An alerts-category investigation produces an ops assessment (Status / Cluster / Severity / Recommended action / On-call header + grounded prose, addressed to an engineer), never a customer reply. - AlertManager webhook receiver (
POST /api/webhooks/alertmanager, bearer-token auth, disabled when unset). Firing alerts become investigations; fingerprint dedup;resolvedcloses the ticket; re-fires re-open it. - Severity floor + judgment: info→suppress, warning→assess,
critical→notify (never silently dropped). Gaby pages on-call (via the
existing escalation channels) only on a confirmed real + urgent issue, and
the verdict's Status is strict:
realneeds the alert condition to still hold AND log corroboration. - Suppression (ADR-0004): an operator suppress-list
(
/api/alert-suppressions, admin) plus learned proposals from repeated dismissals (never auto-applied). Suppressed alerts are recorded, counted, and marked in the inbox, but never investigated; a critical is never suppressed; un-suppressing restores investigation on the next fire. - Alert grounding steer: alert investigations query the impacted cluster around the alert's start time, filtered by its labels, and re-check the alerting condition before concluding.
- Investigation names: a cheap LLM call titles each investigation from the ticket (migration 0018) — the page shows "Admin Console SSO Access Missing In New Realm", not a uuid.
Changed — the loop investigates before it concludes¶
- Planner runs on Claude Opus 4.8 with a forced tool call (never prose), after model research across Anthropic/OpenAI/Gemini on agentic tool-use.
- Environment pinning: Gaby learns valid environment names from
list_environments(and successful calls) and reconciles every tool call'senvironmentargument — a wrong guess is auto-corrected (one valid env) or rejected with the valid options, so doomed calls never fire. Fixes the 6x-deniedenvironment="production"loop observed live. - Reinvestigate guard: a verdict that gives up for lack of information
with live tools available and zero tool calls is routed back to planning
once, and on that re-plan
produce_verdictis removed from the planner's toolset — investigate or explicitly escalate. - Transient LLM failures retry with backoff (529 overloaded, rate limits, 5xx, connection drops; honours Retry-After) instead of halting the investigation.
- Multi-provider dispatch: the Settings → LLM model routing now actually
takes effect (it was cosmetic), and
gpt-*/o*models dispatch to the OpenAI provider whenOPENAI_API_KEYis set —gpt-5.6-solis selectable as the planner for head-to-head testing. Per-model API quirks live in a pure, tested ModelProfile layer (llm/profiles.py); reasoning models (gpt-5.x / o-series) run on OpenAI's Responses API so reasoning and function tools work together.
Added — ops UX¶
- Decision-loop timestamps per step + Started/Finished in the header; "Cost: pending" while a run is live (no more misleading $0.0000).
- Real brand logos for connectors + escalation channels (simple-icons + the real Slack mark), replacing hand-drawn shapes and emojis.
- Inbox flags alert items with a distinct Alert badge; suppressed items get a neutral pill.
- The ticket page no longer shows the drafted reply twice.
Build / deploy¶
- Node + the Slack ticket-source MCP are bundled into the backend image
(the registry spawns it as a subprocess; the image was Python-only, so
Slack sources configured in the UI could never start in Docker). Compose
passes
SLACK_APP_TOKEN(Socket Mode) andGABY_ALERT_WEBHOOK_TOKEN. - Production deployment docs (
docs/deployment/prod.md) + the infra PR deploying Gaby to prod-us and ovh-dev.
Docs¶
- ADR-0003 (alerts as a source category), ADR-0004 (suppression is human-gated), the v0.8 alert-sources plan (grilled, all tracks shipped), and the v0.9 self-observability draft.
[0.6.0] - 2026-07-15 — online grounding + source presets¶
Gaby can now ground answers on the open web, not just the ingested knowledge base: a tiered retriever cites official docs first, operator-trusted domains second, and the open web last (propose-only). Plus a UI + output-quality pass from dogfooding on real Skycloak tickets.
Added — tiered online grounding (ADR-0001)¶
- Three-tier retrieval. Tier 1 = ingested KB (citable, auto-sendable);
tier 2 = operator-trusted domains (auto-sendable,
web://tokens); tier 3 = open web (propose-only, never auto-sent).TieredRetrieverfuses the tiers by RRF; the loop opts in per workspace. - Tavily open-web provider.
TavilySearchProviderdoes one search call (no scraper), carries Tavily's real relevance score, and scopes tier 2 viainclude_domains. BYOK: the Tavily key lives per-workspace in a SecretBox envelope with an env fallback. - Per-workspace Grounding settings tab. Enable/disable online grounding, manage the trusted (tier 2) + community (tier 3) domain lists, and set the Tavily key (write-only, never echoed back). EN + FR.
- Grounding source presets. One-click curated bundles (Keycloak, OAuth/OIDC, SAML, identity providers, security) seed trusted + community sources split by tier, so an operator can arm IAM grounding without hunting URLs.
- Banded thinness signal (A2b). Vector cosine relevance is surfaced through
the fusion and gated by a cosine floor/ceiling plus an
LlmRelevanceJudge, so a thin-but-present KB no longer masks a real gap.
Added — ops UX¶
- Glanceable inbox status. Each row shows a colour-coded status pill with a live pulsing dot on the states that need the eye (Gaby investigating, a fresh proposal, a customer waiting on a human) and calm colour on resolved/closed. Motion means "act now"; colour means severity.
- Outcome card splits the reply from the explanation. The banner renders
the "what happened / why / what's next" as prose and the drafted reply in its
own labelled block (
outcome_replyon the API).
Changed¶
- No raw Markdown reaches a customer. Replies are stripped to plain text on
send across every channel (Zoho, email, Slack/MCP): links collapse to
text (url), emphasis/heading/code markers are removed. Honours the long-standing "Markdown is stripped on send" prompt contract. - Ask answers render. The operator Q&A answer renders as Markdown with its
noisy inline citation tokens folded to compact
[n]footnote refs. - Vendor-integration planning. For "why did X fail" integration tickets the planner now searches the vendor's own docs in the vendor's terminology before escalating (for example "Sign In with LinkedIn using OpenID Connect").
- Dark mode is readable.
dark:variants across every light-tone card and status box (outcome banner, SLA board, verdict tones). - No em-dashes in Gaby's generated replies/answers or UI copy.
- Navbar version reflects the current git-derived version instead of a stale placeholder.
Fixed¶
- Order-dependent flake in the ticket-consumer test (file-backed SQLite plus the FTS5 table created in the fixture).
Docs¶
- ADR-0001 (tiered online grounding), ADR-0002 (grounding source suggestions), and the v0.7 grounding-source-management plan (presets + the ticket-driven suggestion loop).
[0.5.0] - 2026-07-15 — review→learn loop + backtest harness¶
The review→learn wave: Gaby learns from how its drafts get edited, and can be graded against a help desk's real history before being trusted with it.
Added — learning loop¶
- Answer-time consumption of workspace memory. Replay's style
profile and resolution patterns were write-only; working memory now
carries
style_profile(the verdict authors replies in the team's voice, with a reply-in-the-customer's-language override) and up to 5resolution_patterns(planner shortcuts — verify-first, provisional ranked below active). Loaded once at start, snapshot-stable on resume. - Final replies are persisted (migration 0014).
send-replyrecords the full sent markdown, a whitespace-insensitivewas_editedflag,sent_at, and the operator's optional edit reason on the drafting investigation. - Edit-distiller. When the sent reply differs from the draft, one
feedback_distillercall extracts typed deltas with tiered trust: style deltas auto-merge (human edits win), customer facts landproposed/active under the raw ticket customer key, patterns stayprovisionaluntil a second independent sighting. Fire-and-forget; can never break the send path. The fact loader also gained acustomer:-prefix fallback, un-orphaning replay-imported facts. - Follow-up learning. The poller no longer skips modified known
tickets: new thread messages append to the conversation; an
out-of-band human reply (sent from the help desk, not Gaby) becomes
the final reply and feeds the distiller; customer follow-ups are
classified (
followup_classifier) and high-confidence outcomes reinforce or decay the patterns the run used (decay = sightings subtraction, archive at zero — the graph never downgrades status on write). Non-resolved outcomes reopen the ticket; aticket_followupevent resurfaces it in the inbox. Nothing auto-runs. - Grounded propose drafts.
propose_resolutionrequires ≥1 citation or an explicitno_kb_basisdeclaration. - Review UX. Editing a draft reveals an optional "why did you change
it?" one-liner (sharpens the distiller; never sent to the customer);
after send, a sent-reply card with an edited chip replaces the editor;
Settings → Memory gains a "Recent learnings" spot-review panel
(
GET /api/memory/recent); inbox toasts on follow-ups. EN + FR — and parity is now enforced by a vitest test.
Added — backtest harness¶
gaby backtest import / judge / report. Imports the most recent closed tickets (human replies ride along as the judge's reference, never shown to the loop), lets the normal investigation pipeline draft ininvestigatemode, grades each draft on a frozen rubric (backtest_judge; resumable cases file), and emits the release-gate results JSON + a side-by-side HTML compare report. Headline metric: proposable rate (sendable + light-edit share of drafts). Gate suiteskycloak-backtestisoptional_until: v0.6.0(baseline-then-gate).fetch_historypagination. The Zoho adapter pages past the old 50-ticket cap (fromoffset), withoffset=for resumable batches andnewest_first=for the backtest's ordering.
Fixed¶
- Inbox SSE stream crashed on every event — the payload builder
walked
event.__dict__on slots dataclasses (AttributeError killed the stream on first fan-out); nowdataclasses.asdict. - Onboarding's Finish button was unreachable — the 6-step wizard's
footer still switched to Finish at
step < 7, so the last step showed "Next →"; now keyed toMAX_STEP.
v0.3.3¶
Everything below has landed on main since v0.3.2 (the v0.3.x feature
branches — themes, autonomy, full-thread context, OAuth + connector health,
Slack Socket Mode, two fix branches, and inbox + toasts — were merged onto
main in one pass). Tag as v0.3.3.
Added — connectors & remote MCP¶
- Remote/HTTP MCP transport (
mcp_httpkind). Gaby can now connect a remote MCP server at a URL (streamable HTTP), not just spawn stdio subprocesses — the natural fit for a consolidated internal MCP. Config carries__url+__headerswith${ENV}interpolation (so the bearer token is envelope-encrypted, never inline). - OAuth2 client-credentials auto-refresh for remote MCP. A remote MCP
behind OIDC re-validates the bearer's
expon every request (including the long-lived event stream), so a static pasted token inevitably 401s within its TTL.ClientCredentialsAuth(anhttpx.Auth) now holds the durable credential and mints + refreshes short-lived tokens itself — proactively (60 s before expiry, lock-serialised) and reactively (one re-mint + retry on a 401) — applied to both the POSTs and the GET stream. The remote-MCP form gains a Static-header vs OAuth2 (client credentials) auth toggle. - Connector health watchdog. A supervisor that dies after the handshake
(canonically: its token expired and the stream 401'd) now flips the
connector to
CRASHED, records the reason inlast_health_detail, and drops the cached manifest; the tool dispatcher only offers tools from healthy (READY/BUSY) connectors. So a dropped connector reads "Disconnected — 401" instead of a stale "ready · 21 tools", and the agent stops being offered dead tools. anyio-safe: the watchdog only observes task completion. - Connector catalogue frontend. Onboarding step 5 (previously a dead checkbox no-op) and the Settings add-flow now show a source-grouped card grid (First-party / Community / Custom). Community tiles show the spawn command read-only + collect only secrets; Custom and Remote-MCP tiles open their own forms. Brand-safe letter tiles (no vendor logos).
connectors/example-mcp/— a contract-tested reference MCP (the copy-paste starting point for the Custom path).
Added — ticket sources¶
- Slack ticket source over Socket Mode. Rewritten from the Events API
webhook (which needed a public URL, a signing secret, and six config fields)
to Slack Socket Mode: events arrive over an outbound WebSocket, so there
is no inbound endpoint and no public URL. The add form collapses to two
tokens (
SLACK_APP_TOKENxapp- +SLACK_BOT_TOKENxoxb-), created in ~2 minutes from a bundled app manifest (mcp-servers/gaby-mcp-slack/slack-app-manifest.yaml). Invite the bot to your support channel and every top-level message becomes a ticket; replies post in-thread.SLACK_CHANNEL_IDSis now optional (empty = all invited channels). Note: running the Node-based Slack server inside the Docker stack still needs the backend image to bundle Node — follow-up.
Added — agent & autonomy¶
- Per-source response modes. Each ticket source can run at
investigate/propose/act(the workspace default is capped atpropose). The propose path drafts a reply for human approval, with an editable draft and discard → reopen. - Full ticket-thread context. The live investigation now reads the whole ticket conversation, not just the opening message, so a fix late in the thread is actually seen.
- Re-investigate a ticket:
POST /api/tickets/{id}/reinvestigateenqueues a fresh investigation (prior runs preserved); the Ticket page shows the latest + a "previous investigations" list.
Added — UX¶
- Theme modes (light / dark / system) with a Settings → Appearance page, plus a Settings → Behavior page for autonomy defaults.
- Inbox ranks by real last activity —
MAX(received_at, latest investigation start/finish)for tickets and the latest message for chats — so a ticket investigated today rises to the top instead of sorting by its months-old source-creation date. - Toasts (sonner). Connector actions that were silent fire-and-forget
(response-mode change, connector restart/remove, ticket-source remove/test)
now confirm success and surface errors. App-wide
<Toaster>, EN + FR.
[0.3.2] - 2026-05-28¶
Ops-UX pass from live test-drive feedback. Three real bugs, three UX fixes, plus the connector catalogue opened.
Added¶
- LLM call recorder.
llm_callsand the cost dashboard shipped in v0.3 but nothing ever wrote a row, so the dashboard always read "No LLM calls yet".LlmCallRecorderwraps the provider and persists one row per call (success and failure), attributed to workspace + investigation via structlog contextvars. Ask and replay paths now bind the workspace so their calls attribute too. GET /api/inforeturns the running backend version (from hatch-vcs), edition, and build SHA. The navbar reads it instead of the hardcodedv0.1.0.devstring.- Open connector catalogue. Curated community MCP registry (GitHub,
GitLab, Linear, Atlassian, Slack, Notion, Filesystem, Cloudflare,
PagerDuty, Datadog) plus a
mcp_custom"bring your own" kind. The catalogue endpoint merges builtin + community + custom;POSTaccepts the wider set. Docs:connectors/community.md,connectors/byo-mcp.md,connectors/one-vs-many.md(the one-workspace-MCP recommendation). - Decision-loop outcome banner. The investigation page leads with a
plain-language, verdict-toned summary ("Gaby escalated this ticket to a
human because …") and renders each timeline step as a one-line summary
with a status chip, raw audit payload behind a chevron. No more reading
{"kind": "escalated"}JSON to learn the outcome. - Ticket page at
/tickets/:idshowing the ticket, its body, and a deep link into the decision loop.
Fixed¶
- Inbox → ticket click 404'd. The link used the ticket id in an
/investigations/{id}URL, but those are different uuid7 spaces. Inbox now routes to/tickets/:id, which resolves the investigation server-side viaGET /api/tickets/{id}/investigation. - Zoho tickets ingested with no body. Zoho's ticket-list endpoint
omits the body and the
descriptionfield is null for EMAIL-channel tickets, so the agent loop was investigating a bare subject line. The poller now hydrates the body from the oldest inbound conversation thread message (withdescriptionfallback), HTML stripped + entities decoded +<style>/<script>block contents removed. - Docker image reported
0.3.0.dev0in the navbar because hatch-vcs couldn't see.gitin the build context.make upnow derives the real version and threads it through compose'sGABY_VERSIONbuild-arg.
Changed¶
- Dashboard is now a top-5-tickets + top-5-investigations "at a glance" grid with state chips, instead of two long scrolling lists.
- SLA board redesigned: a summary chip row up top, only populated buckets get columns (sized to count so a busy bucket gets full width), each column scrolls in place, and cards link to the ticket with a relative SLA deadline.
- Removed every Gaby version reference from UI copy (locale strings no longer say "v0.1" / "v0.3 scaffold" / "not available in v0.3").
[0.3.1] - 2026-05-28¶
Added¶
- Investigation decision-loop viewer. The detail page now shows the
full loop Gaby walked and the reasoning at each step, read from the
hash-chained audit trail:
GET /api/investigations/{id}/trajectorymaps each audit event to a phase (PLANNING → … → VERDICT) + label and passes the payload (safety reason, tool + args, verdict) through, with the audit hash per entry for the tamper-evident view. This is the "verify, don't trust" surface; the data was always captured but nothing exposed it.
Changed¶
- Retired the dead
InvestigationSteptimeline — it was never written by the live loop. The step trail is the trajectory endpoint now; migration 0011 drops the always-emptyinvestigation_stepstable. - Honesty pass on the docs: removed unprovable "we run Gaby in production for our own customers" claims (it's not dogfooded yet) and the "solo-founder" framing (Skycloak is a small team); reframed around the real mission — low-touch support that escalates the right person.
Fixed¶
- Docs icons render: added
pymdownx.emojiso:material-*:shortcodes become SVG instead of literal text. - CI:
e2esmoke test curled/bootstrap/status; the backend serves it at/api/bootstrap/status— had been red on every push.
[0.3.0] - 2026-05-24¶
Post-Iter-18 — investigation goes real, multi-channel escalation, deploy story¶
The headline: the autonomous investigation path is now wired end to end. Before this, the worker ran on stub adapters and, in a fresh deploy, nothing consumed the ticket queue. Now a real ticket is claimed, investigated against real systems via MCP connectors, and answered.
- Real connector dispatch —
RealToolDispatcherbridges the agent loop'sToolDispatcherprotocol toMCPHost;build_real_loop_dependenciesassembles the real bundle; an in-processInvestigationConsumerRuntime(in the API lifespan, gated onGABY_ENABLE_INVESTIGATION_CONSUMER) claims queued tickets and runs the loop read-only by default. The demoFakeLLMProviderworker was retired. A seam test proves a scripted tool call actually reaches a connector and drives a verdict. - Multi-channel escalation — fan-out to Slack, email (SMTP), SMS (Twilio), Opsgenie, and SIGNL4. DB-backed channel store with envelope-encrypted config, per-channel test + failure isolation, a Settings tab, and a legacy single-Slack compatibility shim.
- Writeback works for all sources —
TicketWritebackAdapter(renamed from the misnamedZohoWritebackAdapter) works for Zoho, email, and Slack alike via the baseTicketAdapterprotocol. - Onboarding step 5 now reads
/api/connectors/catalogueso Redis/ Sentry/Stripe appear instead of a stale hardcoded list. - Bootstrap URL points at the web UI's public origin
(
GABY_PUBLIC_WEB_URL) so reverse-proxy deploys print a reachable link. - Docker build fix —
setuptools_scmversion pinned via build-arg so the backend image builds without.git. - Marketing → docs — the repo-root
index.htmlwas retired; the landing page is nowdocs/index.md, deployed togaby.skycloak.io. AddedBUSINESS.md(honest OSS-vs-commercial position). - Deployment docs — a first-investigation dogfooding runbook and a
Kubernetes deployment guide + raw manifests (
ops/k8s/) for running Gaby in-cluster with no VPN to internal systems.
v0.3 — open Gaby to channels and operators (Iters 11–18)¶
v0.3 is the "open Gaby to channels and operators" release. v0.1 made the agent loop real on tickets; v0.2 made it safe and observable; v0.3 ships the surfaces real teams actually want: an embeddable chat widget, Slack inbound, a Support Lead persona, read-only Redis/Sentry/Stripe connectors, an Ask Gaby read-only Q&A console, a Unified Inbox over every ingress, a cost dashboard, and a versioned MkDocs site. Plus a hardening commit that policy-excludes money-touching writes from the Gaby tool surface entirely.
Eval-graded autoresolution numbers (founder, support-lead, widget, slack suites) are deferred to v0.4 — the eval harness rebuild is the v0.4 headline iter. v0.3.0 ships behavior-tested code: 837 backend tests, 100% safety branch coverage, contract tests on every connector, honest gzip bundle measurement in the release gate.
Release plumbing — Iter 18¶
- Release gate hardened.
scripts/release_gate.pygzips bundles in-memory at level 9 instead of falling back to raw bytes (was misreporting the 12 KB widget as a 31 KB FAIL). CURRENT_VERSION bumped to 0.3.0 sooptional_until: v0.3.0markers flip. Bundle gate is real-measured for the first time. - release.yml builds the widget before the gate so the bundle
check runs against the published artefact. Adds a gated
npm publishstep for@gaby/widget(skipped untilNPM_TOKENsecret is provisioned).NODE_AUTH_TOKENflows through job-level env so the step-levelif:can read it —secrets.*is unavailable in stepif:conditionals. - Widget package.json is publish-ready — drops
private, addsmain,files,publishConfig: {access: public}, repository, license, homepage. README written for npm landing page. - Eval suites deferred to v0.4 —
release_gate.yamlmarks founder/support-lead/widget/slack asoptional_until: v0.4.0. The eval harness is structurally incomplete (1 LLM transcript vs. 15 fixtures; no--jsonon the investigate CLI). v0.4 ships the rebuild. - Perf baseline path corrected to
backend/tests_opt_in/perf/baselines/memory_graph.jsonafter the earlier "remove skipped/deselected tests" cleanup moved it.
Changed — v0.3 hardening: Stripe is read-only (money never inside Gaby)¶
- Removed
issue_refundand every other write tool from the Stripe connector. Money-touching actions (refunds, charges, subscription changes) are policy-excluded from the Gaby tool surface — the agent surfaces the case (charge, amount, eligibility, customer history) and a billing-role human takes the action in Stripe directly. BUILTIN_CATALOGUE["stripe"]drops thedefault_scopeswrite rule; read-only is the structural guarantee, not configurable.- New regression fence:
test_no_write_tools_at_allasserts every reasonable money-touching tool name stays out of the Stripe module. docs/operations/escalation.mddocuments the policy + the v0.4 roles taxonomy (support_l1 / billing / infra / security) + per-workspace channel routing landing next iter.- Refund playbook (
playbooks/refund-request.yaml) bumped to v2 — last step is "surface to billing human" instead of "queue refund for approval." - PCI scope doc updated:
issue_refundrows removed from the per-tool PAN-adjacency table.
Added (v0.3 master-plan Iter 17 — MkDocs docs site + cost dashboard)¶
- MkDocs Material site at the repo root (
mkdocs.yml,docs/). Single-source includes pullSPEC.md/ARCHITECTURE.md/FOUNDATION.md/CHANGELOG.md/CONTRIBUTING.mdfrom the canonical repo files — no fork, no duplication. Pages: Home, Concepts (5), Connectors (6), Operations (3), Reference (changelog), Contributing, Decisions (2 ADRs). - CI workflow
.github/workflows/docs.yml— build--stricton PR, deploymainalias on merge, versioned +latestalias on tag viamike. - Cost dashboard at Settings → Costs (SPEC OBS-5).
GET /api/settings/costs?window=7d|30d|90dreturns totals, by-purpose buckets, and by-connector buckets. Rolling windows (now - Nd). Cache- read tokens surfaced separately so operators see the main cost lever. observability/costs.py— three-query rollup. Critic P0 fix: by-connector attribution usesMIN(action.id)per investigation so a multi-action investigation doesn't row-explode its cost across connectors. Regression test for the multi-connector case.- EN + FR i18n — 16 new keys per locale under
settings.json.
Test infrastructure cleanup¶
- Removed the
-m "not slow and not perf"default-exclude addopt that hid 11 tests frompytest -qoutput. Theslow/perfmarkers were retired; their tests moved tobackend/tests_opt_in/(FalkorDB + AGE backend smokes, 9-cell round-trip matrix, scale benchmarks). Run viamake test-opt-inwhen Docker is reachable. falkordbis now a default dev dep so the optional import doesn't surface as aSKIPPEDline in the main suite.pytest -qnow reports828 passed, 0 skipped, 0 deselected.
Added (Iter 13a — MCP ticket-source contract)¶
- Ticket-source MCP contract (
connectors/_contract/ticket_source.md) pivoting ticket sources from first-party Python adapters to MCP subprocesses. 5 required tools (fetch_new_tickets,get_ticket,post_reply,mark_resolved,healthcheck) with opaque cursor semantics, webhook-port binding protocol, and overflow policy. - Shared contract test suite (
test_ticket_source_contract.py, 8 parametrised tests) + stub ticket-source MCP fixture. TicketSourceBridgeadapting the MCP tool surface onto the existingTicketAdapterProtocol — legacy poller unchanged, both cursor types (datetime + opaque string) coexist until Iter 14.secret_scrub.Scrubber— best-effort redaction of env-derived secrets from connector error messages. Word-boundary regex + URL/HOST denylist avoids false positives onKEYCLOAK_URL,API_HOSTetc.MCPHost.keepalive_ping+eager_startsurface for push-mode ticket sources (consumed by Iter 13b lifespan hook).- Supervisor task-boundary regression fence
(
test_mcp_supervisor_task_boundary.py) — the test the supervisor docstring previously referenced but which didn't exist. - Migration 0007 adding
webhook_port+last_seen_opaque_cursor, idempotent againstcreate_all-bootstrapped databases.
Tests¶
- 710 backend tests (up from 704 at 12b), 0 regressions. Safety still 100% line + branch covered. mypy strict + ruff clean.
Added (Iter 13b — Slack reference MCP + registry)¶
@gaby/mcp-slackTypeScript package undermcp-servers/gaby-mcp-slack/(Apache 2.0, 5 contract tools, Fastify webhook listener bound before MCPinitialize, HMAC + timestamp-skew verification, channel allowlist, in-memory ring buffer with overflow drop-oldest). Passes the full Iter 13a contract suite including real webhook-port bind.TicketSourceRegistry(gaby/connectors/ticket_source_registry.py) — spawns MCP ticket sources from DB rows, encrypts env, eager-starts at boot via a lifespan hook. PublicMCPHost.scrubber_for()replaced private-attr access./api/ticket-sourcesacceptskind="mcp_slack"withenv+webhook_portpayload; delete path stops the subprocess; test path routes toMCPHost.keepalive_ping.- Web
AddConnectorDialogroutesmcp_*kinds through the newenv+webhook_portAPI fields; catalogue includes MCP entries synthesized from the registry. - Iter 13b critic ceremony: 5 must-fixes applied (MCPHost leak on
handshake failure, public scrubber accessor, dropped unreachable
mcp_custombranch, docstring fix, webhook rate-limit docs).
Added (Iter 14 — Unified poller runtime)¶
TicketPollerRuntime(gaby/workers/poller_runtime.py) — one task per ticket source, discovers every enabled row regardless of kind and dispatches: MCP kinds route throughTicketSourceBridge, legacy (zoho_desk,email) build their Python adapter. App lifespan starts + stops the runtime behindGABY_ENABLE_TICKET_POLLER(off by default so tests aren't surprised).- Bridge datetime-cursor cleanup —
TicketSourceBridgeno longer writes a syntheticnow()intolast_seen_modified_at. Iter 13a critic P5 resolved; opaque cursor is the single source of truth for MCP sources.
Scope note¶
- Full TypeScript ports of Zoho Desk (OAuth + threading) and email (IMAP+SMTP + XOAUTH2) deferred to v0.4 — each is an iter of its own. Iter 14 ships the unification that lets those ports swap in transparently.
Added (Iter 15 — Ticket-source registry UI)¶
- Curated 13-card registry in the Add dialog: Slack / Zoho Desk / Email as Available; Intercom, Freshdesk, Zendesk, Help Scout, HubSpot, Linear, GitHub Issues, Discord, Microsoft Teams as Coming soon; plus a Custom-MCP power-user tile. Status derives from the backend catalogue so adding a future adapter auto-flips a card to Available.
- Custom MCP form — command input, args repeater, env repeater with
secret-key masking (
TOKEN|SECRET|KEY|PASSWORD), optional webhook port, amber warning banner ("runs arbitrary commands — trust only"). Length caps on all fields, stable row UUIDs so mid-list removes don't drop input focus. LogoTilefallback — gradient letter tile keeps Gaby out of vendor-brand-policy territory. Only Slack's public-lockup SVG ships; adding other vendor logos later is documented as a licensing step.- Backend —
ALLOWED_MCP_KINDSre-admitsmcp_custom; handler 400s whencommandis missing. One-line scope as planned. - Dev-time drift warning — if the backend catalogue surfaces a kind
not in the frontend registry,
console.warnso regressions surface. - EN + FR i18n — 12 new keys per locale (
registry_*,custom_mcp_*). - Iter 15 critic ceremony — 5 fixes applied (P0 Save-disabled bug, dialog width, drift warning, stable row ids, length caps). Visual verification via Playwright MCP — 3 screens captured, 0 console errors.
Added (v0.3 master-plan Iter 14 — Support Lead persona React port)¶
/support-leadReact route tree — dashboard (auto-resolution %, stats grid, avg resolution time, CSAT stats), playbook library, SLA board (4 buckets), CSAT review queue, escalation rules CRUD. Wrapped in a dedicated two-column shell with a scoped violet accent (distinct from the app-wide indigo), 5th top-nav item slotted between Approvals and Settings.- Escalation rules tiny DSL (
gaby/escalation/rules.py) — hand-written recursive-descent parser + evaluator. Grammar:if <cond> (and <cond>)* then <action>. Operators== != < > <= >= in. Durations30s 5m 2h 1d. Actionsslack_channel("#x"),escalate(),assign_to("user"). Tokenizer hard-capped at 4096 chars for defense-in-depth. Evaluation never raises on missing fields — returns None. 21 unit tests. - CSAT responses + escalation_rules tables (migration 0008). CSAT
admin-gated via
POST /api/csat; v0.4 will add a second short-lived-token route for widget-originated submissions instead of weakening this one's auth. - Playbook library — YAML files under
playbooks/resolved viaGABY_PLAYBOOKS_DIRenv var (Docker-friendly) with repo fallback. Three seeds: account-merge, bulk-user-import, refund-request. - Plain-language toggle scaffold —
useAnswerStylehook persists preference in localStorage withpick()fallback. Backend prompt rewrite that populatesreply_plain_markdowndeferred to v0.3.1 so verdict-eval pass rates don't regress mid-iter. - Backend API:
/api/support-lead/{metrics,sla,csat-queue,playbooks} - full escalation-rules CRUD +
/api/csat[,/summary]. Metrics query collapsed to 3 round-trips via GROUP BY (was 7). Two sub-routers with/api/support-lead+/api/csatprefixes for namespacing consistency. - i18n — new
support_leadnamespace, 40 keys EN + FR, full translation verified in Playwright (FR nav reads "Responsable support", hero reads "RÉSOLUS AUTOMATIQUEMENT", etc.). - Vite proxy fix — swapped
http://localhost:8080tohttp://127.0.0.1:8080so Docker's own ipv6:8080listener can't intercept dev API calls. - Iter S critic ceremony — 5 P0/P1 fixes applied (playbook path env override, metrics query collapse, DSL tokenizer length guard, router prefix consistency, CSAT docstring clarification).
Tests¶
- 753 backend (+40: 21 DSL + 6 playbook loader + 13 API) + 19 web (was 14 — +5 useAnswerStyle) + 32 contract + 16 Slack MCP unit
- safety still 100%.
Added (v0.3 master-plan Iter 15 — Redis + Sentry + Stripe connectors)¶
connectors/redis/server.py— read-only MCP with 5 tools (healthcheck,info,get_key,list_keys,memory_stats). Absence of write tools is the guarantee, not a runtime flag.list_keys(pattern="*")refuses on keyspaces > 100k. All composite types (list/hash/set/zset) consistently cap at 100 entries with{truncated, total}metadata.connectors/sentry/server.py— read-only MCP with 4 tools (healthcheck,get_issue,list_recent_issues,get_event). Hard-fails (ScopeViolationError) on cross-project queries whenSENTRY_PROJECT_SLUGis configured.list_recent_issues(age=...)uses Sentry's DSL (24h/7d) with strict validation.connectors/stripe/server.py— 5 read tools + 1 simulate-only write (issue_refund).mode="execute"raisesNotImplementedErroruntil v0.3.1 lands the approval-queue replay mechanism;mode="simulate"computes remaining-refundable math against a live charge lookup. Usesto_dict_recursive()so nested StripeObjects flatten correctly.- Restricted-key probe — read-only probe via
Refund.list+Customer.list. Warns if the API key hasCustomers:Readscope beyond what Gaby needs. Cached per process; opt-out viaSTRIPE_DISABLE_KEY_PROBE=1. docs/security/pci-scope.md— SAQ-A scope statement. Enumerates every tool's PAN-adjacency (all: none) and the operator guidance for key rotation + restricted-key use.- Backend catalogue —
BUILTIN_CATALOGUEextended with all three; Stripe carries customdefault_scopesrestricting writes toissue_refundonly. - Contract tests — matrix now covers 5 connectors × 8 tests = 40
passing. Secret-leak heuristic given a minimum-length floor so
boolean env flags (
STRIPE_DISABLE_KEY_PROBE=1) aren't misclassified. - Iter 15M critic ceremony — 4 P1 fixes applied (Redis composite-
type truncation flags, Sentry
ageDSL correctness, Stripe read-only probe via Refund.list, Stripeto_dict_recursivefor nested objects).
Scope deferred to v0.3.1¶
- Stripe
issue_refund(mode="execute")— needs approval-queue replay surgery inagent/runner.py+SqlaApprovalWriter. v0.3 ships simulate-only; execute raises NotImplementedError with a clear pointer at v0.3.1. - Connector CI workflow stub (documented, not shipped).
Tests¶
- 789 backend (+36: 10 Redis unit + 12 Sentry unit + 14 Stripe unit) + 19 web + 40 contract (5 connectors × 8) + 32 Slack MCP unit + safety still 100%.
Added (v0.3 master-plan Iter 16 — Ask Gaby + Unified Inbox)¶
- Ask Gaby engine (
gaby/agent/ask/) — three-state read-only Q&A: RETRIEVING → ANSWERING → VERIFYING_CITATIONS. Structural no-writes guarantee: engine shipstools=[]to the provider, so there is literally nothing for the LLM to call. One retry on citation enforcement failure with a tighter prompt, thenNoCitationsError. - Citation enforcement — every cited token must match a retrieved
chunk's
(document_uri, line_start, line_end)triple. Headings are intentionally drift-tolerant (model may shorten them). ask_conversationstable + migration 0009. Failed rows persist witherrorcolumn populated; history endpoint surfaces them with afailed: trueflag so users see drift.POST /api/ask+GET /api/ask/history— admin-gated, conversation_id is a grouping token (not multi-turn).GET /api/inbox— unified list over tickets + chat sessions via SQL UNION ALL with typed discriminator. Workspace-scoped; cursor pagination.GET /api/inbox/stream— SSE stream over shared EventBus. Workspace-scoped filtering; 15s heartbeat for proxy friendliness.- EventBus extension —
WidgetSessionStarted+ChatMessageAppendedvariants, published byWidgetSessionServiceat create + append-message points so Inbox SSE sees real live events. /askand/inboxReact routes + nav links. Filter pills on inbox (all/ticket/widget/chat), per-kind badges, live indicator, load-more pagination. History pane on Ask.- EN + FR i18n for both namespaces.
- Iter 16 critic ceremony — P0 fix (widget/chat events now actually publish) + 2 P1 fixes (request-as-None anti-pattern, failed-asks visible in history).
Scope deferred to v0.3.1¶
- Widget API-key management UI tab (operator key CRUD via direct API for v0.3; Chat settings tab lands with v0.3.1's docs iter).
- Ask LLM call
llm_callsaccounting integration for the cost dashboard — happens naturally via existinganthropic.pyaccounting, but Iter 17's dashboard reads the rows.
Tests¶
- 837 backend (+24: 4 Ask engine + 5 citer + 7 Ask API + 8 inbox API)
- 19 web + 40 contract + 32 Slack MCP unit + safety still 100%.
[0.2.1] - 2026-04-12¶
Polish release focused on making email onboarding work with modern providers without begging users for app passwords.
Added¶
- Gmail + Microsoft 365 OAuth for the email adapter. XOAUTH2 SASL
for IMAP and SMTP, token refresh with rotation handling (Microsoft
rotates, Google doesn't),
asyncio.Lock-guarded refresh to prevent token-endpoint stampedes. New/api/ticket-sources/email/oauth/*routes:providers,start,callback. No third-party OAuth library added — purehttpx+ stdlib. - Pre-commit hooks + Conventional Commits enforcement. Ruff, Biome,
file hygiene, conventional-commits at
commit-msgstage..gitmessagetemplate andCONTRIBUTING.mddocumenting the flow. - docs/testing.md — explains the unit / integration / slow / eval
tiers and what each one needs (Docker for AGE, the
falkordblitewheel for FalkorDB,ANTHROPIC_API_KEYfor the eval smoke).
Tests¶
- 554 backend tests (up from 538), 0 regressions. mypy strict clean. Ruff clean on all new and modified files.
[0.2.0] - 2026-04-12¶
Second release. Ships the history replay engine integrated into onboarding, full Settings tabs, SMTP nightly summary mailer, and connectors UX overhaul.
Iter 9a -- Email adapter + Zoho OAuth refresh¶
- Email-to-ticket adapter: IMAP polling with MIME parsing and threading
- Zoho Desk OAuth2 refresh-token rotation with encrypted storage
- Ticket source abstraction: unified interface across Zoho, email, and future adapters
- Admin API for ticket source CRUD and connection testing
Iter 9b -- History replay engine¶
ReviewEngineprocesses historical tickets through Claude HaikuReviewMemoryWriterextracts customer facts, resolution patterns into the memory graphStyleAggregatorbuilds a team writing-style profile from past responses- Cost estimation using the pricing table before user confirmation
- Replay session API: create (estimate), start, poll progress, get summary
- Background task processing with progress callbacks
Iter 9c -- Connectors UX overhaul¶
- Two-section layout: catalogue browser + active connectors
- Flow visualization showing connector pipeline status
- Improved connector cards with status badges and tool counts
- Responsive grid layout with accessibility improvements
Iter 10 -- Onboarding replay integration, Settings tabs, SMTP mailer¶
- Onboarding wizard expanded to 7 steps (was 6): new "Learn from History" step
- History replay wired into onboarding: cost estimate, start review, progress bar, summary
- Settings page with tabbed layout: Escalation, LLM, Memory
- LLM tab: model picker per purpose, API key status, budget defaults
- Memory tab: graph stats (node/edge counts), node counts by label
- Archive endpoint for memory nodes
- SMTP nightly summary mailer: HTML + plain text email via aiosmtplib + Jinja2
- Nightly summary template with stat cards and top-5 investigations list
- SMTP config: host, port, username, password, from/to addresses, schedule hour
- i18n: EN + FR translations for history replay step, settings tabs
- Settings nav link in the app shell
[0.1.0] - 2026-04-12¶
First public release. Ships a fully functional AI support agent that investigates tickets autonomously, resolves what it can, and escalates what it cannot.
Iter 0 -- Scaffold¶
- FastAPI backend with
/health,/ready,/metricsendpoints - First-run bootstrap URL for admin account creation
- 23 SQLAlchemy models, SQLite-backed
MemoryGraph - Alembic migrations, mypy strict, ruff linting
Iter 1 -- Connectors¶
- MCP connector framework: spawn, handshake, tool call, restart, shutdown
- First-party connectors: PostgreSQL, Keycloak
- 8-point contract test suite per connector
- Admin-only REST API and React UI for connector management
Iter 2 -- Knowledge¶
- Token-aware Markdown chunker with pluggable embedder
- SQLite FTS5 full-text search + brute-force vector store
- Hybrid RRF retrieval with citation tracking
gaby kb ingestandgaby kb queryCLI commands
Iter 3 -- Safety¶
- Scope DSL, authorization evaluator, hash-chained audit log
- PII redaction and approval queue
- 100% line+branch coverage on
safety/enforced bycov-safety.sh - Property tests for scope algebra and audit chain integrity
gaby audit verifytamper-detection CLI
Iter 4a -- Agent loop¶
- Homegrown state machine on
anthropicdirect SDK - Pure-transition functions with Protocol-based side effects
- 4-breakpoint prompt cache, BudgetGuard (tokens/USD/seconds/iterations)
- FakeLLMProvider with scripted replay
gaby investigateCLI for end-to-end loop execution
Iter 4b -- Memory backends¶
PostgresAGEMemoryGraphon Apache AGE with allow-listed Cypher helperFalkorDBMemoryGraphviafalkordb-py+ testcontainers- 9-cell round-trip test matrix (SQLite default, PG+AGE and FalkorDB slow-gated)
- Hypothesis workspace-isolation property test
gaby memoryCLI: export, import, migrate, forgetMemoryFactLoaderpopulatingapplicable_factsper investigation
Iter 5 -- Zoho Desk + worker + observability¶
- Zoho Desk polling adapter via httpx with respx-mocked tests
- Real MCP tool dispatch with anyio cancel-scope supervisor
- Dialect-aware worker claim (Postgres SKIP LOCKED / SQLite asyncio lock)
- Six Prometheus metrics, no-op OTel scaffold, structlog context
- Safety-denied writeback (zero HTTP POSTs to production Zoho)
Iter 6 -- React UI + wizard + Slack¶
- Founder onboarding wizard wired to real APIs
- Dashboard and investigation detail routes with live data
- Slack outbound webhook client (httpx + retry)
- "Remember this" memory API
- Six new backend routers (tickets, investigations, metrics, settings, test-slack, memory)
- 5 Playwright E2E tests against live Vite + FastAPI
Iter 7a -- Eval harness + release gate¶
- 15 YAML eval fixtures: 5 Founder scenarios x 3 variations each
- Promptfoo config with per-fixture assertions
release_gate.pyenforcing auto_resolution >= 60% and zero safety violations
Iter 7b -- i18n¶
- Full EN + FR translation for all UI routes
- i18next integration with browser language detection
- Language switcher component in the app shell
Iter 8 -- Release¶
- Refactored onboarding wizard: 6-step flow (use case, help desk, systems, AI, escalation, telemetry)
onboarding_completedtracking via localStorage- Dockerfiles for backend (Python 3.12 + uv) and web (Node 22 + nginx)
- GitHub Actions release workflow (tag-triggered, GHCR + PyPI wheel)
- Landing page CTA updated from "Join Waitlist" to "Install"
What does NOT ship in v0.1¶
- SMTP email notifications (Slack only)
- Helm chart publishing and SBOM generation
- Database-backed onboarding completion (localStorage for now)
- Real telemetry collection pipeline
- RTL layout support