Skip to content

DO NOT MERGE — merge train: 2993,2972,2974,2989,3003 - #3006

Closed
trinity-ability wants to merge 38 commits into
devfrom
train/20260924-1343
Closed

trinity-ability wants to merge 38 commits into
devfrom
train/20260924-1343

Conversation

@trinity-ability

Copy link
Copy Markdown
Contributor

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.

AndriiPasternak31 and others added 30 commits September 22, 2026 23:29
…#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>
…lembic revision to 0072 on 0071_seat_decisions; keep both registry tails; ResolvedCard takes the #2201 tokens beside the #2915 badge
…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>
webmixgamer and others added 8 commits September 24, 2026 11:39
…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>
# Conflicts:
#	src/backend/db/migrations.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ui PR touches the frontend UI — triggers Playwright e2e tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants