Repository navigation
DO NOT MERGE — merge train: 2993,2972,2974,2989,3003 - #3006
Closed
trinity-ability wants to merge 38 commits into
Closed
trinity-ability wants to merge 38 commits into
trinity-ability wants to merge 38 commits into
Conversation
…#2958) The Claude /api/chat path ran `claude --continue`, which resumes the newest JSONL in the shared project dir whoever wrote it. Every /api/task above 600 s persists its JSONL there (#678), so the next chat silently continued a scheduled run's near-full context and paid an auto-compaction first. - The chat records its session id after a SUCCESSFUL turn and resumes it with --resume <id>; a different effective model starts a fresh session. - A resume whose JSONL is gone gets one cold retry, decided by the filesystem after the failure (no tool use, not cancelled). - A failed resume (exit 0 + is_error) now raises 502 instead of "returned empty response". - reset_session() bumps a generation so an in-flight capture is dropped. - The id is published to ~/.trinity/chat-session.json for the reaper's keep set; cleared on reset and at startup. - Compaction numbers are read from the JSONL, as the headless path does. - Session counters reset on a cold start (no stale context_used).
The chat now resumes its own session by id, and that id lives in agent memory, so no DB keep-set source can see it: the sweep would reap the chat's JSONL an hour after it goes idle (the ent#358 / #2610 class, fourth surface). The sweep reads the agent's marker (~/.trinity/chat-session.json) over docker exec as developer, bounded by head -c 512, an in-container timeout and asyncio.wait_for. Any unclean read aborts that agent's sweep; the log names the failure kind only, never the agent-writable content. The id is fullmatched, so a trailing newline cannot slip through. A parity test pins the agent writer's path to the backend reader's, and the resume-surface guard now counts the marker read.
A chat turn that spent 162 s of 173 s compacting was recorded as a plain
success with a large duration_ms, which callers read as a degraded agent.
The task path already writes compact_metadata; the chat path never did.
- _finalize_chat_success writes compact_metadata and adds
execution.compaction {events, trigger, pre_tokens, post_tokens,
duration_ms} to the /chat response (None when nothing compacted).
- _finalize_http_failure writes it from the #678 structured body.
504/429/plain-500 bodies carry no metadata, so those stay unattributed.
No schema change: the column exists on both migration tracks.
…hrough (#2958) chat_with_agent (sequential) now says it continues the agent's own chat session, never a headless run's, and that a turn crossing the context limit can pay a one-off auto-compaction recorded as compact_metadata. It points to get_execution_result because a long compaction outlives the 25 s MCP gateway window. get_execution_result returns compact_metadata (parsed; a malformed value degrades to null) and its description names the compaction delay.
Requirements §5.38 (AC1-AC4, the per-agent scope as a known limitation, the model rule and its cost, the cold retry and its residual, AC3 partial on 504/429/500) and the §5.9 keep-set bullet. Architecture: agent-runtime (the chat's own --resume, marker, generation guard, retry), execution (the fourth keep-set source, chat compact_metadata), mcp-server (get_execution_result carries compact_metadata).
execution-queue (the wrapper's session choice, the new state fields, reset), parallel-headless-execution (isolation from persisted task JSONLs), mcp-orchestration (sequential mode + compaction attribution), authenticated-chat-tab, file-sharing-outbound and system-wide-trinity-prompt (stale --continue claims about /chat). One combined Recent Updates row. The /task-is-stateless claims stay: they remain true.
app.add_event_handler does not exist on the agent image's FastAPI (0.141),
so agent_server/main.py crashed at import. The host test env's FastAPI
(0.124) has it, so every unit test passed; the in-image smoke (the chat ->
persisted task -> chat scenario run with python3 inside
trinity-agent-base) caught it. Use @app.on_event("startup"), like the
sibling hooks, and pin that the registered hook clears a leftover marker.
…2964) A jsdom mount spec for PortalWorkCard's live chat shape: the steps row and the Stop slot must exist from first paint (reserveLiveRows) and be patched in place when the feed row and the 202's execution id arrive. A second block pins every other host (default props) to today's markup. RED on the current card, as intended (cases 1-4 fail, 5-7 already hold): x 1. pending: the steps row and the Stop slot are reserved, blank and inert x 2. pending -> none swaps the text in place; the shape does not change x 3. pending -> unknown swaps in place and says the unknown sentence x 3b. a long name truncates; the claim never does x 4. Stop arrives in place: same element, same layout, now visible and usable Tests 5 failed | 5 passed (10)
…card (#2964) The chat's live card started from a synthetic item (steps undefined, no execution id). ~2 s in, the feed read inserted "<agent> doesn't report steps." (+22 px) and, as soon as the 202's id arrived, Stop was inserted before Open in Work, sliding it ~58 px sideways. A new PortalWorkCard prop, reserveLiveRows, set only on the chat's live card, holds both rows from first paint: - the steps sentence renders as a blank, aria-hidden one-line row while pending and swaps text in place for none/unknown; in that card only the agent's name truncates, never the claim (stepsLine's none result now carries `who` so the card never re-types the sentence); - Stop's slot is the same button, invisible, disabled, aria-hidden and out of the tab order until canCancelTurn (ent#155), then made usable. Rail, room, Work tab and terminal cards keep the default and render as before (pinned by the mount spec's default-prop cases and a source guard that the prop sits only in the conversation's live block). Mutations (each restored from a cp copy): bare v-else-if -> cases 1-3 red; drop `invisible` -> 1 red; drop tabindex -> 1 red; drop shrink-0 on the claim -> 3b red. The jsdom spec proves structure only; the geometry was measured with the Playwright card probe (h 132->154 before, constant 154 after, Open in Work stationary, across 5 long turns at 1400/800 px).
Two paragraphs only: "Three steps states, not two" gains the chat card's pending footprint and one-line rule; "The row" gains reserveLiveRows' steps-row and Stop-slot reservation, the ent#155 gate, and the transitions it does not cover (stages <-> unknown, a row going stale mid-turn).
hasActions ignored reserveLiveRows, so a host that set the flag without show-open-in-work would still grow the card when the 202's id arrived. The chat always passes show-open-in-work today, so this is not live; the prop's contract now holds on its own. Test 4b is red without it.
…2958) The resume-turn 502 was meant for a dead session, which produces no text. As written it also failed a resumed turn that answered and then reported is_error, which under --continue (and still on a cold turn) returned its text. Since #2958 nearly every chat turn is a resume turn, so that was a behaviour change for most turns. Gate the 502 on an empty response.
… delivery state, fingerprint reconcile, if_match write-back, 409 item_diverged, aging (#2915) A pending approval sat unseen for six days because the agent rewrote and then closed its entry while the platform kept a create-only snapshot and read only the pending/acknowledged file statuses. The file contract stays; every way the two sides can disagree is now detected, recorded on the row, refused where it must be, and audited. Backend - Seven nullable operator_queue columns on both tracks (SQLite `operator_queue_sync_state`, Alembic `0071_operator_queue_sync_state`): sync_state/sync_detail/sync_updated_at/last_confirmed_at, delivery_state/delivery_detail/delivery_updated_at. No backfill: NULL renders as "not yet checked". - The poller reconciles every file entry against its row through a pure content fingerprint (clamped title/question, canonical options, normalised expires_at): confirmed · changed · closed_by_filer · missing · stale_id · unconfirmed. One sync-index read per agent replaces the per-entry exists() probe (a pending entry re-using a terminal row's id was a phantom admit + a "new" broadcast every cycle). Writers are edge-triggered by rowcount; last_confirmed_at is refreshed once a minute per agent, batched. - The cycle reads the tri-state agent_container_states() (Docker unreadable sweeps nothing), runs expiry BEFORE the no-running-agents return (an all-stopped fleet never expired anything), sweeps open rows of stopped agents to unconfirmed:agent_not_running, enters unconfirmed on a read failure only after three consecutive misses, and never writes back over an unparseable file (the old path overwrote the agent's file with the reconstructed responses alone). - The write-back re-reads before writing and writes with if_match; the agent server's PUT /api/files is atomic (tmp + os.replace) and answers 412 on a mismatch. An answer is delivered only into a matching, still-pending entry (a rewritten one is undelivered:entry_changed); terminal flips are fetched by delivery state instead of a 168 h window; platform-minted rows are not_applicable and take no part in delivery. - POST …/respond and the portal answer return 409 item_diverged while sync_state ∈ {changed, closed_by_filer} unless acknowledge_divergence is set (MCP respond_to_operator_queue takes the same parameter, only when given). - operator_queue_aging_hours ops setting (default 24, 0 disables, validated 0–8760, admin-gated ops/config): aging/aged_since are computed once server-side on every list/get and in the portal projection; the receipt platform.aging_since is written into the file entry once at the crossing, taught in the prompt (both copies). - Audit rows for the accountability transitions only (ingested · diverged · reconciled · written_back · undeliverable); the confirmed↔unconfirmed flap is never audited. One thin operator_queue_sync WS trigger per cycle (allow-listed as fleet-level, no payload). Frontend - ONE badge rule (utils/operatorQueue.js::queueSyncBadge) rendered as a BaseBadge by QueueCard, ResolvedCard and PortalAsks, and as a currentColor pill in /m. The store turns 409 item_diverged into a refetch + InlineError beside the controls; the next Send carries the acknowledgement (desktop, /m and the portal). The list's undelivered_count / closed_by_filer_count render in the Operations header with one-click cancel of closed items through the existing ConfirmDialog. The aging bound is a BaseInput in its own component under Settings → Operations. Tests (the fix reverted from a scratch copy, 7 mutations, 7 red): tests/unit/test_2915_operator_queue_sync_honesty.py (divergence fixtures, hysteresis, no write on invalid JSON, tri-state cycle, one broadcast per cycle, write-back gating + if_match, re-read, receipt once, fingerprint stability, the accessors on a real migrated SQLite, both refusal entry points through the real router and service), tests/unit/test_2915_agent_server_files_if_match.py, operatorQueueSyncBadge / queueCardSyncBadge / operatorQueueAgingSetting specs (two mounted), a workspaceAsks case, and a /m e2e case; test_1632/1525 fakes made explicit; ent#467 allowlist. Docs: requirements §26.8 OPS-001-HONEST, operating-room.md "Sync honesty", background-services / api-endpoints / database / workspace / frontend area files, the flows index, two learnings fragments. Residuals registered in the debt inbox (trinity-enterprise#619/#611 own them): closed_by_filer rows still count toward the depth cap until cancelled; a response undelivered because the entry changed after the answer has no re-answer path; a re-ask with a reused id after acknowledged is swallowed by #1631's design. Fixes #2915 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…he ingested audit row; cso diff report (#2915) The new `ingested` audit row copied the entry's `type` — agent-authored and unbounded at ingest and at the DB sink — into audit_log.details, a hash-chained table that cannot be pruned for a year. It now carries the folded token or `other` (`_fold_agent_status`), pinned by a hostile-input test. Verified LOW by an independent verifier and fixed in-branch; the pre-existing unbounded `operator_queue.type` column is registered in the debt inbox. The cso --diff report for the branch is saved beside the prior ones: 0 open findings, 1 found-and-fixed, 7/7 mutations red. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tured dependency; update_file containment via is_relative_to (#2915) - tests: `TestRefusal` answered 401 under CI seed 12345 — an earlier module in that order leaves `routers.operator_queue` holding a different `get_current_user` object than a fresh `from dependencies import …` returns, so an override keyed on the fresh one is ignored and the real auth runs. The override now walks the router's dependant tree and keys off every captured `get_current_user`, asserting it found one. Mechanism reproduced locally with a reloaded `dependencies` module: old shape 401, new shape 409. - agent server: `update_file` guards the workspace with resolved-path `is_relative_to`, the barrier `create_folder` already uses, instead of a string prefix that a sibling name such as `/home/developer2` satisfies. `test_update_file_containment_is_resolved_path_not_string_prefix` goes red with the string prefix restored (sibling case) and green on the branch. - learnings fragment for the override-identity class. CodeQL's 13 `py/path-injection` alerts on this handler are the unmodelled containment-guard false positive of #167–#172 (create_folder); they need a maintainer dismissal. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Failing tests for the porcelain parse in _compute_git_status: the first record is an unstaged ` M .a.txt` beside a real `a.txt`, so the outer strip that eats its X column shows up as a duplicated path. Also pins rename paths (new path + orig_path) and unquoted paths with spaces, and table-tests the NUL-separated parser the fix introduces.
… first character (#2957) _compute_git_status stripped git status --porcelain output before splitting it, which removed the leading space (X column) of an unstaged first entry, so ` M .a.txt` came back as `a.txt`. Plain porcelain also quoted paths with spaces or non-ASCII characters and printed renames as `old -> new`, none of which name a file on disk. Read `git --no-optional-locks status --porcelain -z` instead and parse NUL records in _parse_porcelain_z. A rename or copy entry's `path` is now the new path, with the origin in an additive `orig_path` field; `status` and changes_count are unchanged. The #2742 lock-free argv pins now cover the shipped `-z` argv.
Adds the #2957 revision row to the github-sync flow and appends -z to the three current-state docs that quote the status argv.
…ot 500 the status (#2957) Plain porcelain C-quoted non-ASCII names, so stdout was always ASCII. Under -z git emits the raw bytes, and run_registered's strict text decode raised UnicodeDecodeError on a latin-1 name -> HTTP 500 for the whole /api/git/status (reproduced on git 2.47.3 / python:3.13-slim). run_registered gains an additive errors= passthrough to Popen (default None = strict, unchanged); the status read passes backslashreplace, which stays JSON-encodable (surrogateescape would not). The new real-git test runs on Linux and skips on APFS, which refuses such names; a filesystem-independent test pins the passthrough.
) The chat card's reserved steps sentence is nowrap, so when the feed's read landed with a long agent name ("acme-customer-support-scribe doesn't report steps.") the sentence set the card's width: 207 -> 331 px at 1400 wide, 207 -> 304 at 390, and the "You asked" badge slid with it. `w-0 min-w-full` makes the row take the card's width and never set it; the name truncates ("acme-c... doesn't report steps."), the claim stays whole, and the full sentence is on the row's title. `min-w-0` is dropped because it sets the same property as `min-w-full`. Measured in Chromium on the real card in the chat's container (light and dark x 1400/390 x short/long name): 8/8 stable with the fix; the control (class reverted) jumps on all 4 long-name cells. Case 1 of the mount spec pins the classes and is red with `w-0` dropped.
) The chat's synthetic card titled itself with the raw message; the feed's row carries `clean_title(message)` (whitespace collapsed, cut at 120 with an ellipsis). For a long message on a wide screen the title re-wrapped from two lines to one when the row landed ~2 s in (h 174 -> 154). `previewTitle` in portalWork.js mirrors `clean_title`: Python's exact `\s` set (JS `\s` + U+001C-U+001F + U+0085, minus U+FEFF; checked equal over every code point), a trim that is not `trim()`, and a cut by code point, not UTF-16 unit. Both synthetic chat cards use it: the live one, and the terminal one, which would otherwise re-wrap in the other direction at the end of a long turn. Secret masking is not mirrored; a pasted token still changes the title when the row lands. The contract is a shared fixture, tests/fixtures/portal-work-titles.json, probed from the real `clean_title` (the #925 cron-fixture pattern): test_2964_portal_work_title_fixture.py re-proves every row against the server and that the `masked` markers are current; portalWorkTitle.spec.js proves the mirror agrees. A source guard pins both synthetic cards to previewTitle. Mutations, each red: raw title on the live card; UTF-16 length; `trim()`; no rstrip before the ellipsis.
The steps row takes the card's width and never sets it; both synthetic chat cards title themselves with previewTitle, pinned to clean_title by the shared fixture. The Tests section names the #2964 specs.
…e-2958 # Conflicts: # docs/memory/architecture/mcp-server.md
#2964) frontend-build only triggered on src/frontend/**, so a PR that changes clean_title and re-probes tests/fixtures/portal-work-titles.json would turn the pytest twin green while portalWorkTitle.spec.js never ran: the drift the shared fixture exists to catch would ship green.
The comment called the unmirrored masking 'rare' and limited to a pasted token. sanitize_text also masks plain words: 'Basic question: how do I export?' becomes '***REDACTED***: how do I export?' (the Basic-auth pattern), and so does author=me. Probed from the real clean_title and pinned as a masked fixture row; the comment and the flow doc now say so.
…ntil its owner marks it ready (ent#689) The enforcement half of ent#663: the readiness stamp was display-only, so a calibrating companion's cron seat brief fired exactly like a ready one. - scheduler: _apply_readiness_gate beside the #454 pre-check (sharing a new _record_gate_skip) — cron fires of seat-delivery schedules only; a held brief is a skipped execution carrying the reason, the skipped event and advanced run times; never dispatched, never a failure, retry or alert; every ambiguity fires - backend: GET /api/internal/agents/{name}/brief-readiness over services/role_readiness_gate — stamp ready fires, any other stamp holds; no stamp holds only a companion (template read ≤3 s for x-role presence, its status never trusted); stamp/Docker/container/template ambiguity fails open and is logged - rollout: one-time DB-only seed on both tracks (SQLite versioned entry + Alembic 0073_role_readiness_rollout_seed) stamping ready, changed_by rollout:ent#689, insert-if-absent, for every agent with a live seat brief at deploy — no install changes behaviour; an existing stamp is kept - role card: rollout stamp shown as "carried over", never a person; brief_held adds "its scheduled brief is paused until you/its owner marks it ready" Mutations recorded: the gate letting unstamped companions fire (2 red), the seed overwriting stamps (1 red), the scheduler never asking (1 red). Related to Abilityai/trinity-enterprise#689 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- the readiness gate runs BEFORE the #454 pre-check: a held brief never docker-execs the agent's hook (pinned; red with the order swapped) - rollout seed grandfathers only briefs that fire today — autonomy on too; both tracks (pinned; red without the condition) - role card: brief_held for platform viewers only (/cso hardening), and not when autonomy is off (the autonomy gate stops the schedule first) - agent-existence check moved from the router into the service (Invariant #1) - the one honest limit stated: an UNSTAMPED companion can take itself out of scope; an owner stamp it cannot touch - /cso --diff report: 0 findings Related to Abilityai/trinity-enterprise#689 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ed, an acknowledged answer is delivered, a stopped agent is undelivered, platform alarms are never "missing" (#2915) Blockers from the PR #2989 review, each pinned by a test that was red first: - Wrong-shape JSON (`{"requests": {...}}`, a top-level array) is `unconfirmed:wrong_shape` at both reads and never a write — it used to read as an empty list, mark every row `missing`, overwrite the agent's file and record `delivered`. - `acknowledge_divergence` reaches the file: the operator route and the portal answer stamp `divergence_acknowledged_at` (eighth column, same unshipped migration), and the write-back delivers into the entry as it is now — a rewritten entry, or one the agent closed (status → responded). A landed answer is recognised by its own `responded_at`, never by entry status, and `mark_acknowledged` flips a responded row only when `delivery_state = delivered`, so an entry the agent closed itself is no longer read as an ack of an answer never seen. - The "rows the file no longer carries" loop skips platform-minted alarms (they were never in the file) and responded rows (the write-back re-appends them the same cycle); the `unconfirmed` sweeps exclude reserved id prefixes in SQL. Warnings addressed: - a duplicated id is reconciled once, first entry wins (no confirmed ↔ changed flip-flop); - `if_match_holds` decodes with errors="replace", the same text the download endpoint serves; - the fingerprint also covers type / priority / context / addressee (addressee only when the row resolved one — that value is a roster decision, not a copy); - an answer or terminal flip owed to a non-running agent is `undelivered:agent_not_running` (edge-triggered, audited once) — AC3 on the row, not only the sync state; - the terminal retry set excludes `entry_missing` (it can never land) and goes oldest first; the header's undelivered count excludes a dropped cancellation; the card copy stops promising a retry for it and names when it last tried; - a file present at the cycle-start read and gone at the re-read is left alone that cycle. Docs: operating-room.md, feature-flows.md, security.md §26.8, database/background-services/ api-endpoints area files. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ation entries, re-parent the Alembic revision to 0073 on 0072_agent_capability_grants (#2915) Conflicts: `src/backend/db/migrations.py` (both PRs appended a registry entry — dev's `agent_capability_grants` first, then `operator_queue_sync_state`) and `tests/registry.json` (both appended entries — dev's kept, this PR's two carry their round-2 clauses). `0072_operator_queue_sync_state` → `0073_operator_queue_sync_state`, `down_revision = "0072_agent_capability_grants"`; `check_alembic_heads.py` reports one head (74 revisions). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…E_CHECK_TIMEOUT (ent#689) /review on #3003: the scheduler read the knob but no compose file passed it through, so it could not be set from .env. Also documented in the scheduler env table. Related to Abilityai/trinity-enterprise#689 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ness_rollout_seed)
This was referenced Sep 24, 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 for #2993, #2972, #2974, #2989, #3003. Never merged; members merge individually once green. The last commit re-parents #3003's Alembic revision onto #2989's (0073 → 0074_role_readiness_rollout_seed) so the combined graph is single-head; the same change is applied to #3003's branch after #2989 lands.