Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3a5b6fd
feat(deploy): list Trinity on Vultr as an imageless marketplace app
obasilakis Sep 23, 2026
1d620a5
docs(deploy): the measured first boot, and a log that outlives the at…
obasilakis Sep 23, 2026
2b6ae0d
Merge remote-tracking branch 'origin/dev' into feature/2282-vultr-ima…
obasilakis Sep 29, 2026
128a0f6
fix(onboarding): the first-run seeder can create Cornelius again
obasilakis Sep 30, 2026
2755278
fix(onboarding): the seeded Cornelius announces itself
obasilakis Sep 30, 2026
dc38b4d
feat(workspace): Discuss and Dismiss on asks (ent#747, ent#748)
dolho Oct 2, 2026
7483de8
fix(workspace): Discuss opens its new chat instead of "isn't available"
dolho Oct 2, 2026
07ff38b
Merge remote-tracking branch 'origin/dev' into feature/ent747-748-ask…
dolho Oct 2, 2026
8b761e6
feat(workspace): an answered ask's result comes back to its chat (ent…
dolho Oct 2, 2026
cd4e483
Merge remote-tracking branch 'origin/dev' into feature/ent747-748-ask…
dolho Oct 5, 2026
e7a7886
fix(workspace): address #3181 review β€” discussed-ask audience, link-w…
dolho Oct 5, 2026
2444d6a
fix(operator-queue): the flood guard no longer feeds on itself (#3130)
dolho Oct 5, 2026
d9dcc36
fix(operator-queue): pin the write-back's marker default; review foll…
dolho Oct 5, 2026
905ea1d
fix(pull): close the scheduler and breaker gaps on pull pilots (#2514)
obasilakis Oct 5, 2026
cbc01d2
Merge remote-tracking branch 'origin/dev' into feature/2282-vultr-ima…
obasilakis Oct 5, 2026
723b853
fix(canvas): follow a newly created canvas when another is open (#3218)
dolho Oct 5, 2026
76c6f9d
docs(workspace): correct #3181 review I1/I2 doc staleness
dolho Oct 5, 2026
7340c32
fix(git-sync): restore the lost-work caveat a merge reverted (#3022 f…
dolho Oct 5, 2026
0064ddd
fix(canvas): an empty list is not a follow baseline; a search pick ca…
dolho Oct 5, 2026
c29566f
fix(operator-queue): a failed flood-alert create no longer silences t…
dolho Oct 5, 2026
3adbbd1
Merge remote-tracking branch 'origin/dev' into pr-3224
trinity-ability Oct 5, 2026
5a85599
merge-train: update test_3004 --cloud allowlist pins for vultr (#2282…
trinity-ability Oct 5, 2026
6c7dd39
train: #3226
trinity-ability Oct 5, 2026
e9994ed
train: #3225
trinity-ability Oct 5, 2026
cbaa0ee
train: #3222
trinity-ability Oct 5, 2026
a404166
train: #3224
trinity-ability Oct 5, 2026
3defab7
train: #3220
trinity-ability Oct 5, 2026
2faf189
train: #3181
trinity-ability Oct 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions config/trinity-meta-prompt/prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Before performing an action that cannot be undone or verified afterwards β€” pay

**Priority levels:** `critical`, `high`, `medium`, `low`

**Set `expires_at`** on requests that gate an action: an ISO-8601 time with a timezone, at least 15 minutes out. If it passes without a response the ask ends `expired` β€” treat that as "not approved; do not proceed", and do not re-ask the same action without new information. When you do re-ask, set `supersedes_expired` to the expired ask's `request_id`.
**Set `expires_at`** on requests that gate an action: an ISO-8601 time with a timezone, at least 15 minutes out. If it passes without a response the ask ends `expired` β€” treat that as "not approved; do not proceed", and do not re-ask the same action without new information. When you do re-ask, set `supersedes_expired` to the expired ask's `request_id`. If the person you addressed dismisses it, it ends `dismissed` (`response` empty) β€” they chose not to decide: do not proceed, and do not raise the same ask again straight away.

### The queue file (fallback)

Expand All @@ -80,7 +80,7 @@ Until it is removed, an entry appended to the `requests` array of `~/.trinity/op
}
```

The operator's answer is written back into the entry: `status: "responded"` with `response`, `response_text` and `responded_at` β€” never who answered. An item that has waited past the operator's aging bound carries a `platform.aging_since` timestamp written by Trinity β€” read it, never write to `platform`. After processing a response, update the item's status to `"acknowledged"`. Keep only `pending` and `responded` items plus up to 3 recent `acknowledged` items. The platform database is the permanent record. An ID you raised with `ask_operator` is never read from the file.
The operator's answer is written back into the entry: `status: "responded"` with `response`, `response_text` and `responded_at` β€” never who answered. An item that has waited past the operator's aging bound carries a `platform.aging_since` timestamp written by Trinity β€” read it, never write to `platform`. While Trinity is holding new entries from this file, the file carries a `platform.ingestion` block: `reason` is `queue_full` (you already have `max_pending` open requests β€” wait for one to end), `rate_limited` (too many too fast β€” they are read again shortly) or `invalid_id` (an entry's `id` is malformed and will never be read β€” fix it), and `since` says when the hold began. Held entries stay `pending` and are read again every few seconds; the block is removed once nothing is held. `ask_operator` refuses the same cases immediately, with the reason. After processing a response, update the item's status to `"acknowledged"`. Keep only `pending` and `responded` items plus up to 3 recent `acknowledged` items. The platform database is the permanent record. An ID you raised with `ask_operator` is never read from the file.

### When to Use

Expand Down
4 changes: 2 additions & 2 deletions docker/base-image/agent_server/auto_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,8 @@ async def run_one_cycle(client: httpx.AsyncClient, home: Path, run_once) -> Opti
# trinity-enterprise#703 β€” the pull cycle (invariant G3: human and fleet work
# reaches the agent within a bound). A second loop beside the push loop, gated
# per cycle on the owner's `pull_sync_enabled`; the cycle itself is
# `routers/git.py::_run_pull_once` (never under a running execution, never
# discards local work).
# `routers/git.py::_run_pull_once` (never starts under a running execution,
# never resets over a registered one).
# ---------------------------------------------------------------------------

_last_pull_resolved: Optional[bool] = None
Expand Down
13 changes: 8 additions & 5 deletions docker/base-image/agent_server/routers/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -1177,9 +1177,10 @@ def git(*args, timeout=60):


def _integrate_remote(home_dir: Path, branch: str, ahead: int) -> Optional[str]:
"""Bring `origin/<branch>` into the checked-out branch without ever losing
local work: a fast-forward when nothing is committed locally, a rebase
(aborted on conflict) when something is. See `_with_stash`."""
"""Bring `origin/<branch>` into the checked-out branch, putting uncommitted
edits back (or naming the stash): a fast-forward when nothing is committed
locally, a rebase (aborted on conflict) when something is. See
`_with_stash` / `_safe_to_reset` for what an undo does not protect."""
def step() -> Optional[str]:
if ahead == 0:
ff = run_registered(["git", "merge", "--ff-only", f"origin/{branch}"],
Expand Down Expand Up @@ -1254,8 +1255,10 @@ def _run_pull_once(home_dir: Path) -> Dict:
sync-state.json happens under that lock).
- Stale lock litter is reaped first, as the push cycle does: a pull-only
agent has no push cycle to do it.
- Refuses to run over unmerged paths. Local work is never discarded; a
failure is recorded, not resolved.
- Refuses to run over unmerged paths. An undo never resets over a
registered execution (`_safe_to_reset`); writes outside the process
registry (Files API, docker exec, web terminal) made during the
integrate window are not protected. A failure is recorded, not resolved.

`behind_after_pull` is the checked-out branch's own lag behind
`origin/<branch>`, not its lag behind `main`; `behind_main` in the git
Expand Down
2 changes: 1 addition & 1 deletion docs/memory/architecture/api-endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ WebSocket events: `operator_queue_new`, `operator_queue_responded` / `operator_q

**Addressed asks (ent#364).** `operator_queue.addressed_to_email` names the HUMAN an item is for; `agent_name` only ever said which agent. NULL β€” every pre-ent#364 row β€” means "the operator answers", so the column is additive by construction. It is a **column and not a key in `context`** because it is an authorization decision (who may answer, whose Workspace sidebar it appears in) and `context` is agent-authored JSON whose clamp bounds size and type only. Validated at the one agent-authored boundary (`operator_queue_service._validated_addressee`, inside the #1632 clamp) against `client_portal.service.agent_on_roster(agent, email, include_owned=False)` β€” an agent may address only someone already on its roster β€” and **fails closed**: malformed, off-roster, or an unreadable roster all drop to NULL. The read/answer surface is **OSS core** (ent#428): `client_portal/asks/`, mounted unconditionally in `main.py` at `/api/enterprise/client-portal/asks` β€” a prefix that is retained history, like the `enterprise_`-prefixed portal tables, not a licensing claim. It re-checks the roster at read time so a revoked share stops showing an already-raised ask, projects an explicit client-facing shape (never `context`), and answers through the same ask sink as the operator respond route (`services/ask_service.py`, trinity-enterprise#611), so the ledger, write-back, audit and the WS trigger are unforked, and it is person-only like respond (a non-person platform principal gets 403 `person_required`). `responded_by_id` stays NULL for a client answerer β€” there is no `users` row, and inventing one would be a lie in the audit trail. **The list fails loud (trinity-enterprise#610 PR A0):** `GET /api/enterprise/client-portal/asks` returns **503 `{code: "asks_unavailable", message}`** with `Retry-After: 20` (the Workspace poll interval) when any of its queue reads raises (the agent set, the count or the page, #3059) or the read-time roster re-check cannot read the roster (`asks/service.AsksUnavailable`, strict `_on_roster`); it never answers an outage with `[]`. `answer_ask`'s roster failure stays the uniform 404. **One ownership check (trinity-enterprise#610 PR A2):** `asks/service._owned_ask` (lookup + addressee + `_VISIBLE_KINDS` + roster) is shared by the answer path (non-strict β€” its refusal is the 404 whatever the cause) and the context read (strict β€” a roster outage is 503); an addressed row of a kind no list shows is now a 404 on answer too.

**Ingestion caps (#1632).** There is **no HTTP create** β€” every operator-queue item is created through `db.create_operator_queue_item`, and the only untrusted producer is the sync service reading the agent-authored `~/.trinity/operator-queue.json` (`operator_queue_service._sync_agent`). #1402 makes this queue the approval channel for irreversible actions, so a compromised / prompt-injected agent that floods plausible "approve this" items causes operator fatigue β†’ reflexive approval (XSS is already handled by DOMPurify; the exposure is volume + social engineering). The one agent-authored seam is bounded by two independent limits plus field hygiene, all env-tunable (see requirements Β§26.7): (1) a per-agent **pending-DEPTH cap** (`db.count_operator_queue_pending_for_agent`, default 25) β€” the **primary** bound, DB-measured β‡’ Redis-independent; at the cap the sync **stops** ingesting (never drips a growing file) and holds the surplus behind one aggregated alert; (2) a per-agent + fleet **create RATE limit** (`rate_limiter.check`, 60/60s + 300/60s, fail-open); (3) a total field-hygiene clamp (`_clamp_ingested_item`, inside the #1525 create try/except) β€” truncate-with-marker title/question, context/options over cap β†’ marker (validated `execution_id` only), non-dict context β†’ `{}`, `created_at` normalized to ingest time, priority validate-only; (4) a **reserved-id guard** rejecting agent ids that start with a platform-reserved prefix (`queue-flood-`/`poison-`/`cb-dormant-`/`sync-failing-`/`git-bloat-`/`skill-not-found-`/`val_`/`system-seed-`/`base-image-stale-`/`alert-budget-`) so an agent can't pre-create β€” and via `on_conflict_do_nothing` silence β€” its own flood alarm or the #1402 poison alert, plus a malformed/oversize-id reject; (5) a per-cycle scan bound (skip an oversized file wholesale; cap at 500 requests/cycle). The 5s sync loop is **leader-locked** (`opqueue:leader`, mirror monitoring #1464) so `--workers 2` doesn't double-charge / double-broadcast / double-scan. Platform creates bypass `_sync_agent`, and the exemption is **scoped by influence, not by caller location (#1677)**: a *platform-only* emitter (volume bound by platform cadence β€” edge-triggered, idempotent/bucketed id, leader-locked, or operator-driven: lease-reaper poison-park #1402, `validation_service._notify_operator_on_failure` β€” converted to a **direct DB create** in #1632 β€” and the internal breaker/skill/git alerts) stays direct and unthrottled, while an *agent-influenceable* one β€” `task_execution_service._alert_skill_not_found` (#1410), whose per-command dedup distinct unknown slash-commands defeat at $0/turn β€” routes through `operator_queue_service.create_bounded_alert`: a per-(agent, registered-type) pending-DEPTH budget (`OPERATOR_ALERT_MAX_PENDING_PER_TYPE`, default 5; type derived from `item["type"]` against the `_BUDGETED_ALERT_TYPES` frozenset; **fail-closed on every arm** β€” an unreadable count / unregistered type / failed create suppresses the ALERT only, the FAILED execution rows stay the primary surface, and the paired notification is gated on the same bool), with ONE cooldown-gated `alert-budget-{agent}-{type}-b{bucket}` episode alert per window (deterministic bucket β‡’ the `(agent_name, request_id)` on-conflict target dedups cross-worker; no `held` count, no agent-controlled text). The classification is CI-forced by the AST caller-parity guard `tests/unit/test_1677_operator_alert_emitters.py` (every `create_operator_queue_item` call site must be allowlisted platform-only or routed; OSS tree only β€” the private submodule owns its twin); a sink-level default bound was rejected because it fails QUIETLY at the load-bearing poison-park create where the parity test fails LOUDLY at CI (#1890's lesson deliberately inverted). A generous DB-sink belt in `create_item` (reject title>4 KiB / question>16 KiB / context>64 KiB / id>512; the derived `execution_id` COLUMN belted to None on non-str/>512, #1677) is a second layer so the exemption is never solely load-bearing (#1525 validate-at-boundary-AND-at-sink). A depth-held / rate-skipped / oversize-file episode emits **one** `queue-flood-{agent}-{utc_now_iso()}` alert (un-guessable id, priority `high`, in-memory cooldown, emit-failure-safe). The platform-emitter residual named on the pull-mode default-ON gate list (#1081 / `TARGET_ARCHITECTURE.md`) is closed by #1677; the remaining gate items are unchanged.
**Ingestion caps (#1632).** There is **no HTTP create** β€” every operator-queue item is created through `db.create_operator_queue_item`, and the only untrusted producer is the sync service reading the agent-authored `~/.trinity/operator-queue.json` (`operator_queue_service._sync_agent`). #1402 makes this queue the approval channel for irreversible actions, so a compromised / prompt-injected agent that floods plausible "approve this" items causes operator fatigue β†’ reflexive approval (XSS is already handled by DOMPurify; the exposure is volume + social engineering). The one agent-authored seam is bounded by two independent limits plus field hygiene, all env-tunable (see requirements Β§26.7): (1) a per-agent **pending-DEPTH cap** (`db.count_operator_queue_pending_for_agent`, default 25) β€” the **primary** bound, DB-measured β‡’ Redis-independent; at the cap the sync **stops** ingesting (never drips a growing file) and holds the surplus behind one aggregated alert; (2) a per-agent + fleet **create RATE limit** (`rate_limiter.check`, 60/60s + 300/60s, fail-open); (3) a total field-hygiene clamp (`_clamp_ingested_item`, inside the #1525 create try/except) β€” truncate-with-marker title/question, context/options over cap β†’ marker (validated `execution_id` only), non-dict context β†’ `{}`, `created_at` normalized to ingest time, priority validate-only; (4) a **reserved-id guard** rejecting agent ids that start with a platform-reserved prefix (`queue-flood-`/`poison-`/`cb-dormant-`/`sync-failing-`/`git-bloat-`/`skill-not-found-`/`val_`/`system-seed-`/`base-image-stale-`/`alert-budget-`) so an agent can't pre-create β€” and via `on_conflict_do_nothing` silence β€” its own flood alarm or the #1402 poison alert, plus a malformed/oversize-id reject; (5) a per-cycle scan bound (skip an oversized file wholesale; cap at 500 requests/cycle). The 5s sync loop is **leader-locked** (`opqueue:leader`, mirror monitoring #1464) so `--workers 2` doesn't double-charge / double-broadcast / double-scan. Platform creates bypass `_sync_agent`, and the exemption is **scoped by influence, not by caller location (#1677)**: a *platform-only* emitter (volume bound by platform cadence β€” edge-triggered, idempotent/bucketed id, leader-locked, or operator-driven: lease-reaper poison-park #1402, `validation_service._notify_operator_on_failure` β€” converted to a **direct DB create** in #1632 β€” and the internal breaker/skill/git alerts) stays direct and unthrottled, while an *agent-influenceable* one β€” `task_execution_service._alert_skill_not_found` (#1410), whose per-command dedup distinct unknown slash-commands defeat at $0/turn β€” routes through `operator_queue_service.create_bounded_alert`: a per-(agent, registered-type) pending-DEPTH budget (`OPERATOR_ALERT_MAX_PENDING_PER_TYPE`, default 5; type derived from `item["type"]` against the `_BUDGETED_ALERT_TYPES` frozenset; **fail-closed on every arm** β€” an unreadable count / unregistered type / failed create suppresses the ALERT only, the FAILED execution rows stay the primary surface, and the paired notification is gated on the same bool), with ONE cooldown-gated `alert-budget-{agent}-{type}-b{bucket}` episode alert per window (deterministic bucket β‡’ the `(agent_name, request_id)` on-conflict target dedups cross-worker; no `held` count, no agent-controlled text). The classification is CI-forced by the AST caller-parity guard `tests/unit/test_1677_operator_alert_emitters.py` (every `create_operator_queue_item` call site must be allowlisted platform-only or routed; OSS tree only β€” the private submodule owns its twin); a sink-level default bound was rejected because it fails QUIETLY at the load-bearing poison-park create where the parity test fails LOUDLY at CI (#1890's lesson deliberately inverted). A generous DB-sink belt in `create_item` (reject title>4 KiB / question>16 KiB / context>64 KiB / id>512; the derived `execution_id` COLUMN belted to None on non-str/>512, #1677) is a second layer so the exemption is never solely load-bearing (#1525 validate-at-boundary-AND-at-sink). A depth-held / rate-skipped / oversize-file episode emits **one** `queue-flood-{agent}-{utc_now_iso()}` item per over-cap **episode** (#3130: edge-triggered β€” a second alert needs the condition to have cleared; the 300 s cooldown is only the minimum spacing between episodes), created through `create_bounded_alert` as the budgeted type `queue_flood` (#1677 backstop: ≀5 pending per agent), un-guessable id, priority `high`, wrapped so an emit failure never kills the sync. Platform-minted rows do **not** count toward the agent's own depth (file seam and native `queue_full` read one predicate, `_own_pending_conds`, with `_RESERVED_ID_PREFIXES` excluded) β€” before #3130 the alert counted toward the depth it reported and was re-minted every cooldown window (386 of one agent's 435 rows). A held file is told so: a file-level `platform.ingestion` block (`reason` ∈ `queue_full`/`rate_limited`/`invalid_id`, `max_pending`, `since`) rides the guarded write-back, written only when it changes and removed once nothing is held; a refused write is retried only after the file changes. The pre-#3130 backlog is collapsed once by the `supersede_queue_flood_backlog` migration (both tracks): newest pending flood row per agent kept, the rest `cancelled`, `disposed_by = 'platform'`, reason `superseded`. The platform-emitter residual named on the pull-mode default-ON gate list (#1081 / `TARGET_ARCHITECTURE.md`) is closed by #1677; the remaining gate items are unchanged.

### Respond β†’ Resume Dispatch (ent#329)

Expand Down
Loading
Loading