Conversation
…igger is stranded (#2524) Completes #1081 Phase 4. After this, every autonomous trigger can run on the durable queue; only the interactive ones stay on push, which is the deliberate Open Question 7 scope cut (#1982/#1989). FAN-OUT. `FanOutService.execute` built a `dict[task_id, FanOutTaskResult]` inside one `asyncio.gather`, so the batch existed only while the request that started it did. A pull-claimed subtask returns nothing to collect (the row is queued and the turn runs later in the agent's worker), and nothing could answer about a batch afterwards — no `async_mode`, no status endpoint, a disconnect lost it. The batch now lives on `schedule_executions`: every subtask row carries `fan_out_id` plus the caller's own `fan_out_task_id` (new column — the id used to be a dict key no async batch could reach), and `build_aggregate` rebuilds the result from the rows. Adds `async_mode` and `GET /api/agents/{name}/fan-out/{fan_out_id}`, which also checks the batch belongs to that agent: `fan_out_id` is opaque but not secret. Two decisions the issue asked for. `max_concurrency` keeps its meaning and needed no branch. The semaphore stays around the `execute_task` call: on push that call spans the whole turn so it paces dispatch as before, and under pull it returns in milliseconds so the worker pool becomes the cap — Phase 5's "capacity becomes physical", by construction. Deleting it, as first planned, would have fired N dispatches at an agent whose `max_parallel_tasks` is 3 and turned the excess into CapacityFull. The outer deadline bounds the WAIT, not the work — a contract change. A still-open subtask now reports `running`, not `failed`; the batch still reports `deadline_exceeded`. A queued or claimed row is not the backend's to cancel, and on push the old cancellation was half-illusory anyway (it abandoned the HTTP call while the agent kept running and billing the turn). After a deadline the status endpoint is the source of truth. A2A + OPERATOR_RESPONSE. These were deferred with "a2a cannot hand back a receipt to poll" — true and beside the point: it does not need a receipt, it needs to BLOCK CORRECTLY while the turn happens elsewhere, which `sync_waiter.wait_for_sync_terminal` already did for `/task`. `dispatch_and_await_terminal` is the adapter: `execute_task`, and on a QUEUED return, wait for that row's terminal and rebuild from it. Nothing signals that waiter on the pull path, so the wake is the 5s DB poll — up to ~5s of extra tail latency on an a2a call against a pilot, deliberately not worth a second signalling path. `PULL_REACHABLE_TRIGGERS` now equals `_AUTONOMOUS_TRIGGERS` and stays an enumerated allow-list on purpose: a structural test forbids deriving it, because that would hand reach to the next autonomous trigger with nobody checking dispatch can deliver it — precisely #2048's defect. `note_unreachable_pull_trigger` is kept and still tested, against a synthetic narrowing. The loop advance (#2523) and fan-out join share one `_terminal_side_effects` shim off `spawn_task_terminal_event`, with separate guards so one raising cannot cost the other its terminal. Migration 0051 + the SQLite twin: `fan_out_task_id`, plus `idx_executions_fan_out_status` — the join COUNTs non-terminal rows for one batch on every fan-out terminal. Refs #1081, #2048, #2391, #2523, ent#157, ent#329. #2392 gets worse from here: `effect_guard` still fails open when the execution id is absent, and a fan-out multiplies that by N. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Tm7UEkd4G9KQZD5oeLRSa
…ync-join # Conflicts: # tests/registry.json
… presence (#2695) `stt_available` was `bool(tts_service.is_available())` — a non-empty check on the ElevenLabs key. ElevenLabs permissions are per endpoint, so a key granted Text-to-Speech but not Speech-to-Text rendered a fully working-looking mic that failed on every press with `401 missing_permissions`, while spoken replies played normally on the same instance. Nothing in the UI or the admin panel could say so: `key_configured` was honestly true. New `services/stt_capability_service.py`: one provider probe per key — a one-byte non-audio POST to `/v1/speech-to-text`, which authorises before it validates, so 401/403 is `refused` (with the provider's status word), any other definitive status is `capable`, and a transport error / 5xx is `unknown`. The verdict is cached in Redis under a digest of the key (6h decided / 2min unknown, per-process fallback when Redis is down), so a key change is a miss by construction and the key resolver stays uncached across workers. Reads are bounded (`WAIT_BUDGET_SECONDS`: a slow provider answers `unknown` now and the probe fills the cache in the background) and fail soft: only a definitive refusal hides the mic. `client_portal.service._stt_ready` is now THE gate — the roster card, the agent page and `transcribe_portal_audio` all resolve it, so the control a client sees and the endpoint it calls still cannot disagree (#2212's rule). A real `/stt` 401 stores `refused`, so the reported symptom heals the cache even if the probe never ran. `GET`/`PUT /api/settings/elevenlabs` carry `stt_capability`, `stt_detail` and `stt_checked_at` beside `key_configured`; re-saving a key invalidates its row. Settings → Voice renders "can transcribe" / "cannot transcribe — <reason>" / "transcription not verified" next to the presence badge, via the pure `utils/sttCapability.js` rule. Tests execute every consumer of the verdict (classifier partition, cache keying, bounded wait, roster threading, endpoint refusal, live-refusal feedback, settings state) — none read source text. Fixes #2695 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015owqMKD5QDjzZrUTF2Joht
Resolve docs/memory/learnings.md: append-only log, both sides kept (dev's 2026-09-10 entries first, this branch's 2026-09-11 entry after). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015owqMKD5QDjzZrUTF2Joht
…ace-model-choice) The spec's own docstring says every box is taken from ONE render, but the icon-button loop re-measured each button after `picker`/`send` were captured, and skipped Send by coordinate (`box.x >= send.x`). The picker is a native <select> sized by its widest option label, which re-measures ~150 ms after paint when the web font lands (211 -> 199 px at 768 on a cold profile); a Send read after that sits left of the earlier `send.x`, is mistaken for an icon button, and fails 'an icon button sits right of the picker'. The race is on dev too — instrumented: dev's picker drifts identically — this branch just loses it more often (its unmocked requests shift the timing). Wait for `document.fonts.ready` and for the picker to hold its box across two consecutive reads before measuring anything, and skip Send by element identity. 3/3 green locally at 375/768/1280 on this branch (was 2 of 5 failing every run). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015owqMKD5QDjzZrUTF2Joht
`list_agent_schedules` and `get_agent_schedule` have returned `validation_enabled`, `validation_prompt` and `validation_timeout_seconds` since VALIDATE-001 landed, and neither write tool accepted them. Both schemas are strict, so the fields could not be smuggled through either -- the post-execution validation hook could be observed over MCP but never configured over it. The backend takes all three on both write paths (`ScheduleCreate`, `ScheduleUpdateRequest`), so this is an MCP-layer parity gap and needs no backend change. Same shape as #85, which added `timeout_seconds` / `allowed_tools` / `model` to these two tools; the `validation_*` trio was never added. `validation_timeout_seconds` is bounded 30-600 in the Zod schema, so an out-of-range value is refused by name rather than reaching the backend, matching how `retry_delay_seconds` already handles the same range. The `validation_enabled` description states what enabling it costs: one extra execution on the same agent after each technically-successful run, a `business_status = failed_validation` plus one operator alert on FAIL/PARTIAL, and no retry -- retries key on technical failure, and validation only runs after technical success (#1573). Refs #2759
… hardening guide (#2691, #2692) The first-run hardening card offered "Add a domain" with no statement of what it buys or what has to be true first, and then showed a green tick over a value nobody had checked. Walked live on a fresh droplet, nobody in the room could say why an operator would add a domain, or whether the setting changed routing at all. What the setting actually does, now documented in the copy and in requirements/infrastructure.md: it is the address Trinity hands out (Telegram, WhatsApp and VoIP callbacks, Slack's OAuth return, public links, workspace and file links), and on a provisioned host it authorises the web server in front to obtain a certificate for that one name. It does not create a DNS record, and Trinity issues no certificate itself. The tick is now earned. Caddy's on-demand-TLS gate is called during a real handshake for the saved name, which is proof of the whole chain an operator cannot otherwise confirm from inside Trinity — DNS resolves, traffic arrives, a certificate follows — and stays true behind Cloudflare's proxy, a load balancer or a reserved IP, where comparing the name's DNS answer against this instance's own address says the opposite. An authorised ask latches `<iso>|<host>`; the reader compares that host to the one in force, so a stale row reads as not-reached rather than showing a tick for a name nobody visited. Until it flips, both the first-run step and Settings say "saved, waiting for the first visit". Step completion is unchanged, so no established install re-opens the overlay. Only Caddy's own ask latches. The same route is reachable from the public internet — Caddy proxies to the frontend and nginx forwards /api/ — and the domain is published in every webhook URL, so an unguarded latch could be forged with one curl. The guard fails closed: an unrecognised caller is still answered, it just does not record anything. Two live defects fixed alongside: - The gate never IDNA-encoded. SNI is ASCII so Caddy asks about the A-label while an operator saves the name as they read it, so a domain with any non-ASCII character could never obtain a certificate — silently, forever, on every visitor's page load. - The generic settings PUT accepted any string for this key. `htp://typo.com` stored cleanly and the back-fill immediately re-pointed every Telegram webhook and WhatsApp binding at an address that answers nothing. A value that cannot take effect is now refused before the write, with a named error. Plain http:// stays legal — the managed fleet advertises exactly that behind a tunnel. Copy: benefit and prerequisite are readable without opening the disclosure, the sentence claiming the proxy "picks up the name" is gone (it never did), the webhook side effect is stated where the setting is owned, and the tunnel is "optional but recommended" rather than "dismissing it here is a fine answer" — which contradicted the guide it now links to. Settings → General carries the same explanation from the same module, and the http-only regex no longer lets `http://` complete the step it exists to move people off. tunnel, or a private network instead, each stage with a verification step, the marketplace-specific bits the generic public-access page does not cover, and Tailscale framed honestly (operator access, no Trinity integration, not a substitute for the tunnel). Linked from the deploy index, DEPLOYMENT.md and the marketplace listing. Fixes #2691 Fixes #2692 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CtMP6kk2GP9oKaEzGwA8H3
…rd (#2692) The guide listed Slack events among the integrations a VPN breaks. #2380's own decision table says the opposite, and the code agrees: the live transport is adapters/transports/slack_socket.py, an outbound WebSocket that needs no public URL. Only the one-time OAuth install callback does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CtMP6kk2GP9oKaEzGwA8H3
…2692) The page was hand-written and followed the feature template. Pages under guides/deploying/ that cover an operation follow the operational one instead: When to Run This → Pre-flight → Procedure → Verify → Recovery, with the verification table and the reusable compose-restart rule stated verbatim rather than paraphrased. Also per the skill's maintenance trigger, the FAQ pages that neighbour this topic gain the questions a user would actually type — how to lock down a marketplace instance, what 'waiting for the first visit' means, and why a domain can show a certificate error while Trinity reports it as set — and faq/README.md is regenerated from the pages' own headings rather than hand-edited. That regeneration also corrects one pre-existing anchor that had drifted from the heading it points at. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CtMP6kk2GP9oKaEzGwA8H3
…this image has (#2692) There is no one-click to lean on, and that is now a checked fact rather than an assumption: DigitalOcean's marketplace has no Tailscale app (its application image list has none), Vultr's API lists none either, and both apply marketplace images at create time only — so nothing can be added to a droplet that is already running. The improvement available is a single non-interactive command with an auth key, which is now written out, along with the two traps that bite later: node keys expire after 180 days by default, which silently drops the machine off the tailnet after port 22 is closed, and --ssh must be opted into per device before that happens. It also records a gap rather than papering over it. The provisioned web server holds certificates for the droplet's public IP and the saved domain, and the gate authorises only that one name, so https://<tailnet-ip> gets no certificate — and the public domain still resolves to the address the operator is about to close off. The private-network path therefore covers shell and agent access today; reaching the web UI over a tailnet needs a recipe nobody has verified on a live droplet yet, and the page says so instead of implying it works. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CtMP6kk2GP9oKaEzGwA8H3
…hat works (#2692) The private-network path left the operator with shell access and no way into the web UI, and the page did not explain why. It now does: the web server picks a site by the hostname in the request and holds exactly two — the droplet's public IP and the saved domain — so a tailnet address matches neither, Trinity refuses the certificate request (the same refusal that stops the instance being an open certificate requester), and no authority could issue for carrier-grade NAT space anyway. The saved domain does not help either, since public DNS resolves it to the address the operator just closed off. The supported answer today is an SSH tunnel to the local frontend port, which needs no certificate and is unaffected by the container firewall — the same mechanism the ops agent already ships. The page also explains why browsing the container port directly does not work, since that is the next thing anyone tries: container ports are dropped from off-box, and 80/443 work in the public case only because the web server in front is a host process. Serving a private address natively is named as a product improvement rather than dressed up as configuration. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CtMP6kk2GP9oKaEzGwA8H3
Every site in the provisioned Caddyfile is matched by hostname, and there are two: the instance's public IP and the domain an admin saved. A tailnet address matches neither, the certificate gate refuses it — correctly, that refusal is what stops the instance requesting certificates for any name pointed at it — and carrier-grade NAT space cannot be validated by a public CA in any case. So an operator who moved onto a VPN and closed 80/443 was left with shell access and no URL to open. The saved domain does not help either: public DNS resolves it to the address they just closed off. PRIVATE_NETWORK_CIDRS (space-separated, empty by default) renders an `@private remote_ip` matcher into the http:// site that serves those sources directly instead of redirecting them. Plain HTTP costs nothing there — the VPN already encrypts the transport, which is the posture the managed fleet runs and that DEPLOYMENT.md already calls finished rather than a compromise. Unset behaves exactly as before. Source address, never the Host header. A header is supplied by the caller, so matching on it would let anyone on the internet send `Host: 100.64.0.1` to port 80 and be served the login page in cleartext, having bypassed the HTTPS redirect — a worse hole than the one being fixed. A source address cannot be forged into a completed TCP handshake. Three more properties, each because the failure mode is a box you cannot reach: 0.0.0.0/0 and ::/0 are refused; anything that is not an address range is dropped with a warning rather than rendered; and the generated file is validated before the reload, since an invalid config stops the web server outright. Applying a change is `--caddy-only`, a phase that re-renders the config alone. The site phase also rewrites FRONTEND_URL and TRINITY_INSTALL_SOURCE, so re-running it to pick up one variable would silently re-stamp a marketplace droplet's provenance as a doc-driven install. Beyond the two tickets this branch carries, added at the user's request after the gap surfaced while documenting the VPN path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CtMP6kk2GP9oKaEzGwA8H3
…it (#2692) The FAQ answer was written while the fix did not exist and still told operators to tunnel over SSH because Trinity could not serve a private address. It can now. The install-provenance flow's Caddyfile walkthrough and the TLS decision table in DEPLOYMENT.md gain the same setting, with the trade stated where an operator picks the shape: a private network costs every inbound channel, since those need a public URL to call. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CtMP6kk2GP9oKaEzGwA8H3
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Le7kBh9PbTNGHPc5bxm3qz
Three conflicts, resolved on their merits rather than by side: * `docs/memory/learnings.md` — both sides appended a new entry at the top of a ledger. Kept BOTH; this is an append-only file and picking a side would delete someone's lesson. * `client_portal/service.py` — `_row_to_card` gained `stt_ready` here and `can_manage_canvases` on dev, in the same signature and the same roster call. Kept both parameters and both kwargs; dev's `may_manage_canvases` comment is carried verbatim. Both call sites (the roster and the #2160 single-agent lookup) now pass both. * `workspace-model-choice.spec.js` — took dev's version WHOLESALE, which makes this branch's `06b86be9` a no-op. Both commits fix the same flake: this branch waited for `document.fonts.ready` and the picker's width to settle, then skipped Send by element identity; dev's #2705 instead takes every box in ONE `page.evaluate` and emulates reduced motion. Dev's is strictly stronger — when all boxes come from one frame, a font re-measure cannot invalidate a relative assertion, so the coordinate Send-skip it keeps is sound again and no wait is needed at all. Two fixes for one race is worse than either; this one loses. The two `--check` whitespace warnings (`hardeningGuide.js`, `test_voice_auth.py`) are pre-existing on origin/dev and arrive with the merge — verified byte-for-byte against `origin/dev` — so they are deliberately left alone rather than swept into an unrelated branch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q19uRCksdn4DiRAJ55rfpZ
… test (#2691) The feature-flags handler now reads settings_service.is_public_url_reached(), and this hand-rolled stub AttributeErrored on it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Le7kBh9PbTNGHPc5bxm3qz
- Settings → General: the "waiting for the first visit" state now shows only on provisioned installs (hardening_guide_eligible). Only the provisioned Caddyfile's TLS ask writes the latch, so own-proxy, tunnel and tailnet installs would have waited forever; they keep the saved tick. - start.sh: render the Caddyfile to Caddyfile.new, validate that, and move it over the live file only on success. An invalid file on disk with the unit enabled took the site down on the next restart. New test proves the previous file survives a failed validate. - _is_caddy_ask: docstring and PROV-016 now say the stamp is unforgeable from the public front door, not from inside the Docker network. A loopback source check cannot close that: Caddy reaches the published port from a bridge gateway. Advisory stamp, accepted. - test_2691: monkeypatch ss.db.get_setting_value instead of assigning it on the shared db instance (reload did not undo it). - settings_service: the reached stamp is never cleared; comment said it was. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Le7kBh9PbTNGHPc5bxm3qz
Keep both the ent#498 deliver_to_workspace_email fields and the #2759 validation_* fields in schedules.ts / types.ts. The description assertion in schedules.validation.test.ts now reads the zod .description accessor, because dev is on zod 4, which no longer serialises it into JSON.stringify.
…en it cannot be asked (#2695) Review finding on the AC the issue underlines ("must not reintroduce cross-worker staleness"): `read_cached` fell through to the per-process `_local` on a Redis MISS, not only on a Redis error, while `store` wrote `_local` unconditionally and `invalidate` popped it in the calling worker only. Sequence: admin re-saves the key on worker A → A pops its local, deletes the Redis row, re-probes; if that probe lands `unknown` (2-minute row) or times out, the row expires and worker B serves its stale `refused` from `_local` for the rest of the 6 h — mic withheld on B, offered on A, for the same customer. `read_cached` now treats Redis as authoritative whenever it ANSWERS: a hit is returned, a miss returns None AND evicts this worker's local copy (a corrupt row is a miss too, never a fall-through); `_local` is read only when there is no client or the read raised. `store` still writes both, so the outage fallback stays warm. Tests: the Redis-present branch had zero executing coverage (the autouse fixture stubs `_redis` to None). A dict-backed `_FakeRedis` shared by two module instances (= two uvicorn workers, each with its own `_local`) now pins: TTLs reach the wire and rows decode; an invalidate on A is honoured by B and B's local copy is evicted; the review's 6 h sequence (unknown row expires) is a miss; the local copy is used only while the read raises; a corrupt row is a miss. 4 of the 5 are red against the pre-fix service. Riders from the same review: - `PROBE_TIMEOUT_SECONDS` comment said it sits BELOW `WAIT_BUDGET`; it is 8.0 vs 4.0 and deliberately above — the reader stops at 4 s, the probe runs on to fill the cache. Comment now says why. - "the Workspace mic is hidden" (service log, docstring, Settings hint, backend.md) is browser-dependent: `resolveMicMode` falls back to Web Speech when `serverStt` is false, so a refused key disables server-side dictation and hides the mic only where the browser has no engine. - `stt_capability_service.py` gets its own catalog entry in backend.md instead of riding the `tts_service.py` line. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Resolves conflicts with #2679 (#2670) by adopting its shipped GET /api/agents/{name}/fan-out/{fan_out_id} and dropping this branch's duplicate route, get_status and batch_belongs_to. The GET gains an additive task_id from fan_out_task_id. Review fixes (#2524, merge-train 2026-09-09/10): - Create each subtask row at slot grant inside the max_concurrency semaphore instead of up front. Pre-created RUNNING rows waiting behind the semaphore were bulk-FAILed by the #106 no-session sweep; QUEUED rows would be claimed by claim_next_queued while _dispatch_all also dispatched them. The sync caller now waits for the shielded dispatch, then for queued rows to reach a terminal. - Default wait budget covers ceil(N / min(max_concurrency, max_parallel_tasks)) waves instead of one subtask's bound. - Snapshot subscription_id on fan-out rows (SUB-004). - Carry execute_task's error_code into the sync aggregate. - _fail_subtask gates side effects on the CAS and emits through spawn_task_terminal_event. - A failed fan-out DB poll read no longer escapes the wait. - MCP fan_out: async_mode param, corrected deadline wording. - Docs: architecture/execution.md, api-endpoints.md, requirements/scheduling.md 37.4, fan-out flow. - Real-schema test for the fan-out SQL. Renumber the Alembic revision to 0062_execution_fan_out_task_id off 0061_execution_open_canvas (single head). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ver with their own keys (#2349) The journey harness for J10, "my agents can call each other, and I can see what they said". Two ephemeral agents; every call is made the way a real playbook makes it — through the MCP server with the caller's own agent-scoped key, read from its container because no API returns it — so the harness crosses the same checkAgentAccess gate a real agent does. Credential-free on every PR: the permitted call lands on the callee attributed to the caller with a collaboration activity on the caller (IA-01, AC-01); a call with no edge is refused with a reason naming both agents and nothing runs (P-02); a stopped callee answers 503 "Agent is not running" within seconds and leaves no row (IA-03); a fan-out is refused past 50 at the tool and the backend and lands as one batch on one agent (IA-02); a loop stops at its budget; deleting the callee leaves no dangling edge (L-03). On a keyed stack the callee's real answer is read back from its execution record and every fan-out subtask completes. Three strict=True xfails carry the findings, each with its own issue: no chain-depth guard on agent-to-agent chat chains (#2806); a refused call is audited as a successful tool call (#2807); run_agent_loop never runs the permission gate (Abilityai/trinity-enterprise#628). The backend routes' owner- equivalence for agent keys is a ruling, Abilityai/trinity-enterprise#629, with no public reproducer. Journey conftest gains the primitives: create_agent_and_wait (lifted out of journey_agent), agent_mcp_key (Docker SDK, by the trinity.agent-name label, extracting exactly one variable), McpSession (httpx JSON-RPC over the server's streamable-HTTP transport, reprs never carry headers), permission-edge and read-back helpers, and a module-scoped concurrently provisioned agent pair. Catalog: J10 flips to tier journey-smoke (the smoke lane runs the directory wholesale) with built: false, J03-style. IA-03's A2A sentence is corrected — the inbound route never answers 409. Registry entry, regenerated JOURNEYS.md, Testing pointers in four feature flows, two learnings entries, and the diff-scoped CSO report. Journey Impact: extends: J10 Fixes #2349 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ot a bare sys.modules write (#2695) Both red checks on this PR were the same two lines. `_load_worker` builds a second instance of the module to stand in for a second uvicorn worker. The synthetic name has to be in `sys.modules` while `exec_module` runs — `@dataclass` resolves the class's module BY NAME during the module body — and that was done with a bare assignment plus a `finally` pop: sys.modules[name] = mod try: spec.loader.exec_module(mod) finally: sys.modules.pop(name, None) `tests/lint_sys_modules.py` exists to stop exactly that, because one leaked entry is a module every later test in the session imports instead of the real one. It failed twice over: the `lint (sys.modules pollution check)` job directly, and `regression diff`, whose sole new failure was the pytest mirror of the same rule (`test_lint_sys_modules::test_committed_baseline_matches_current_repo_state`). `monkeypatch.setitem(sys.modules, name, mod)` is the fix the lint names, and it is available here because the only caller is a fixture. It is also stronger than the hand-rolled pair: pytest removes the entry at teardown even if `exec_module` raises before the `finally` is reached. The entry now lives until teardown instead of being popped immediately. Harmless and deliberate — the name is unique per fixture instance and nothing after `exec_module` resolves it. Not a baseline regeneration: the violations are gone, so the committed baseline (0 for this file) is satisfied as written. Verified: `python tests/lint_sys_modules.py` → "no new violations"; the two affected test files pass under all three CI seeds (12345 / 67890 / 99999), 58 tests each. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf
…est boundary (#2796) `model` arrived as an unvalidated free string on all three interactive entry points (ParallelTaskRequest, ChatMessageRequest, SessionMessageRequest) and reached the runtime as a `--model` argv element. A value that cannot name a model therefore died agent-side as `unrecognized_model` — exit code 1, no output, and no field named anywhere in the message. Shape-check it at the boundary and answer 422 naming the offending value instead. The reported mechanism does not reproduce: Trinity does not pass the caller's role into the model slot. With the body the Chat tab sends, the slot is None, and the only "admin" in scope is the `source_user_email` fallback for an admin account with no email, which lands in its own column. The value reaches `model` only when the request body carries it. The accepted families are mirrored from `model_context._FAMILY_PREFIX_WINDOWS`, which is already the platform's answer to "is this id one we recognise?" — so Claude, Gemini and Codex (`gpt-*`, `codex`) agents all keep dispatching, bare aliases keep working, and a newly supported runtime is one edit away. A guard test fails the build if the two lists drift. Deliberately NOT the Workspace's closed allow-list: that set is 3 of the 11 ids in `model_catalog.py`, and the operator picker documents free-text passthrough (including the `[1m]` extended-context suffix), so a closed set would refuse ids this repo ships on the surface they were written for. A leading `-` matches no family, so argv smuggling closes in passing. `run_resumable_turn` splats one value into `execute_task` twice (initial attempt and cold retry); validating the router's single source covers both, and a test asserts the second call is not missed.
… the SSE parser skips bad frames (#2349) Review of #2809, findings I1 and I3. `clear_edges` now asserts every revoke and the empty edge set afterwards, so a leaked edge blames the harness reset rather than the product in a later permission test — the property the module-scoped pair's order-independence rests on. `McpSession._post` skips a `data:` frame that is not JSON or not an object instead of raising from inside the transport, so a malformed frame surfaces as the caller's named failure. No assertion changed; J10 rerun on a live stack: 9 passed, 3 xfailed. I2 (the model gate reads the pytest host's environment, so keyed journeys skip invisibly on a subscription-authenticated stack) predates this PR — J03 carries the same gate — and is filed as #2812. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…py-and-hardening-docs # Conflicts: # docs/user-docs/faq/README.md
Resolves the append-only ledger conflict in docs/memory/learnings.md by keeping both sides in landing order: dev's three 2026-09-15 entries (#2795, #2794 ×2) first, then this branch's two (#2349). tests/registry.json auto-merged; no migration files were incoming. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The tick's client half is correctly wired, but the store action that
parses it had no assertion at all: deleting both
`stores/sessions.js:164` and `:195` left the full 2930-test frontend
suite green, with `publicUrlReached` permanently false, the
`https-domain-reached` copy dead, and the tick this PR exists to earn
never appearing.
Extends the existing `describe('the store fails closed')` harness, which
already pins three sibling flags on this exact code path, with the
fourth field — plus one sequenced test the other four cannot replace:
they each start from a fresh store where every closed value equals the
state default, so they pass with the `catch` block's resets deleted and
cannot tell a reset from an initial value. Earning the tick and then
failing a forced re-read is the only shape that pins the fail-closed
branch itself, and it is the real path — `FirstRunOverlay.vue:378` and
the three `Settings.vue` save handlers all re-read with `force`, which
is when a just-saved domain's tick resolves.
Both mutations now caught (drop the success assignment → 2 red; drop the
catch reset → 1 red); 130 files / 2931 tests pass.
The overlay's own binding (`FirstRunOverlay.vue:217`) is deliberately
left uncovered: `vitest.config.js` is `environment: 'node'` with no DOM,
so driving it needs a mount harness — an author decision, not a
mechanical fix. A `toContain` pin over the SFC text would only restate
the source.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`_row_to_card`'s new `stt_ready` parameter carried `= None`, and an omitted value resolves to the PRE-FIX presence behaviour (`stt_ready if stt_ready is not None else True`). The guard that exists to forbid exactly that — `test_row_to_cards_capability_arguments_have_no_defaults`, whose own docstring says "a default would let a call site keep compiling while silently serving the wrong card" — pinned only `is_platform`, `runtime` and `model_context`, so the fourth capability argument slipped past it. Not a live bug: both production call sites (`get_agent_card`, `get_roster`) pass it explicitly. But a third call site added later would silently un-fix #2695 with the whole suite green, which is the regression the guard was written to make impossible. Drops the default, adds `stt_ready` to the guard's tuple, and passes the neutral `True` at the four older call sites that relied on it — `True` reproduces exactly what the bit meant before the capability probe existed, so no assertion changes meaning. Also corrects the field docstring in `client_portal/models.py`, which still defined `stt_available` as "an ElevenLabs key resolves" — the pre-#2695 gate this PR replaced. Verified: reintroducing the default fails the guard; reverting the card gate to presence-only fails 2 tests in the PR's own suite; 2081 related unit tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Sep 15, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Integration surface only. This PR is never merged. Members merge individually once this is green.
Merging this would put merge commits on the squash history. It exists to pay the merge gate once for the batch, and to buy the one thing nothing else in the lifecycle provides: every PR here is tested against
dev, never against its siblings, so two PRs that are individually green and jointly broken are invisible until both land.fix(mcp): let create/update_agent_schedule write the validation configfix(chat): shape-check the caller-supplied model at the interfacefix(workspace): the mic renders on speech-to-text capabilityfeat(tests): J10 journey — agents call each other through the MCP serverfix(onboarding): say what a domain buys, earn the tickfeat(pull): async fan-out join + sync edge adapterNo closing keywords: the members carry those, and this PR must promote and close nothing.
What this batch is actually testing
Six files are touched by more than one member — all merged textually clean, which is exactly why a joint run matters:
src/backend/routers/settings.pyandsrc/frontend/src/views/Settings.vue— fix(workspace): the mic renders on speech-to-text capability, not key presence (#2695) #2699 + fix(onboarding): say what a domain buys, earn the tick, and write the hardening guide (#2691, #2692) #2773src/mcp-server/src/types.ts— fix(mcp): let create/update_agent_schedule write the validation config #2761 + feat(pull): async fan-out join + sync edge adapter — no autonomous trigger is stranded (#2524) #2532docs/memory/feature-flows/fan-out.md,docs/memory/learnings.md,tests/registry.json— feat(tests): J10 journey — agents call each other through the MCP server with their own keys (#2349) #2809 + fix(workspace): the mic renders on speech-to-text capability, not key presence (#2695) #2699/feat(pull): async fan-out join + sync edge adapter — no autonomous trigger is stranded (#2524) #2532The pairing to watch: #2809's J10 journey asserts fan-out cap and batch behaviour, while #2532 rewrites fan-out into an async join. Both are green alone.
journey-smokeruns unconditionally on PRs todev, so this is the first time those two meet.Alembic: one schema member (#2532,
0062on0061), single head verified before and after.Gate
Not the four required contexts — none of them runs a test. Gating on:
backend-unit-test/ the pytest seeds,regression diff,frontend-build,frontend-e2e(uilabel applied — 8 frontend files),secret-scan,pg-migrations(#2532 touchesdb/+migrations/versions/), plusschema-parity, both CodeQL analyses andverify-non-root.Ejected from this batch: #2811 — headline AC falsified by an untouched second credential writer; three mutations restoring the reported bug leave all 3029 tests green. Reasons on the PR.
🤖 Generated with Claude Code