Skip to content

DO NOT MERGE — merge train: 3075,3090,3086,3035 - #3095

Closed
vybe wants to merge 28 commits into
devfrom
train/20260929-1926
Closed

vybe wants to merge 28 commits into
devfrom
train/20260929-1926

Conversation

@vybe

@vybe vybe commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

Integration surface for #3075, #3090, #3086, #3035. Never merged; members merge individually once green.

AndriiPasternak31 and others added 28 commits September 27, 2026 19:26
…-enterprise#706)

Amends §11.8 (health now measures agreement with the repository, not only
push success) and adds §11.19: the four persisted columns, the one policy
module and its scheduler mirror, the work-agent predicate, the state rules,
the freshness-guarded freeze and the one-item-per-episode alert.

Refs Abilityai/trinity-enterprise#706
…ty-enterprise#706)

services/sync_freeze_policy.py::classify decides an agent's sync state
(green / yellow / red / unknown), the operator-readable reason, the
recommendation and whether its schedules freeze. Stdlib only, never raises,
vendored byte-identically to src/scheduler/ (the #1088 precedent) with a
parity test. The scheduler's SYNC_FAILURE_FREEZE_THRESHOLD now comes from
the mirror, so there is one number.

Nothing calls classify yet; the poller, the gates and the surfaces follow.

Refs Abilityai/trinity-enterprise#706
…count, last push (trinity-enterprise#706)

agent_sync_state gains diverged_since, dirty_files, dirty_since and
last_successful_push_at, on both tracks: the SQLite migration
agent_sync_state_divergence and Alembic 0080_agent_sync_state_divergence
(parent 0079_telegram_group_context, the live head), plus schema.py and
tables.py. All nullable, no backfill: the clocks start at the first poll
after upgrade, which is the 24 h soak before any divergence freeze.

The upsert takes a KEEP sentinel for the two clocks so a caller can say
clear (None) as distinct from unchanged, which _merged() cannot express.

Refs Abilityai/trinity-enterprise#706
…d the last push (trinity-enterprise#706)

On every poll, inside the leased cycle: diverged_since is set once when the
working tuple (origin on the agent's own branch) shows ahead or behind,
cleared when both are known 0 (or ahead is 0 with no upstream), and kept
when ahead could not be computed. dirty_files comes from the changes_count
the status call already makes, with dirty_since as its own episode clock.
last_successful_push_at is the later of the heartbeat's #3011 record, an
operator Push and the stored value; an image older than #3011 falls back to
its last success cycle. Agent timestamps are validated (explicit offset,
not in the future) before they can win a max().

Refs Abilityai/trinity-enterprise#706
…ce freeze (trinity-enterprise#706)

When a divergence freeze is in force, the poller (inside its leader lease)
raises one sync_diverged item with the deterministic id
sync-diverged-{agent}-{diverged_since}. create_item's (agent_name,
request_id) conflict target makes repeat polls, a restart and a fail-open
double leader converge on one row; a new episode has a new id. The prefix
is platform-reserved so an agent cannot pre-create the id and silence the
alarm about itself. No item for red-but-not-frozen; not auto-resolved. The
context carries counts and the recommendation, no agent-authored error text.

Refs Abilityai/trinity-enterprise#706
… agents (trinity-enterprise#706)

Both enforcement points now decide with the one policy module:

- src/scheduler/database.py: sync_freeze_reason() returns why an agent's
  cron firing is frozen (or None), reading the divergence inputs in the same
  fail-open query; should_freeze_schedules() is its bool view. A reason
  string, not a (bool, reason) tuple, because a tuple is truthy and a caller
  on the old shape would freeze the fleet.
- src/scheduler/service.py: the skipped row's skip_reason carries the
  reason ("Git sync frozen: diverged 0 behind / 7 ahead for 26h"), and the
  comment no longer calls the row count bounded.
- routers/internal.py: sync-health-status keeps its keys and adds
  freeze_reason, divergence_age_s and work_agent via the new
  services/sync_health_view.py, which the poller now uses too.

Refs Abilityai/trinity-enterprise#706
…verdict (trinity-enterprise#706)

GET /api/agents/sync-health and GET /api/agents/{name}/git/sync-state add
state, reason, recommendation, binding, freeze, the divergence age and the
new columns; every pre-existing key is unchanged. MCP get_git_sync_state
passes the backend JSON through, so it carries them too with no TS change.

The batch now reads through one shared query, db.list_sync_health_rows
(agent_git_config joined to live ownership, left-joined to
agent_sync_state), so the dot and the poller see the same set of agents.

Refs Abilityai/trinity-enterprise#706
…(trinity-enterprise#706)

utils/syncHealth.js no longer owns a threshold: classifySyncHealth maps the
backend's state (unknown -> gray) and syncHealthLabel shows its reason and
recommendation on hover. The 24 h / 7 d / behind-is-red rules are gone from
the frontend, so the dot cannot disagree with the scheduler's freeze.

Visible change: an agent ahead of origin with auto-sync off used to render
gray (it had never synced); it now renders the backend's yellow or red.

Refs Abilityai/trinity-enterprise#706
…freeze (trinity-enterprise#706)

Architecture (agent-lifecycle Git Sync Health, database DDL, background
services, backend catalog, api-endpoints), the git-sync-health flow (data
model, poller, the new §2b policy and freeze, the dot, tests, controls,
known limitations), the github-sync journey note, the feature-flows index
row, the RD-03 invariant note, and the user docs: what the sync state
means, when schedules pause and resume, the new Operating Room entry.
The MCP get_git_sync_state description names the new fields (the tool is
a pass-through; no behaviour change).

Refs Abilityai/trinity-enterprise#706
…nterprise#706)

When the agent cannot compute its working tuple (a git timeout sends
ahead_working/behind_working as null), the poller correctly KEEPS
diverged_since, but _coerce_counter still stored the unknown counts as 0.
The row then read "diverged 25 h" next to 0/0: the freeze fired with the
self-contradicting reason "diverged 0 behind / 0 ahead" and the episode's
one sync_diverged item carried ahead 0 / behind 0.

Pass None (upsert's "keep the prior value") for the two working counts
exactly when the clock is kept on an unknown tuple. Scoped to that case:
with no episode to keep, an unknown tuple is still stored as 0 (#2827).

Found by /review on #3035. The new test was red before the fix
(assert (0, 0) == (7, 31)); the control arm is green before and after.
#3005 (skill sets) landed 0080_agent_skill_sets on 0079. This branch's
Alembic revision becomes 0081_agent_sync_state_divergence on top of it,
and the SQLite entry follows agent_skill_sets in MIGRATIONS. No change
to what the migration does; docs updated to the new revision id.
GET /client-portal/agents/{name}/history serialised `cost` on every message to
every principal, a portal token (an external Workspace client) included. The
Workspace contract excludes costs for external clients, and every other
projection already honours it. The history door was the one that leaked.

- PortalHistoryMessage no longer declares `cost`, so the route's response_model
  strips it. The row still stores it.
- Dropped for EVERY principal, not split by principal: nothing reads a history
  message's cost. replyFromHistory copied it into a reply object whose `cost`
  no caller used, so that dead copy goes too.
- Execution ids are kept on purpose. in_flight_execution_id and
  last_turn_outcome.execution_id are the client's OWN turn handles: the 202
  dispatch response issues the same id, and the client streams, cancels and
  reattaches by it. Removing them would break reattach-after-reload and the
  failed-turn verdict (#2320).

Test: test_3063 runs the real route against a real sqlite row written with a
cost of 0.4242, for a portal token and a platform principal, plus the reply
poll's narrow read. It was red on dev ("cost reached the wire … 0.4242") and is
green now. It has a positive control that the costed row is in the response.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on pull agents (#2392)

The effect guard de-duplicates outbound messages, calls, shares and A2A
calls on the execution id, which reached it only as an optional value the
agent typed. Pull re-delivers the same execution after a lease expiry, so a
missing id meant a duplicate reaching a real recipient.

- Agent MCP config sends X-Trinity-Execution-Id per process:
  ${TRINITY_EXECUTION_ID:-manual} for Claude Code and Gemini,
  env_http_headers for Codex. Validator accepts that one extra shape.
- MCP server reads the header on every POST; it wins over the tool arg,
  which stays as the older-image fallback.
- effect_guard: on a pull-mode agent an effect with no usable id (absent,
  unknown, another agent's) is refused with a 422 effect_unguarded and a
  budgeted operator alarm. `manual` (a person's terminal session) and a
  lookup error proceed. Other agents proceed and log effect_guard.degraded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#2392)

A model typing execution_id="manual" as a tool argument (Codex, which sends no
header when unset, or an older image) would skip the pull-mode refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#3063)

POST /client-portal/agents/{name}/chat, the streaming fallback, still
declared PortalChatResponse.cost, so a portal token received the turn's
cost. Drop the field; the service keeps returning it for internal
accounting and response_model strips it. Route test for both principals
(positive control on response/message_id) plus a model-level pin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dentity (Abilityai/trinity-enterprise#715)

The operator queue's reads handed agent-, system- and other machine keys the
email of the person who answered an ask and of the person it was for, and the
file write-back put the answerer's email into the agent's own queue file.

- GET /api/operator-queue, /{id} and /agents/{name}: a non-person principal
  gets an ALLOWLIST projection (`_MACHINE_ROW_FIELDS`) with no
  responded_by_id/responded_by_email, addressed_to_email (file rows too),
  disposed_by_email or resolved_to. A person (JWT or their own user-scoped
  key, `is_person_principal`) still reads the whole row.
- The platform's heads-ups ABOUT a person (the ent#499 problem report: a
  client's email and verbatim words; the ent#308 inbox collision: client
  addresses, an email-slug id) are not returned to a machine at all (skipped
  on list, 404 on the item route). `portal-inbox-collision-` joins the
  reserved prefixes, so that alert also stays out of the agent's file, its
  resume turn and the Execution Context "Ended asks" line.
- The file write-back no longer writes `responded_by` (delivery and
  reconstruction); the prompt says `response`, `response_text` and
  `responded_at`.
- The clear-resolved /ws trigger is the count only (no `cleared_by`).

Tests: tests/unit/test_ent715_queue_person_fields.py (63). Mutations, each
reverted from a scratch copy and red: the allowlist (42 red), the row filter
and the item 404 (3 each), both write-back lines (1 each), the reserved
prefix (2), the trigger (1), the prompt (1), and a new unclassified column
(the person-side classification test, 6 red). One existing assertion flipped:
test_ent611_native_ask.py (a file row's addressee is now withheld).

Registered residuals: the answer/ending wakes still attribute the turn to the
person (source_user_email on the execution row); REST row breadth for an
admin-owned agent key stays with trinity-enterprise#719.

Fixes Abilityai/trinity-enterprise#715

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… read rule (Abilityai/trinity-enterprise#715)

The ent#499 problem report's "the agent never sees the words" now holds through the
agent's own key too (workspace.md, core-agent.md). /cso --diff: no findings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…is reserved (Abilityai/trinity-enterprise#715)

/review I1 on #3090. `_ABOUT_A_PERSON_ID_PREFIXES` said it was a subset of
`_RESERVED_ID_PREFIXES`, and nothing checked that. The machine reads key on the first
tuple; the file write-back, the resume wake and the ended-asks line key on the
second. A heads-up prefix outside the reserved set would be hidden from a machine's
read yet still written into the agent's file.

Mutation: a third, unreserved prefix on the tuple turns exactly this test red
(1 failed, 63 passed); the emitter-driven tests stay green, which is the gap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Alembic 0081_agent_sync_state_divergence -> 0082, down_revision
0081_portal_messages_unread_idx; SQLite entry after
portal_messages_unread_index. Docs/registry conflicts keep both sides.
…L_MIGRATION_TESTING.md (#2392) — mechanical, per the merge-train note on the PR

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…06-707

# Conflicts:
#	docs/memory/feature-flows.md
@vybe vybe added the ui PR touches the frontend UI — triggers Playwright e2e tests label Sep 29, 2026
@vybe

vybe commented Sep 29, 2026

Copy link
Copy Markdown
Contributor Author

merge-train done: #3075, #3090, #3086, #3035 squash-merged individually.

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