June 1, 2026
Act-loop refactor lands T1–T5; provider & chat fixes
The bulk of this batch is the flat MessageProcessor refactor across T1–T5 of TKT-795 / doc-112
The bulk of this batch is the flat MessageProcessor refactor across T1–T5 of TKT-795 / doc-112. T1 adds the frozen ProcessorConfig dataclass and a configs.channels module with static constants and factories (1403 tests pass). T2 builds the flat MessageProcessor skeleton — static process() entry, _run/_setup/_loop/_should_stop/_record lifecycle, cancel/deadline/iteration-cap stop conditions, plus Ability.dispatch and AbilityRegistry.build_tools stubs; 1422 tests green.
T3 makes Ability.dispatch() the single chokepoint for every ACT-loop tool call: param sanitisation, act_summary pop, WS start/end events, policy enforcement, MCP routing, timeout, exactly one trail row. PolicyService.enforce() and _permission_gates / _build_action_description move out of act_dispatcher_service, which is deleted along with four test files. ASYNC_CAPABLE ClassVar + _active_delegates registry land, and 36 new dispatch tests take the suite to 1428.
T4 replaces the in-memory _act_trail list with DB queries. Ability.record() / fetch_by_transcript_id() / render() become the only write/read/format path; _from_last_compaction slices rows from the LATEST trail_compaction row; _has_trail / _render_act_trail / _record_narration / _purge_ephemeral_tool_calls are added to MessageProcessor. ToolRenderAndRecordService is deleted entirely with all callers migrated; 36 new tests, 1464 pass.
T5 introduces Providers.calculate() — builds the real request body via build_request_body() and divides token count by the provider context window, returning a 0.0–1.0 fraction (0.0 on error or unknown max_tokens). get_compact_at(), estimate_payload_tokens(), COMPACTION_THRESHOLD_RATIO, and the compact_at schema column are deleted. A follow-up hotfix migrates the live _check_threshold() send-path off the now-absent methods to a single Providers.calculate() call, threshold pct > 0.80, and uses MagicMock(spec_set=) in the regression so future calls to deleted methods raise AttributeError instead of auto-fabricating.
The architecture doc is updated alongside T3 to reflect Ability.dispatch() as the chokepoint, PolicyService.enforce() replacing ActDispatcherService.dispatch_action(), config.channel replacing ordinal injection, and ASYNC_CAPABLE on the Ability ABC. ConversationPhaseService — zero production readers, write-only telemetry — is removed along with its test and two post_turn phase.update() calls; metrics and proactive-skill paths stay intact.
On the provider side, the soft-delete is_active column is dropped: deletion is now a physical DELETE, the column is removed from schema.sql/seeder.sql/SELECT/INSERT/filters, and run.py purges any pre-existing is_active=0 rows before schema convergence. sqlite3.IntegrityError on create/update is mapped to a 409 with a friendly duplicate-name message instead of a raw 500. Separately, _fetch_gemini_models() is wired into /providers/list-models so the Gemini provider form stops hitting the “Unsupported platform ‘gemini’” else branch.
The chat frontend gets two fixes: uploaded images no longer vanish from the user’s bubble (preview metadata is captured before clear() and threaded through _startTurn → renderer.appendUserForm with image thumbnails and doc chips via shared CSS vars), and turn-level backend errors with recoverable:false no longer bounce the page to /login/. onError now only triggers the auth redirect on an explicit data.auth_failed signal; genuine session loss is still caught by /auth/status and api.js 401 handling, and the error bubble finalises the turn in place.
-
T3 deletes services/act_dispatcher_service.py and four test files; Ability.dispatch() becomes the single ACT-loop chokepoint with PolicyService.enforce() handling policy
-
T4 deletes ToolRenderAndRecordService; act-trail moves from in-memory list to DB queries via Ability.record/fetch_by_transcript_id/render with _from_last_compaction slicing
-
T5 replaces get_compact_at / estimate_payload_tokens / COMPACTION_THRESHOLD_RATIO with Providers.calculate() returning a 0.0–1.0 fraction; compact_at column dropped from schema.sql
-
Hotfix 4acdbea migrates live _check_threshold() send-path to Providers.calculate() with MagicMock(spec_set=) guarding against future silent auto-fabrication of deleted methods
-
Soft-delete is_active removed from providers: deletion is a physical DELETE, duplicate-name maps to 409 instead of 500, and run.py purges existing is_active=0 rows before schema convergence
-
Chat fix keeps uploaded images in the user’s bubble and stops misclassifying recoverable:false backend errors as auth failures, preventing the error→reload bounce