Skip to content

Release: v0.6.1#1171

Merged
vybe merged 275 commits into
mainfrom
dev
Jun 12, 2026
Merged

Release: v0.6.1#1171
vybe merged 275 commits into
mainfrom
dev

Conversation

@vybe

@vybe vybe commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Release v0.6.1 — cumulative changes since v0.6.0 (108 commits, 34 issues).

Full notes: docs/releases/v0.6.1.md (ships in this PR).

Trinity v0.6.1

Released: 2026-06-12 · Tag: v0.6.1
Diff: v0.6.0...v0.6.1 · 108 commits · 34 issues · 6 contributors

A UI/UX-and-reliability release. Headline themes: the Operations area consolidation (Health + Ops + Executions in one tabbed view, #1109), the Agent Detail Overview dashboard (#1107), sequential agent loops shipped end-to-end (MCP tool #740 + web UI #1106), VoIP telephony Phase 1 — agents place outbound phone calls over Twilio Media Streams + Gemini Live (#1056), a fleet-wide secure-by-default access policy (#1129), and a model-catalog refresh adding Opus 4.8 and retiring EOL Gemini models (#1080, #1130, #1137).

This document lists every issue shipped in the release (all 34 status-in-dev issues closed on merge), grouped by type.

Features (14)

Fixes (15)

Refactors (5)

Other changes

Upgrade notes

Contributors: Eugene Vyborov, dolho, dependabot[bot], andrii.pasternak, obasilakis, oleksandr-korin


Closes #18
Closes #307
Closes #548
Closes #740
Closes #868
Closes #951
Closes #993
Closes #1017
Closes #1023
Closes #1026
Closes #1037
Closes #1042
Closes #1056
Closes #1068
Closes #1073
Closes #1076
Closes #1080
Closes #1094
Closes #1098
Closes #1101
Closes #1106
Closes #1107
Closes #1108
Closes #1109
Closes #1114
Closes #1121
Closes #1126
Closes #1129
Closes #1130
Closes #1137
Closes #1140
Closes #1143
Closes #1150
Closes #1153

Pre-release checklist passed 2026-06-12. Ready for squash-merge.

vybe and others added 30 commits May 5, 2026 18:08
…v workflow

Introduces docs/planning/TARGET_ARCHITECTURE.md — the optimal steady-state
design Trinity should converge toward (PostgreSQL, actor model coordination,
async-first agent communication, fleet observability, GuardAgent security).

Updates CLAUDE.md, DEVELOPMENT_WORKFLOW.md, and the groom/roadmap/sprint
playbooks to distinguish current architecture (what is built today) from
target architecture (where decisions should point), and to use target
architecture alignment as a ranking signal during backlog grooming and
issue prioritization.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(security): Encrypt Slack bot tokens at rest (#453)

The SLACK-001 public-link Slack integration (`db/slack.py`) was the last
holdout still storing bot tokens as plaintext in SQLite, violating
Architectural Invariant #12. Telegram (`telegram_bindings.bot_token_encrypted`),
WhatsApp (`whatsapp_bindings.auth_token_encrypted`), and SLACK-002
(`slack_workspaces.bot_token`) all already encrypt via `services.credential_encryption`
(AES-256-GCM, JSON envelope). This brings SLACK-001 in line with that pattern.

Additionally, `slack_workspaces.bot_token` was rolled out with lazy
encryption (encrypt-on-write + plaintext fallback at `slack_channels.py:47-49`)
which left two sources of plaintext on disk:
- Rows written before the encryption rollout
- Rows copied from `slack_link_connections` by `_migrate_slack_channel_agents`

A one-shot migration walks both tables on startup and re-encrypts any
plaintext `xoxb-*` rows. Idempotent at row level (skip JSON envelopes)
and at migration level (schema_migrations runner).

src/backend/db/slack.py
- Add `_get_encryption_service` / `_encrypt_token` / `_decrypt_token`
  (exact copy of the pattern in `db/slack_channels.py`,
  `db/telegram_channels.py`, `db/whatsapp_channels.py`)
- Encrypt at `create_slack_connection` (write site)
- Decrypt at `_row_to_connection` (read site, with `xoxb-*` plaintext
  fallback so runtime works pre-migration on legacy rows)
- Caller-facing API surface unchanged — `slack_bot_token` field still
  contains plaintext in the returned dict

src/backend/db/migrations.py
- New `_migrate_slack_bot_token_encryption` registered in MIGRATIONS list
- Walks BOTH `slack_link_connections.slack_bot_token` AND
  `slack_workspaces.bot_token`, encrypts plaintext rows in place
- Hard-fail on missing CREDENTIAL_ENCRYPTION_KEY (matches the implicit
  pattern of every other consumer of CredentialEncryptionService)
- Skips already-encrypted rows (signature: starts with `{` not `xoxb-`)
- Defensive: skips silently if a table doesn't exist

docs/memory/architecture.md
- Reword Invariant #12 to acknowledge channel/subscription tokens as a
  documented exception (persisted but mandatorily encrypted), with the
  full list of tables under that rule
- Add `slack_link_connections` DDL block; update `slack_workspaces`
  block to clarify the column-name vs content-type distinction

tests/unit/test_slack_token_encryption.py (NEW, 14 tests)
- TestRoundTrip: write encrypts, read decrypts, raw DB value is JSON envelope
- TestPlaintextFallback: legacy `xoxb-*` row returns token + warning logged;
  corrupt envelope returns None + error logged
- TestEncryptionHelpers: encrypt+decrypt isolation; encrypt raises ValueError
  on missing key; decrypt returns None on missing key
- TestMigration: encrypts plaintext in both tables, skips encrypted, idempotent
  on second run, hard-fails without key, no-op on missing/empty tables

Live verification on running backend:
- Migration ran on startup: 1 row in each table re-encrypted
  (the real `ability.ai` workspace data)
- On-disk now `{"version": 1, "algorithm": "AES-256-GCM", ...}` envelopes
- `SlackOperations.get_slack_connection` returns the original `xoxb-...`
  plaintext via decrypt — caller-facing API surface unchanged

Out of scope (filed separately):
- Encryption tests for slack_channels.py + telegram_channels.py +
  whatsapp_channels.py (all shipped without dedicated test coverage):
  tracked in #664
- Renaming `bot_token` → `bot_token_encrypted` columns: cosmetic, would
  require a real schema migration; current naming works behind the
  service-layer encapsulation

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(req): annotate SLACK-001 bot token as encrypted at rest (#453)

SLACK-002 (line 809) noted "bot_token encrypted"; SLACK-001 (line 781)
didn't, even after #453 brought slack_link_connections.slack_bot_token
under the same AES-256-GCM regime. Mirror the annotation for parity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by:  Pavlo <pash@pashs-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
56 checks across 10 categories covering static file checks, YAML/JSON
schema validation, security scanning, and AI-evaluable logical checks
(skill coherence, CLAUDE.md quality, cross-file consistency). Serves as
the canonical check list for the compatibility validation API and MCP tool.

Closes-adjacent: #668

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…NULL status row (#669) (#676)

`GET /api/monitoring/status` (the endpoint MCP `get_fleet_health` calls) was
returning 500 whenever any `agent_health_checks` row had `status = NULL`.
Root cause: the build loop did
    AgentHealthSummary(status=check.get("status", "unknown"), ...)
`dict.get(key, default)` only returns the default when the key is *missing*.
A row with the key present but value `None` returned `None`, which Pydantic
v2 rejected because `AgentHealthSummary.status: str` is required.

The sibling per-agent endpoint dodged this by triggering a fresh health check
when no aggregate row existed, which is why `get_agent_health` worked while
`get_fleet_health` 500'd in the production fleet that triggered #669.

Fix:
- Extract `_build_agent_summary(name, check)` and `_coerce_status(raw)` so
  NULL/missing/non-str status degrades to `"unknown"` consistently. Same
  for NULL `error_message` (would explode `.split("; ")`).
- Wrap the aggregator in try/except returning a structured "unknown" payload
  rather than 500 (issue ask #1). Future schema drift surfaces as data, not
  as an outage.
- Reconcile `docs/user-docs/operations/monitoring.md` — the listed
  `/api/monitoring/fleet-health` path doesn't exist; correct to
  `/api/monitoring/status`.

Tests: 7 new unit tests in `tests/unit/test_fleet_status_resilience.py`
covering NULL status, missing status key, missing check row, NULL
error_message, non-string status, and sort-key tolerance.

Does NOT fix the underlying scheduler stoppage (8 of 9 agents going months
without a health-check refresh) — that root cause needs production logs and
is split out as a separate ticket.

Closes #669 (symptom)
Refs #675 (scheduler stoppage follow-up)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#640 debugging (#662)

After diagnosing #640 against a running agent with an npx-launched stdio MCP
(@upstash/context7-mcp@latest), the issue body's "MCP child inherits fd > 2"
theory does not hold: Node.js spawn already isolates the MCP child to fd 0/1/2
on socketpairs. The remaining wire-corruption mechanism (some descendant
outside claude's pgid acquiring agent-server's pipe write-end via setsid+dup)
needs a live production trace to root-cause — single-session repro budget here
isn't enough to manifest the failure (issue says ≥100 turns / 18min).

What this commit does ship: make the next production failure usable by
surfacing the diagnostic data the existing #618/#639 mitigations already had
at hand but discarded.

  read_stdout (chat + headless paths)
    - JSONDecodeError no longer silently `pass`-swallowed.
    - Track count + capture first sanitised, length-capped (300 char) sample.
    - Surface in completion log line as parse_failures=N + WARNING with sample
      text when N > 0. Lets operators distinguish wire corruption (#640) from
      reader-leak-past-claude-exit (#520/#618) in production logs.

  _classify_empty_result
    - New parse_failure_count / parse_failure_sample kwargs (defaults preserve
      legacy callers — chat path doesn't wire it; backward compat covered by
      test_default_parse_failure_args_preserve_legacy_callers).
    - Detail string now includes parse_failures + raw_messages type histogram
      (top 6 types) + first malformed line. The histogram tells operators
      whether the reader caught most of the stream or stalled near the start.

  _kill_orphan_pipe_writers (#618)
    - Was logging orphan count only.
    - Now captures cmdline / ppid / pgid per orphan BEFORE SIGKILL (after
      the kill /proc/{pid} is gone) and emits one INFO line per pid, capped
      at 10 lines + count-only summary, so log volume stays bounded under a
      runaway MCP fan-out.
    - First pass at identifying which package consistently leaks. Issue
      body's "npm setsid" hypothesis is testable now without re-instrumenting.

5 new tests (33 total, all green):
  - test_parse_failure_count_surfaces_in_detail
  - test_parse_failure_sample_appended_when_present
  - test_parse_failure_sample_omitted_when_count_is_zero
  - test_raw_messages_type_summary_in_detail
  - test_default_parse_failure_args_preserve_legacy_callers

Does not close #640. The wire-interleaving root cause remains open — but
the diagnostic surface is now sufficient to identify it from a single
production failure rather than needing to re-instrument and wait for the
next occurrence.

Issue: #640

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… late agent reply (#671) (#681)

When an operator cancels a running execution mid-flight, two writers race
on the same `schedule_executions` row:

  Writer A — terminate handler (`routers/chat.py:~1841`)
      writes status = CANCELLED.
  Writer B — TaskExecutionService success branch
      (`services/task_execution_service.py:498`)
      writes status = SUCCESS once the agent's HTTP reply lands. Claude Code
      typically catches the cancel signal, emits a graceful final message,
      and exits 0 — so the agent reports "completed successfully" and B
      lands well after A.

Pre-fix CAS (RELIABILITY-005, db/schedules.py): SUCCESS writes were
unconditional ("agent's own completion result always wins"). When A landed
first and B landed second, B silently clobbered the CANCELLED status with
SUCCESS. Effects in production:

  - schedule's `next_run_at` advanced as if the run had succeeded,
    suppressing recovery on the next cron tick (silent skip)
  - cost telemetry counted the partial run as billable success
  - on-call had no signal that deliverables were incomplete
  - for agents with side effects (Slack post, sheet rows, CRM), wrongly-
    green status hid incomplete work from operators

Reporter saw two consecutive incidents on bdr-agent / `Daily Lead Outreach`
ending with status=success despite the operator cancelling and no Slack
ping / sheet rows / final deliverable being produced.

Fix: narrow the CAS carve-out so SUCCESS writes are blocked when the row
is already CANCELLED, but still win over RUNNING / QUEUED / PENDING_RETRY /
SKIPPED and over a phantom-stale FAILED (preserves the #378 invariant —
real completions still beat misfired Phase-3 cleanup).

  - SUCCESS over RUNNING       — wins (happy path)
  - SUCCESS over phantom FAILED — wins (#378 invariant preserved)
  - SUCCESS over CANCELLED     — blocked (#671)
  - FAILED/CANCELLED over any terminal — blocked (RELIABILITY-005, unchanged)

Tests: 5 unit tests in tests/unit/test_cancelled_not_overwritten.py
covering each transition above. The exact prod race repro
(`test_success_blocked_when_row_already_cancelled`) fails pre-fix, passes
post-fix. Live-verified against running stack:

  cancel write ok: True
  after cancel: ('cancelled',)
  late-success write ok: False
  after late-success: ('cancelled', None, None)   # response/cost NOT recorded

Defense-in-depth (plumb cancel signal into the agent task-runner so its
reply carries `status=cancelled` and its log says "cancelled by user"
instead of "completed successfully") tracked separately as a follow-up.

Closes #671 (minimum CAS guard)
Refs #679 (defense-in-depth follow-up)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…244) (#684)

Implements Slack's documented multi-connection Socket Mode pattern in
adapters/transports/slack_socket.py. Slack's edge fans out events across
active connections, so when one half-closes, siblings keep absorbing
traffic — eliminating the 430 ms reconnect gap absorbed by the watchdog.

Empirical basis: 7-day production ops report on ability-services showed
68 disconnect/reconnect cycles, 100% recovered by the watchdog (#278), all
ending in identical "Cannot write to closing transport" — Slack's edge
half-closing without a WebSocket close frame. Slack's own docs and support
team recommend running multiple concurrent connections (up to 10 per app)
as the architectural answer to this exact pattern.

Changes
- N concurrent SocketModeClient instances (default 2; range 1-10) via
  SLACK_SOCKET_CONNECTION_COUNT env var, clamped fail-safe on parse error
- _ClientCtx dataclass per client (own watchdog task, own backoff counter)
- Envelope-ID dedup ring (OrderedDict cap 1024 + asyncio.Lock) defends
  against possible cross-connection duplicate delivery; INFO log on hit so
  we measure whether Slack ever actually dual-delivers
- Per-client log prefix [c=N] so ops can attribute disconnects per client
- is_connected returns "any client healthy" (permissive) + new
  connected_count property exposes degraded mode
- stop() iterates all clients/watchdogs (cleanup correctness)
- Parallel start via asyncio.gather keeps boot at ~10s ceiling
- Env-var WARN no longer echoes raw value (prevents accidental token leak
  if operator pastes app token into wrong env var)

Tests
- 56/56 passing (28 existing watchdog + 28 new multi-connection)
- New test_slack_multi_connection.py covers env-var bounds,
  is_connected/connected_count semantics, dedup ring (skip + concurrent
  + FIFO eviction), per-client backoff isolation, partial startup,
  N=1 backward compat, stop() cleanup

Deferred
- #683 — wrap connect_to_new_endpoint() in asyncio.wait_for to prevent
  watchdog stall (pre-existing watchdog hole, blast-radius reduced by
  this PR's per-client isolation)

Verified
- Single-worker uvicorn confirmed (docker-compose.yml line 82, no
  --workers flag; line 230 comment confirms intent), so per-process
  dedup ring is correct
- Architectural Invariants preserved (Channel Adapter ABC unchanged;
  no new endpoints; no new persistent storage; no Invariant #12 regression)
- /review found 0 critical, 2 informational; C1 applied
- /cso --diff: 0 critical / 0 high / 0 medium / 0 low

Fixes #244

Co-authored-by:  Pavlo <pash@pashs-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add 6 missing deploying/ spokes: local-development, single-server,
  public-access, upgrading, backup-and-restore, monitoring
- Add 9 UI screenshots and wire into 10 existing feature docs
- Update deploying-trinity.md hub with spoke navigation table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… failures while open (#687) (#688)

record_failure() was resetting last_failure_time on every call, including
when the circuit was already open. Continuous probe failures (cleanup
re-verify, scheduler dispatches) kept the cooldown timer near zero,
making the half-open transition unreachable and leaving the circuit
permanently open until backend restart.

Fix: only update last_failure_time when state != "open", so the 30s
cooldown starts from when the circuit first opens and is not disturbed
by subsequent failures.

Fixes #687

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
`config.REDIS_URL` is the canonical Redis URL gate (#589 / PR #643) —
it raises at import if creds are missing. Three services bypassed
that gate by reading the env var directly with an unauthenticated
localhost fallback, both at the factory site and in the constructor
default:

  redis_url = os.getenv("REDIS_URL", "redis://redis:6379")
  def __init__(self, redis_url: str = "redis://redis:6379"):

In docker-compose these branches don't fire — REDIS_URL is always
populated. But test/CI paths and ad-hoc debug shells silently fall
back to unauth localhost, then hit NOAUTH at runtime instead of the
clear startup-time error #589 establishes.

Changes:
- slot_service / ssh_service: factories drop the os.getenv fallback;
  constructors take Optional[str] = None and lazy-import
  config.REDIS_URL when called with no arg.
- capacity_manager: same pattern, lazy-import in __init__.
- tests/unit/conftest.py: setdefault REDIS_URL with creds before any
  backend import — unit tests don't share the parent conftest
  (norecursedirs = ..) so the env wasn't being primed.
- tests/unit/test_redis_url_no_fallback.py: lint-style regression
  test that greps src/backend/services/ for `os.getenv("REDIS_URL"`
  and `"redis://redis:6379"` and fails if either resurfaces.

Verified:
- 30 unit tests pass in trinity-backend container
  (test_redis_url_no_fallback + test_capacity_manager + test_config_fail_fast)
- 14 ssh_service tests + 9 redis-url-related tests pass on host venv
- Live backend bootstraps cleanly: SlotService / SshService /
  CapacityManager all resolve REDIS_URL via config
- Lint test catches regression: stashing the slot_service fix flips
  both assertions to FAIL with offending line:number

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(security): close TOCTOU race in webhook rate limiter (#644)

Pre-fix path issued a separate GET then INCR. N concurrent callers
could all observe count < WEBHOOK_RATE_LIMIT before any of them
incremented, slipping past the 429 and pushing the actual call rate
to limit + N.

Switched to INCR-then-compare (Redis INCR is atomic): increment
unconditionally, then 429 the caller whose post-increment count crosses
the threshold. Trade-off: blocked requests still tick the counter,
slightly extending cool-down for an over-limit token. Acceptable for
a rate-limiter — we only stop accepting work, we don't unwind.

Tests:
- tests/unit/test_webhook_rate_limit_toctou.py — pins INCR-first
  semantics. The structural assertion (r.get() not called) reliably
  catches a partial revert that re-adds the GET; the wide-window race
  belongs in integration tests against real Redis.
- tests/integration/test_webhook_rate_limit.py — adds concurrent
  burst test alongside the existing #589 sequential coverage.

Verified live in trinity-backend with real Redis:
- pre-fix: 15/20 succeeded under 20-thread burst (limit 10) — race
  reproduced.
- post-fix: exactly 10/20 succeeded — limit holds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(webhooks): unblock trigger endpoint — schedule model + audit signature (#647 follow-up)

While verifying #644 against a live stack, found two additional facade
gaps that #648 (the WEBHOOK-001 delegation fix) didn't catch — both
crash trigger_webhook before the rate-limiter even runs to completion:

1. `Schedule` pydantic model never carried `webhook_enabled` /
   `webhook_token` fields. The DB columns exist, but the row mapper
   discarded them, so `if not schedule.webhook_enabled:` raised
   AttributeError on every trigger call.

2. `webhooks.py:trigger_webhook` called `platform_audit_service.log()`
   with `actor_type="system"`. The service derives actor_type
   internally from actor_user / actor_agent_name / mcp_scope and has no
   such kwarg; every accepted webhook 500'd in the audit step.

Both are tiny:
- Add the two fields to `Schedule` (db_models.py).
- Pull them through `_row_to_schedule` (db/schedules.py).
- Drop the bogus actor_type kwarg, pass actor_ip instead — webhook
  callers are unauthenticated so caller IP is the only attributable
  signal.

With these, the integration test in tests/integration/test_webhook_rate_limit.py
now exercises the full HTTP path end-to-end. Live verification against
the running backend: 15-way concurrent burst → 10 × 202 + 5 × 429,
limit holds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both copy actions on /api-keys (Copy Config, Copy key icon) called
navigator.clipboard.writeText() with no fallback and swallowed every
rejection in console.error, so users in modal-focus or non-secure
contexts saw nothing on the clipboard and no error.

- Add `utils/clipboard.js` with a textarea + execCommand fallback,
  returning a boolean for caller-driven UX.
- Wire ApiKeys.vue's copyApiKey / copyMcpConfig through the helper.
  Visual "Copied!" / green-check state only fires on confirmed
  success; failure shows an alert telling the user to copy manually.
- Add e2e spec exercising both buttons with a granted
  clipboard-read permission.

Verified in a real browser (admin login, /api-keys, create key,
click both buttons): clipboard contained the expected MCP JSON and
raw `trinity_mcp_*` key respectively; no console errors.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…694)

`GET /api/monitoring/status` returned 500 TypeError for non-admin users
because routers/monitoring.py called the helper with the pre-refactor
two-arg signature `(email, agent_names)`. Every other call site was
updated to `(current_user)` — only this one was missed.

Adds a unit regression test that pins the canonical helper signature
and spy-verifies the router calls it with exactly one positional
User arg.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…theses falsified (#693)

* test(harness): repro scaffolding for #640 — controlled stdio MCP leaks

Adds tests/harness/640/ — three files implementing a deterministic repro
harness for the open root cause behind the reader-thread / wire-corruption
failure family (#640, manifesting in #678, #630, #618, #548, #586).

Background: PR #662's author tried 1.5h with `@upstash/context7-mcp` —
issue body says ≥100 turns / 18min are needed to manifest. Hunting for a
naturally-leaky package is unreliable. Instead this harness builds a
controlled experiment: a minimal stdio MCP server with switchable leak
variants, each testing one hypothesis from the issue body and #662's
empirical notes.

Files:
- noisy_mcp_server.py — stdlib-only stdio MCP server with --leak knob:
    none           : control / baseline
    stderr-flood   : MCP child stderr noise
    setsid-child   : grandchild that escapes pgid (#618 family)
                     and retains protocol-pipe write end
    proc-fd-write  : raw writes to /proc/self/fd/1 interleaved
                     with MCP frames
    delayed-stdout : partial-line writes that race the reader at
                     line boundary
    npm-wrapper    : real-world npx-style boilerplate emitted to
                     stdout BEFORE protocol handshake — most likely
                     production culprit

- run_repro.py — driver that hits an agent's chat API for N turns and
  measures null-cost / null-response rate (the observable symptom from
  #678). Exits 1 if rate exceeds --null-cost-fail-rate (default 5%) so
  the harness can also serve as a CI regression gate once a fix lands.

- README.md — runbook: agent setup, .mcp.json wiring, expected output,
  caveats. Documents that parse_failures-counter assertions wait on
  PR #662 merging.

Smoke-tested:
- Simulator's CLI parses --help.
- Sample initialize + tools/list session round-trips clean JSON
  responses with --leak=none, sidecar log captures protocol activity.
- npm-wrapper variant emits the boilerplate BEFORE the JSON-RPC reply
  as designed.

This commit is scaffolding only — actual variant characterization
against a running stack is a follow-up. Each variant takes ~10-15 min
of Sonnet wall-time at 50 turns, so running the full 6-variant matrix
is a budgeted exercise rather than something to do in one CI pass.

Refs #640
Refs #662 (parse_failures instrumentation prerequisite)
Refs #678 (production manifestation that motivated this work)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(harness): document negative results for #640 — 6 hypotheses falsified

Adds the results section to tests/harness/640/README.md after running 2
variants empirically (npm-wrapper, setsid-child) plus 4 hypotheses ruled
out via static inspection of claude-cli's cli.js and Linux kernel
behaviour. Also fixes a driver bug where cost was read from
top-level `cost_usd` instead of `metadata.cost_usd` (real cost lives
under metadata; the chat endpoint nests observability fields there).

The harness as designed cannot reproduce #640 because every leak path
it can exercise is on the MCP protocol pipe (claude-side), not on the
agent-server claude-stdout pipe (which is where the wire corruption
in #640 actually manifests). Claude+SDK isolate MCP child stdio
correctly; Linux refuses /proc/*/fd/* bypass with ENXIO; Claude has
no stray stdout writes in stream-json hot path.

Negative results preserved so future #640 hunts don't re-walk the
same paths. Real next step is landing PR #662 and getting prod-data-
driven evidence on which package actually leaks.

Refs #640
Refs #662 (diagnostic instrumentation prerequisite)
Refs #678 (production manifestation)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tools (#699) (#703)

* feat(voice): agent workspace page with canvas panel and Gemini panel tools (#699)

Adds a full-page voice workspace at /agents/:name/workspace with a split
layout: orb + controls on the left, an agent-controlled canvas panel on
the right. Introduces four in-process panel tools (show_markdown,
update_panel, append_to_panel, clear_panel) that let Gemini write
structured content to the canvas during a voice conversation without
delegating to the agent container. Panel state is polled at 300ms via
a new GET /voice/{session_id}/panel endpoint. Workspace mode is gated
on a new voice_available feature flag (GEMINI_API_KEY + VOICE_ENABLED)
and surfaced via a BETA-badged button in AgentHeader.

Security: panel content is DOMPurify-sanitised before v-html rendering;
panel endpoint has session ownership checks; append_to_panel caps
accumulated content at 512 KB to bound per-session memory.

Tests: 7 new panel-tool unit tests (test_voice_tools.py); 5 new panel
endpoint ownership tests (test_voice_auth.py).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(voice): add VOICE-008 to requirements + voice API table in architecture

Adds VOICE-008 (Voice Workspace / #699) requirements entry and Phase 4
roadmap entry. Adds voice API endpoint table to architecture.md (was
entirely absent) and updates feature-flags description to mention
voice_available.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
) (#706)

#704: Voice sessions written to Redis (dual-write with in-memory) so a
WebSocket worker can auth-check a session created on a different Uvicorn
process. VoiceService.create/get/remove_session are now async; Redis key
TTL = VOICE_MAX_DURATION + 60 (360s); Redis failure at /voice/start raises
loudly rather than silently producing an intermittently-failing session ID.

#705: on_tool_call callback passed legacy actor_type=/actor_id=/actor_email=
kwargs that don't exist on platform_audit_service.log(), causing a TypeError
that silently swallowed every voice tool-call audit record. Fixed to use
actor_user=types.SimpleNamespace(id=..., email=...) matching the _resolve_actor
contract; wrapped in asyncio.create_task so the audit write doesn't block.

Tests: +7 Redis fallback tests (TestRedisSessionFallback in test_voice_tools.py),
+1 audit attribution source-inspection test (TestVoiceAuditAttribution in
test_voice_auth.py). Catalog updated: 45 voice unit tests total.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…nel (#707) (#709)

- updated_at change-detection gate in fetchPanel() prevents 3x/sec Vue
  re-renders and stops empty state from overwriting content when session ends
- in-flight guard (panelFetching flag) prevents overlapping 300ms requests
- panel content preserved on session end; reset on new session start
- replace v-html+sanitizedHtml computed with ref+renderHtmlPanel():
  DOMPurify.sanitize(html, {ADD_TAGS:['script']}) + _execScripts() re-clones
  script nodes as live DOM so Chart.js new Chart() calls execute correctly
- Chart.js 4.4.0 pre-loaded via injectChartJs() on mount (CDN, id-guarded)
- WORKSPACE_PANEL_INSTRUCTIONS updated: document Chart.js pre-loaded rule

Fixes #707

Co-authored-by: Claude <noreply@anthropic.com>
…factor

ApiKeys.vue deleted; /api-keys now redirects to /settings?tab=mcp-keys.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…#302) (#700)

Splits the 2,600-line Settings page into 5 logical tabs (General, Access,
Integrations, MCP Keys, Agents) with URL ?tab= deep-linking and ROLE-001
role-gated visibility. Absorbs the standalone /api-keys page into the new
MCP Keys tab; preserves bookmarks via a permanent SPA redirect.

Built test-first via Canon TDD against a 12-behavior list documented at
docs/planning/302-settings-test-list.md. 14 Playwright tests pass; 13
match the test-list items 1:1 plus 1 regression test pinning the
non-admin admin-403-bounce fix surfaced during /review.

Behavior
- /settings ?tab=<id> deep links to any tab. Unknown ?tab= falls back to
  the user's default tab. Browser back/forward navigates tab history.
- Tab visibility gates by role: admin sees all 5 tabs; non-admin sees
  only MCP Keys (matches today's /api-keys page being non-admin).
- Default tab: General for admin, MCP Keys for non-admin.
- /api-keys redirects to /settings?tab=mcp-keys (static literal target,
  no open-redirect surface).
- NavBar "Keys" link removed; Settings link now visible to all auth
  users (was admin-only).

Implementation
- New components/settings/McpKeysTab.vue extracted from views/ApiKeys.vue
  (deleted). Same auth posture preserved verbatim.
- New composables/useRole.js mirrors backend ROLE-001 hierarchy
  (user < operator < creator < admin) for client-side UI gating.
- New authStore.role getter sourced from /api/users/me; new
  fetchUserProfile() action populates role on login + session restore.
- 13 existing Settings sections wrapped with v-if matching their tab.
- watch(isAdmin, ..., { immediate: true }) guards admin-only data
  fetches so non-admin users don't trigger 403 → router.push('/')
  bounce — this was the bug surfaced by /review and fixed pre-merge.

Security
- Backend require_admin/require_role in routers/settings.py UNCHANGED.
  UI hiding is convenience, not the security boundary. A non-admin who
  edits localStorage.user.role = 'admin' sees all 5 tabs but every admin
  endpoint still returns 403 — UI bypass yields zero capability gain.
- /api-keys redirect target is a hardcoded literal — no user input.
- /review: 0 critical (after C1 fix), 5 informational.
- /cso --diff: 0 critical / 0 high / 0 medium / 0 low.

Acceptance criteria status
- [x] Tabbed nav with ?tab= URL query param
- [x] 12+ sections organized into 5 logical tabs
- [x] MCP API Keys absorbed into Settings
- [~] "Each tab a separate Vue component" — only McpKeysTab.vue
      extracted; the other 4 tabs remain inline v-if sections in
      Settings.vue. Follow-up issue worth filing for full extraction
      (state, methods, computed props need to move per-tab).
- [x] NavBar simplified (Keys link removed)
- [x] Non-admin users still access MCP key management
- [x] No functionality lost — covered by behavior 11 regression test

Test plan
- 14/14 settings-tabs.spec.js pass (13 list-driven + 1 regression)
- Existing smoke.spec.js updated (no longer asserts Keys link)
- 3 unrelated session-tab.spec.js failures pre-exist on dev (unaffected)

Out of scope (intentionally deferred)
- Full tab-as-component extraction (4 remaining)
- v-show vs v-if for modal-state preservation across tab switches
- Vitest unit-test infra (frontend has Playwright e2e only)

Fixes #302

Co-authored-by:  Pavlo <pash@pashs-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#726)

Fixes three independent regressions in the voice workspace:

1. **test_voice_auth.py collection failure** — `_stub_docker_service()` was
   missing `docker_client` and four other attrs that `services/__init__.py`
   imports; adding a template_service stub also prevents a cascade from
   migration code that imports `services.credential_encryption`.
   Set `SECRET_KEY` env var early so the real config and `test_voice_tools.py`
   stub both sign/verify JWTs with the same key (avoids 4001 on ownership tests).

2. **test_voice_tools.py import error** — `services.gemini_voice` stub left in
   `sys.modules` by `test_voice_auth.py` blocked the real import of
   `GeminiVoiceService`. One-line eviction before the import fixes it.

3. **Chart.js CDN → bundle** — replace the dynamic CDN script injection with a
   proper `import Chart from 'chart.js/auto'` + `window.Chart = Chart`
   in `AgentWorkspace.vue`. The CDN approach was unreliable under load;
   the bundled path is deterministic. `fetchPanel` null-guard updated so
   panel content is never overwritten by an empty response after session end.
   Also exposes `VOICE_MODEL` env var in docker-compose and updates the default
   model identifier to `models/gemini-3.1-flash-live-preview`.

All 45 tests in test_voice_auth.py + test_voice_tools.py pass.

Closes #723

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
) (#727)

* docs(validate-pr): add infrastructure change check to align with DEVELOPMENT_WORKFLOW.md

Adds Step 4.7 to flag docker-compose/Dockerfile changes without justification,
matching the Red Flags checklist in docs/DEVELOPMENT_WORKFLOW.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(schedules): restore auth parity on GET webhook status endpoint (#724)

GET /{name}/schedules/{id}/webhook used AuthorizedAgent which calls
db.get_agent_owner() and 404s for agents not in the ownership table.
POST and DELETE already use name:str + can_user_access_agent; align GET
to match so all three webhook endpoints behave consistently.

Fixes #724

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
… files pass (#725) (#729)

* fix(tests): patch sys.modules stubs in monitoring router and skill service user agent tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(tests): add get_agent_default_resources stub to readiness probe test (#725)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(tests): recover real fastapi in monitoring router loader (#725)

test_inject_assigned_credentials.py permanently overwrites sys.modules['fastapi']
with a Mock at collection time via sys.modules.update().  When collected after that
file (alphabetically 'i' < 'm'), _load_monitoring_router() exec'd monitoring.py with
a mocked APIRouter, causing @router.get() to return a Mock instead of the original
async function.  asyncio.run() then raised TypeError on the Mock.

Fix: briefly evict the polluted fastapi entry, re-import from disk to get the real
module, restore the Mock, then include the real fastapi in the patch.dict context
used during exec_module.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…spin (#730)

`safe_close_pipes` deadlocks on Python's BufferedReader internal lock when
a subscription token is expired (no claude child ever spawns, reader threads
stay alive). The deadlock wedges `asyncio.run(_drain_reader_threads(...))` in
the executor thread for up to 7200 s at 87–91% CPU.

Add `_drain_bounded()` in `claude_code.py`: runs the drain inside a daemon
thread with a `threading.Event` + 90 s `done.wait()` budget. Replaces all 4
`asyncio.run(_drain_reader_threads(...))` call sites. `subprocess_pgroup.py`
is untouched to minimise regression risk.

4 unit tests in `tests/unit/test_drain_bounded.py`.

Fixes #728

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Follow-up to PR #700 (issue #302). Adds 6 new Playwright tests and a
manual test runbook to lift coverage from "navigation works" to
"regression-safe + integration".

New e2e tests (in src/frontend/e2e/settings-tabs.spec.js):

F1. Every section appears under its expected tab — replaces the limited
    behavior 11 (4 of 13 sections) with full 13-section regression. Uses
    exact:true match to disambiguate "API Keys" from "MCP API Keys".

F2. Parametric deep links — 4 ?tab= IDs not already covered by behavior 2
    (general, access, integrations, agents). Round out the 5-ID matrix.

F3. MCP Keys CRUD integration — drives create + revoke through the UI,
    verifies state via API queries. Uses CLEANUP_PREFIX 'test-302-e2e-
    cleanup' for the test key name. afterEach hook + DELETE response
    assertion cleans up; manual recovery one-liner documented in the
    file header in case the hook somehow misses (zero stray rows
    observed in local runs).

F4. Admin login fetches /api/users/me — pins the new fetchUserProfile()
    wiring from auth.js so a future refactor can't silently break role-
    based UI gating.

F5. Re-click active tab does not push duplicate history — guards against
    regressing the early-return guard in selectTab().

F6. Non-admin does not see MCP Server URL section in MCP Keys tab —
    confirms the inner v-if="isAdmin" gate (independent of the tab-level
    v-if) works.

Test infra changes
- Whole spec file marked test.describe.configure({ mode: 'serial' })
  because the CRUD test creates real keys and parallel workers race
  against the McpKeysTab list re-fetch + ensureDefaultKey side effects
  in headless mode. Total runtime: ~12s serial vs ~5s parallel —
  acceptable.
- New cleanupTestMcpKeys helper used by afterEach hook.
- New CLEANUP_PREFIX constant exported in the file header comment with
  the docker-exec recovery one-liner.

Manual test plan (docs/testing/302-settings-tabbed-layout-manual-test-
plan.md): 7-section runbook covering everything the e2e suite covers
plus the things only humans can verify (visual UX, real non-admin
session via DevTools localStorage spoof, /api-keys bookmark redirect
demo). ~25 minutes for full pass.

Verified
- 23/23 settings-tabs.spec.js pass in 12.2s
- 0 stray test rows in mcp_api_keys after a full run
- No backend, docker, or CI changes — this is purely test-infrastructure
  hardening for a frontend-only feature

Refs #302

Co-authored-by:  Pavlo <pash@pashs-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… failures (#708) (#719)

When ALL initial Socket Mode connect attempts fail at backend boot
(transient DNS slowness, edge throttle, etc. exceeding the 10s connect
ceiling), `start()` now spawns a recovery supervisor task that retries
in the background with the watchdog's exponential backoff (60→120→240→
300s cap) until at least one client connects, then graduates to the
per-client watchdog model.

Pre-fix behavior: silent permanent-offline state until manual restart.
The watchdog model assumed at-least-one-connection, leaving no path
out of "zero contexts" once initial gather failed.

Behavior matrix:
- All initial succeed → no supervisor, watchdogs run (no overhead change)
- Partial succeed → no supervisor, degraded mode unchanged
- All initial fail (transient) → supervisor retries, exits on recovery
- All initial fail (bad creds) → supervisor retries forever; backend
  HTTP stays fully responsive; ERROR "STARTUP UNREACHABLE" log fires
  after 3 consecutive failures for operator paging
- Token format invalid (no xapp- prefix) → existing early-return path
  preserved, no supervisor (permanent error must not spin)
- stop() called mid-supervisor → supervisor cancelled cleanly, await
  propagates CancelledError, no zombie task

main.py: stop nilling _slack_transport when initial connect fails so
the supervisor task isn't orphaned.

Test coverage: 10 new unit tests (TestStartupRecoverySupervisor) +
1 flipped existing (test_start_aborts_when_all_clients_fail →
test_start_spawns_supervisor_when_all_clients_fail). 65/65 pass.
End-to-end smoke verified against running backend with extra_hosts
DNS poisoning + bad-credentials scenarios; backend HTTP confirmed
responsive throughout.

Co-authored-by:  Pavlo <pash@pashs-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…changeme propagation paths (#692)

- docker-compose.yml: TRINITY_PASSWORD now reads ADMIN_PASSWORD directly
- docker-compose.prod.yml: switch to fail-loud ${ADMIN_PASSWORD:?...} on both backend and mcp-server
- .env.example: collapse duplicate FRONTEND_URL; add GOOGLE_API_KEY, LOG_*, TRINITY_DATA_PATH, HOST_TEMPLATES_PATH
- src/mcp-server/src/server.ts: drop || "changeme" fallback; throw on startup when MCP_REQUIRE_API_KEY=false and no usable credential
- scripts/deploy/gcp-deploy.sh: refuse to deploy if ADMIN_PASSWORD is unset or literally "changeme"
- deploy.config.example: drop "changeme" default
- docs: update mcp-orchestration flow, single-server deploy guide, TEST_REPORT historical note, feature-flows index

Default MCP_REQUIRE_API_KEY=true mode is unaffected.

Closes #692

Co-Authored-By: AndriiPasternak31 <andriipasternak31@gmail.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves all 49 failures + 2 collection errors in `uv run pytest tests/unit/`.
Final state: 754 passed, 1 skipped, 0 failed across 3 consecutive runs.
Production schema/migrations and backend code are untouched.

Failure groups fixed:
- A (14): missing backend deps in tests/requirements-test.txt
- B (4): test_telegram_webhook_backfill missing platform_audit_service stub
- C (2): test_fleet_sync_audit S7 partial UNIQUE index prevents seeding duplicate-binding state
- D (28): test_file_upload cascading pollution from test_backlog tmp_db + test_slack_watchdog sys.modules stub
- E (4+1): test_git_status_dual_ahead_behind inverted args + agent_server package shadow
- F (1): test_agent_server_auto_sync same package shadow as E

Also adds slackify-markdown>=0.2.0 floor for supply-chain consistency.

Co-Authored-By: Andrii Pasternak <andriipasternak31@gmail.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…#712)

Adds 11 tables, 14 columns, and ~22 indexes that existed only in
migrations.py to schema.py, restoring it as a faithful reference for
the database (Architectural Invariant #3).

Mechanical and additive. Migrations are append-only history. Existing
databases unaffected — every new statement uses IF NOT EXISTS.

Tables added: subscription_credentials, agent_notifications,
subscription_rate_limit_events, slack_workspaces, slack_channel_agents,
slack_active_threads, telegram_bindings, telegram_chat_links,
telegram_group_configs, whatsapp_bindings, whatsapp_chat_links.

Columns added: agent_ownership (full_capabilities, max_backlog_depth,
voice_system_prompt), schedule_executions (source_user_id,
source_user_email, source_agent_name, source_mcp_key_id,
source_mcp_key_name, claude_session_id, queued_at, backlog_metadata,
fan_out_id), agent_schedules (webhook_token, webhook_enabled).

Indexes added: subscription, notification, rate-limit, multi-agent
slack, telegram, whatsapp, plus partial indexes for execution backlog
(idx_executions_queued), retry (idx_executions_pending_retry),
fan-out (idx_executions_fan_out), webhook tokens
(idx_schedules_webhook_token), and proactive sharing
(idx_agent_sharing_proactive).

Verified: name-only and strict DDL parity scripts both pass with
zero missing and zero different entries. test_migrations.py 17/17.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ports the metrics skill from feature/704-705-voice-bugs to dev.
Provides velocity, cycle time, bug ratio, and backlog health reporting
via GitHub Issues + project board data. Includes the 2026-05-07 baseline
report generated during initial development.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Eugene Vyborov and others added 26 commits June 10, 2026 16:20
Dependabot had no target-branch configured, so all version-update PRs
opened against main — violating the feature → dev → main SDLC and
accumulating merge conflicts against dev-first changes. Set
target-branch: dev on all five ecosystems. Takes effect when this
lands on main (next release cut); the config change itself triggers
an immediate Dependabot re-run, which recreates the dropped PRs
against dev.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
) (#1138)

Google retired gemini-2.0-flash (404 NOT_FOUND), breaking Telegram voice
transcription entirely and silently degrading image-gen prompt refinement.
The model id was hardcoded in both services with no config escape hatch.

- Add GEMINI_TEXT_MODEL / GEMINI_TRANSCRIPTION_MODEL to config.py
  (default gemini-3.5-flash, live-verified for text + inline audio;
  2.5-flash was rejected: it carries an announced Oct 2026 shutdown)
- or-coalesce + non-empty compose defaults + commented .env.example
  entries, mirroring the VOICE_MODEL pattern (#1076) so an empty env
  var can't shadow the default
- Wire both vars through docker-compose.yml AND docker-compose.prod.yml
  (#1039 packaging-gap class)
- Explicit retired-model log hints on 404 in both call paths so the
  next retirement is a same-day config fix, not a user-reported bug
- Regression-guard test: text_refinement model must not be gemini-2.0*/1.x
- Feature flows synced (image-generation, telegram-integration,
  agent-avatars)

Agent-server model allowlist still offers retired models — tracked
separately in #1137 (different deploy surface, needs base-image rebuild).

Fixes #1130

Co-authored-by: Eugene Vyborov <eugene@beingluminous.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
- agent-guardrails.md: Guardrails tab renamed to Settings (#1108) —
  GuardrailsPanel now section #1 inside components/settings/SettingsPanel.vue,
  ?tab=guardrails deep-link alias, refreshed AgentDetail wiring + Key Files
- agent-monitoring.md: rewrite stale Frontend Layer that still documented
  the deleted /monitoring route and views/Monitoring.vue as live; now
  describes the redirect + admin-gated Health tab (MonitoringPanel.vue, #1109)
- agent-overview-dashboard.md: note the Health section empty state when
  monitoring is off (6df68c9)
- feature-flows.md: add Recent Updates rows for #1130 and #1108

Refs #1108, #1109, #1130

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the accreted architecture doc with a deduplicated rewrite:
each cross-cutting feature (capacity, circuit breakers, heartbeat,
soft-delete/retention, session tab, VoIP, canary, ...) now has exactly
one canonical block; catalogs and endpoint tables point to it instead
of repeating behavior. Content-equivalent (495 lines vs 1192 removed —
redundancy and changelog narration only); the #868 tenant-boundary
warning is preserved explicitly. Previous version archived at
docs/archive/memory/architecture-2026-06-11.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bulk clear actions for the Operating Room queue and agent notifications:

- POST /api/operator-queue/bulk-cancel — cancels only the ids the operator
  actually saw (race-safe vs the 5s sync loop); per-id accessible-agent
  scoping; returns {cancelled, skipped}
- POST /api/operator-queue/clear-resolved — hides terminal rows
  (acknowledged/cancelled/expired) via a new cleared_at column. NOT a
  DELETE: the sync loop re-creates DB-missing items whose agent-file entry
  still says pending (always true for expired, and for cancelled inside
  the propagation window). Deletion deferred to the retention sweep (#1142)
- POST /api/notifications/dismiss-all — pending+acknowledged → dismissed
  in one statement, same accessible-set the list endpoint uses
- Sync write-back now propagates cancelled/expired status into agent queue
  files (in-place flips only) so agents stop waiting on cleared items
- Respond-vs-bulk-cancel race now returns 409 instead of a silent 200
  that never recorded the response
- One audit-log entry + one WS event (operator_queue_cleared /
  notifications_cleared) per bulk op; stores refetch on receipt
- Operations.vue: per-tab Clear All button (hidden when empty) with
  blast-radius confirmation; Resolved feed now shows cancelled/expired
- Tests: tests/test_ops_clear_all.py (20 tests — scoping isolation,
  tri-state accessible-set, idempotency, 409 marker, hide-not-delete)

Closes #1017

Co-authored-by: Eugene Vyborov <eugene@beingluminous.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…1137) (#1144)

Follow-up from #1130. The agent-server model endpoints still advertised
Gemini models Google has retired, so a gemini-cli runtime agent could
select one and get a 404 from Google at runtime.

- GET /api/model `available_models`: replace the gemini-2.0-flash entry
  with the live set — gemini-3-pro, gemini-3-flash, gemini-2.5-pro,
  gemini-2.5-flash; update the note accordingly.
- PUT /api/model `valid_models` allowlist: drop gemini-2.0-flash,
  gemini-1.5-pro, gemini-1.5-flash; keep only the four live models.
  Update the 400 error hint. The `startswith("gemini-")` forward-compat
  fallback is retained intentionally.
- gemini_runtime.py pricing table: comment the 2.0-flash / 2.0-flash-lite
  entries as historical — kept only so cost calc of older executions
  still resolves, not offered in the lists.

Model set sourced from the runtime's own GEMINI_PRICING table (default
gemini-3-flash), not the issue's parenthetical guesses. Base image
rebuilt (trinity-agent-base:0.6.0/latest); new lists verified inside the
image.

Related to #1137

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tion timeout (#1094) (#1147)

* fix(agent-server): distinct error for stall-watchdog kill vs max-duration timeout (#1094)

The per-tool no-output stall watchdog and the max-duration budget are two
distinct termination paths, but the terminal 504 always claimed
"Task execution timed out after {effective_timeout} seconds" regardless of
which fired. A 300s stall-kill at ~531s wall-clock was therefore persisted
as "timed out after 2400 seconds", misleading operators into bumping the
execution timeout — the wrong knob (the 300s no-output threshold is what
actually fired).

The watchdog already distinguished the two causes for its log line but
discarded that for the persisted error. Now:

- `HeadlessRunContext` carries `termination_reason` ("stall_no_output" |
  "max_duration") and `stalled_tool`, set in the watchdog's
  `except subprocess.TimeoutExpired` before re-raising.
- The orchestrator builds a reason-specific 504. Stall returns a structured
  dict detail `{message, termination_reason, stalled_tool}`; max-duration
  returns `{message, termination_reason}`. The backend's existing
  HTTPError handler (task_execution_service.py:1019-1021) reads
  `detail["message"]`, so the persisted `schedule_executions.error` now
  reflects the real cause and the structured reason rides along for the
  UI/API — no DB migration.
- The "timed out after Ns" label is now reserved for genuine budget
  exhaustion (elapsed ≈ N), as the issue asked.

Out of scope (per issue): a per-task override of the 300s stall threshold,
and a dedicated termination_reason DB column.

Tests: tests/unit/test_1094_stall_termination_reason.py asserts both ctx
outcomes via the #970 fake-Popen harness; full #970 wait-loop suite still
green (11 passed). Base image rebuilt (trinity-agent-base:0.6.0/latest);
fix verified inside the image.

Related to #1094

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(test): make #1094 stall test self-contained (CI collection error)

The regression-diff CI runs `cd tests; pytest unit/` and collects each
changed test file in isolation, where `tests/unit` is not on sys.path —
so `from test_headless_executor_970_timeout import ...` raised
ModuleNotFoundError at collection (reported as a new [E] under HEAD).
It only passed locally because I ran it next to the #970 file.

Inline the small fake-Popen harness (ctx/_FakePopen/_FakePipe/
patched_loop/_install_popen) instead of importing from the sibling test
module. Verified CI-style: `pytest unit/test_1094_...py` from tests/
under pytest-randomly now passes (2), and 11 pass alongside the #970 file.

Related to #1094

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(agent-server): structured termination_reason on outer-timeout path too (#1094 review)

Review follow-up: the inner subprocess.TimeoutExpired max-duration branch
returned a structured detail dict carrying termination_reason, but the
sibling outer asyncio.TimeoutError safety-net raise — same semantic cause
(budget exhausted) — still emitted a plain string. A consumer filtering
executions on termination_reason="max_duration" would miss budget timeouts
that fired via the outer net. Make the two exits symmetric.

Related to #1094

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(default OFF) (#1121) (#1123)

* fix(monitoring): wire fleet-health loop to lifespan, persist enabled (default OFF) (#1121)

The MON-001 fleet-monitoring loop was never started at backend boot — the
only path that started it was an admin calling POST /api/monitoring/enable.
Every trinity-backend restart silently killed the loop and left it off until
a human re-enabled it; the persisted monitoring_config was never consulted on
startup. The documented "30s monitoring loop is authoritative" invariant was
silently false on any restarted instance.

- main.py lifespan(): delayed (+12s) resume that reads the persisted
  monitoring_config and starts the loop only when enabled — survives restarts.
  Best-effort stop added to shutdown for parity with the other loops.
- MonitoringConfig.enabled now defaults to OFF and is the single source of
  truth (was dead code defaulting to True). enable/disable and PUT /config
  persist the flag and reconcile the running loop.
- Negative/zero *_check_interval rejected via a Pydantic validator (422 on
  request body; persisted bad config falls back to DEFAULT). Loop also clamps
  its sleep to >=1s as a belt-and-suspenders guard against a flood loop.
- routers/monitoring.py: shared load_persisted_monitoring_config() +
  _persist_monitoring_enabled() helpers (one reader, no drift).
- architecture.md: monitoring loop now described as lifespan-wired and
  gated by the persisted default-off setting; coordinated the #307
  heartbeat "stays authoritative" wording.
- test_monitoring.py: test_update_config_validation now asserts 422 for
  non-positive intervals (it previously asserted the unvalidated 200).

7-day health-check retention (#772) unchanged; longer retention is a
follow-up Enterprise knob (#1049) per the issue.

Verified end-to-end on the live stack: enable -> restart -> loop resumes
(fleet checks every ~30s); disable -> restart -> no loop; bad intervals -> 422.

Related to #1121

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(monitoring): address review — partial-PUT preserves enabled, inline start/stop (#1121)

Two issues from PR review of the enable/disable/PUT reconcile path:

1. Partial `PUT /config` silently disabled monitoring. Every MonitoringConfig
   field has a default, so a body omitting `enabled` deserialized to
   `enabled=False` and the reconcile stopped a running loop on a routine
   interval tweak. Now only treat `enabled` as authoritative when the client
   explicitly sent it (`model_fields_set`); otherwise preserve the persisted
   run-state.

2. enable→disable race left the loop running against persisted `enabled=False`:
   enable scheduled the start as a background task, so a racing disable could
   no-op (is_running still False) before the start ran. start()/stop() only
   create/cancel the loop task and don't block, so enable/disable/PUT now
   reconcile inline (awaited) — runtime state matches the persisted flag before
   the response returns. Dropped the now-unused background_tasks params.

Verified live: enable -> is_running true immediately; partial PUT keeps
enabled+loop; explicit enabled:false PUT stops the loop; left clean (disabled).

Related to #1121

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Eugene Vyborov <eugene@beingluminous.com>
Bump vulnerable npm dependencies flagged by Dependabot:

- vitest 2.1.8 -> 3.2.6 in tests/git-sync (GHSA-5xrq-8626-4rwp,
  critical — Vitest UI arbitrary file read/exec; alert #150)
- js-cookie -> 3.0.8 via overrides in tests/git-sync (GHSA-qjx8-664m-686j,
  high — prototype hijack in assign(); alert #127). Transitive via
  js-beautify; pinned with an overrides entry.
- fast-uri -> 3.1.2 via root overrides (GHSA-v39h-62p7-jpjc #121 +
  GHSA-q3j6-qgpj-74h6 #113, high — host confusion / path traversal).
  Transitive via ajv; pinned to ^3.1.2 to stay within ajv's expected
  3.x range rather than jumping to the 4.x major.

git-sync vitest suite passes (10/10) on the new versions.

Related to #1140

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#1126) (#1128)

Backend (the confirmed root cause): every container-creation site passed
`cpu_count` to docker-py, which maps to the Windows-only HostConfig.CpuCount
and leaves NanoCpus=0 on Linux — so NO agent ever got a CPU cgroup limit
(created, recreated, or system). Memory was unaffected (`mem_limit` is the
correct Linux param). Replaced with `nano_cpus = cores * 1e9` (Linux CFS
quota) at all three sites: crud.py (create), lifecycle.py (recreate),
system_agent_service.py (system agent).

Frontend:
- ResourceModal: pre-select the EFFECTIVE value (`cpu ?? current_cpu`,
  `memory ?? current_memory`) instead of the DB override (null when unset),
  so the dialog reflects what the agent actually runs with; the null option
  becomes an explicit "Inherit default". Adds a load-error banner.
- useAgentSettings: `loadResourceLimits` records `error` (surfaced in the
  modal) instead of silently swallowing; `updateResourceLimits` returns a
  success boolean.
- AgentHeader: show the configured core count + max memory alongside the
  live CPU/MEM gauges while running (was only shown when stopped).
- AgentDetail.saveResourceLimits: gate the restart on the container actually
  reaching a stopped state (poll via fetchAgent, 30s cap) instead of a fixed
  1s sleep, skip restart if the save failed, and report restart failures to
  the user instead of leaving the agent stopped silently.

Verified live: PUT cpu + restart → HostConfig.NanoCpus = cores*1e9
(0 → 1e9 for 1 core, → 2e9 for 2 cores), label trinity.cpu and the cgroup
agree, memory unchanged. UI files compile (vue/compiler-sfc).

Optional capacity-pressure hint: deferred (issue marks it drop-if-not-easy).

Related to #1126

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
chore(ci): point Dependabot version updates at dev, not main
…e-by-default (#1129) (#1132)

* feat(security): fleet-wide default for require-verified-email, secure-by-default (#1129)

Promote the per-agent `require_email` access-policy flag (#311) to an
admin-controlled fleet-wide default that defaults ON, so new agents are
secure-by-default instead of opt-in per agent. Mirrors the RES-001
agent-defaults/resources pattern.

- settings_service: AGENT_DEFAULT_REQUIRE_EMAIL_KEY + get_agent_default_require_email()
  (default ON; resolved from system_settings, code fallback so fresh installs
  are secure without a data migration).
- routers/settings: GET/PUT /api/settings/agent-defaults/access-policy (admin-only)
  + AgentDefaultAccessPolicyUpdate model.
- register_agent_owner (db/agents + database facade): new require_email param,
  written explicitly in the INSERT (same SQLite-baked-default reasoning as #665's
  execution_timeout_seconds). Defaults False so the system agent is unaffected.
- crud: seed require_email from the platform default at user-agent creation.
- Settings UI: admin toggle in the Platform section, consistent with Default Model.
- No schema change (column + system_settings already exist); existing agents are
  never rewritten — the default is read only at creation time.

Verified live: default GET = ON; new agent seeds require_email ON; flip default
OFF → next new agent OFF; existing agent unaffected by the flip; per-agent
override still works; UI toggle renders + reflects the default. Throwaway agents
deleted, default restored ON.

Related to #1129

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(security): audit-log the agent-default access-policy change (#1129 review)

PUT /api/settings/agent-defaults/access-policy wrote the fleet-wide
require_email default via db.set_setting() with no audit trail, while every
other settings write in the router (API-key, GitHub PAT, generic /{key})
emits AuditEventType.CONFIGURATION. Flipping a security default must leave a
trace — add the same platform_audit_service.log() call.

Verified live: flipping the default writes a `settings_change` audit row with
{setting: agent_default_require_email, require_email: <new>}.

Related to #1129

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Eugene Vyborov <eugene@beingluminous.com>
…small for build scratch (#1098) (#1124)

Agent containers mount /tmp as a 100 MB RAM-backed tmpfs (noexec,nosuid).
Heavy scratch — pip/npm install, compiling C extensions, ML wheels
(torch/transformers) — overflows it with "No space left on device" even
though the home volume has hundreds of GB free, and noexec breaks
compile-and-exec-from-tmp. This blocks a core class of agent setup work.

Fix: set a default TMPDIR=/home/developer/.tmp on the disk-backed,
exec-capable home volume. pip/npm/most build tooling honor TMPDIR, so this
redirects scratch onto real disk and dodges noexec in one move — while
keeping /tmp's hardened noexec,nosuid posture unchanged.

- New constants AGENT_TMPFS_MOUNT + AGENT_DEFAULT_TMPDIR in the low-dep
  capabilities.py (single source of truth). Wired into all three
  provisioning paths so create / recreate / system-agent can't drift:
  crud.py (create), lifecycle.py (recreate, setdefault so a template/user
  TMPDIR carried on the container wins), system_agent_service.py.
- startup.sh creates the dir (idempotent, chmod 700) before its own pip
  install, so existing agents pick it up on restart/recreate and tools
  honoring TMPDIR land on disk.
- architecture.md: note the TMPDIR redirect alongside the /tmp tmpfs claim.

Requires a base-image rebuild (startup.sh change) — start.sh /
build-base-image.sh handles it; env + startup.sh ship together.

Verified e2e on a fresh agent (rebuilt base image): container env carries
TMPDIR=/home/developer/.tmp; startup.sh auto-creates the dir (700,
developer-owned) on the 600 GB-free home disk; tempfile.mkdtemp() lands
there; 150 MB scratch succeeds via TMPDIR but caps at exactly 100 MB on
/tmp ("No space left"); /tmp stays noexec,nosuid,size=100m.

Related to #1098

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Eugene Vyborov <eugene@beingluminous.com>
…timeline views (#1150) (#1152)

* feat(analytics): loop executions as a distinct Loops bucket/color in timeline views (#1150)

- Remap triggered_by=loop from Scheduled to a new Loops bucket in
  _TRIGGER_BUCKETS / _BUCKET_ORDER (read-time bucketing — historical
  loop rows remap automatically, no migration)
- BUCKET_COLORS: Loops = fuchsia-400, a deliberate accent so loop
  bursts read distinctly from Scheduled (cyan) and Agent-to-agent (teal)
- ExecutionsPanel: fuchsia loop trigger chip (light+dark) + loop option
  in the trigger filter dropdown
- routers/executions.py: add loop to _VALID_TRIGGERS — without it the
  new dropdown option would be a silent no-op (router nulls unknown values)
- StackedBarChart: slate fallback for buckets missing from the colors
  map (stale bundle vs newer backend renders gray, not invisible)
- Tests: Loops bucket assertions + dropdown/_VALID_TRIGGERS drift guard

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: add Loops bucket to agent-overview-dashboard flow (#1150)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: sync executions-dashboard, overview, and run-agent-loop flows with Loops bucket (#1150)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Eugene Vyborov <eugene@beingluminous.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Two new pages: sequential agent loops (automation/agent-loops.md) and
outbound VoIP telephony (advanced/voip-telephony.md). Updated 17 pages
for the unified Operations nav, Agent Detail Overview tab + tab
overflow, Guardrails-to-Settings rename, dispatch circuit breaker,
fleet executions dashboard, per-schedule analytics, idempotency keys,
heartbeat liveness, operator-queue MCP tools, and the fleet-wide
require-verified-email default. MCP tool count corrected to 80; fixed
stale routes, broken links, and a leaked internal codename.

Refs #1106 #1056 #1109 #1107 #1108 #852 #868 #525 #526 #307 #1101 #1129

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n sweeper (#1153) (#1154)

The periodic cgroup orphan sweeper (#817) SIGKILLed operator `docker exec`
sessions within 0–30s — the same class of actor (an operator/platform process
maintaining the agent) that the allowlist already deliberately protects for
SSH. A long `docker exec agent-X sh -c '...sleep...'` died with exit 137; weeks
of FAISS index-build failures were misdiagnosed as OOM/capacity.

Root cause: a `docker exec` entry process matches none of the allowlist sources
(no execution id, no env tag, no shared pgid, not sshd) and was reaped as an
orphan — yet it is exactly the SSH-session case.

Fix:
- orphan_allowlist: new `_docker_exec_session_pids()` — inside the container PID
  namespace a `docker exec` entry has **PPID 0** (real parent is outside the
  namespace); genuinely-leaked orphans reparent to **PID 1**, never 0, so PPID 0
  is an unspoofable "operator exec session" signal. Allowlist every PPID-0
  process (excluding PID 1, already hard-protected) + its descendant tree.
  Wired into `resolve_allowlist`. When the session exits, leftover children
  reparent to PID 1 and fall out → reaped next sweep (#817 guarantee preserved).
- orphan_sweep: each real kill now logs at **WARNING** with `pid` + a
  length-capped `cmd=` (was INFO). One such line turns the weeks-long
  "which process keeps dying?" misdiagnosis into a 5-minute fix. dry_run stays
  INFO. cmdline capped at 200 chars.
- docs: TRINITY_COMPATIBLE_AGENT_GUIDE gains a "Long-Running & Background
  Processes (the orphan sweeper)" section — the `persistent-processes.allow`
  escape hatch and "run maintenance as a platform execution" guidance.

Tests: tests/unit/test_1153_docker_exec_allowlist.py (5) — PPID-0 entry +
descendants protected, PID 1 (and its children) excluded, reparented orphan
not protected, resolve_allowlist end-to-end, post-exit child reaped. #912
suite still green (13 passed together).

Live-verified on a fresh trinity-agent-base:0.6.0 agent: a 30s `docker exec`
loop survived a real mid-flight sweep and exited 0; agent-server + sshd spared
under the production sweep_pid; a planted PPID-1 orphan was reaped.

Related to #1153

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nt loops into a usage guide

Verified each abilities plugin against its actual skill directories
(README lags the repo): create-agent 13 wizards (+doctor), agent-dev
19 skills (claim/close renames, git-sync, backlog dev cycle,
pipelines), trinity 6 skills (+loop, create-dashboard,
deploy-new-instance), dev-methodology 24 skills.

Loops documentation:
- automation/agent-loops.md: new Common Patterns section (iterative
  refinement, agentic retry, bounded polling, backlog draining) and
  Writing Good Until-Mode Loops guidance (verifiable sentinels, caps,
  per-run timeouts, stall watching); /trinity:loop entry point
- abilities/trinity-plugin.md: documents /trinity:loop usage,
  modifiers, and when-to-use-what vs chat/fan-out/schedules

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t` (#1162)

The CLI shipped an obfuscated third-party API credential in
trinity_cli/_notify.py, used to email an access request during
`trinity init` when no instance URL was given. Any installed copy
(PyPI/Homebrew) could recover and use the key.

Delete _notify.py and replace the no-URL branch of `trinity init`
with plain instructions to email access@ability.ai directly — the
CLI now carries no credential and makes no outbound send. The
credential has been revoked in the provider console.

Refs #1158

Co-authored-by: Eugene Vyborov <eugene@beingluminous.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…avoid duplicate commit sha

The build-stamped VERSION is "X.Y.Z+g<sha>" (#993), and the NavBar chip
(#926) appends git_commit_short again, rendering e.g.
"v0.6.0+gcccac44d · cccac44". Add displayVersion to useBuildInfo
(version with the "+..." suffix stripped) and use it in the NavBar chip,
build-info modal, and Settings build panel — commit stays its own field.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Skills updated in trinity-dev: /review (plan completion audit,
merge-base diffing, evidence-gated findings), /cso (calibration +
refuter verification), /autoplan (user-challenge gate, full-depth
contract), /release (landing verification); workflow doc reconciled.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ped (#1143) (#1145)

`stores/notifications.js:fetchPendingCount()` read `count` off
`GET /api/notifications?status=pending&limit=1`, where `count` is
`len(returned page)` — so the polled NavBar badge clamped to 1 every
60s regardless of how many pending notifications existed.

- Add `GET /api/notifications/count?status=pending` — returns the true
  total scoped to the caller's accessible agents (mirrors the per-agent
  `/api/agents/{name}/notifications/count`). Registered before the
  `/{notification_id}` catch-all (Invariant #4).
- `db.count_pending_notifications` gains an `agent_names` list filter
  (fleet count over accessible agents); empty list → 0, not "all"
  (avoids invalid `IN ()`).
- Point `fetchPendingCount()` at the new endpoint.

Verified live: a fleet with 32 pending now reports 32 (old limit=1
query still returns 1); `/{id}` route unshadowed (404 on unknown id);
invalid/unsupported status → 400; unauth → 401.

Related to #1143

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Eugene Vyborov <eugene@beingluminous.com>
…shop videos

- Restructure Quick Start around the abilities plugin marketplace; CLI
  moves to "Other ways to deploy" (still fully documented)
- Update Abilities section to the current plugin inventory (four-step
  lifecycle, trinity plugin's 6 skills, doctor wizard, accurate skill counts)
- Add AGENTS.md: task router, key facts, and verification steps for AI
  agents; referenced from the top of the README (discovery by reference)
- Point agents to docs/user-docs/README.md as the detailed docs index
- Add workshop video links (watch-more list + Thursday archive), Cornelius
  repo link; fix workshops URL (workshopsgt -> workshops)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… to main

Reconciles divergence before the 0.6.1 release cut. README kept at dev's
newer version (main's copy was stale and carried a workshops-link typo).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

# Conflicts:
#	README.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vybe
vybe requested a review from AndriiPasternak31 as a code owner June 12, 2026 09:31
…release gate

The dev→main PR's unit-suite regression gate surfaced four test-only breaks
(invisible on dev pushes, where the base/head matrix doesn't run):

- test_1073_voip_media_stream_ticket: config stub missing VOIP_MAX_CALL_DURATION
  (added by #1091) — the import error aborted the entire head-side collection
- test_telegram_webhook_backfill: models/settings_service stubs missing the
  #1129 names (AgentDefaultAccessPolicyUpdate, AGENT_DEFAULT_REQUIRE_EMAIL*)
- test_image_generation_service: config mock missing GEMINI_TEXT_MODEL (#1130
  made the text model configurable; MagicMock repr leaked into the URL)
- test_admin_recovery: patch DB_PATH via the get_db_connection globals each
  ops module actually captured, so sibling tests that replace the db package
  in sys.modules can't redirect the fixture's tmp DB

Full unit suite verified locally under all three CI seeds (12345/67890/99999):
2124 passed; only the two pre-existing test_orphaned_execution_recovery
failures remain, which also fail on base/main, so the diff gate ignores them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vybe
vybe merged commit e396414 into main Jun 12, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment