Skip to content
4 changes: 2 additions & 2 deletions config/trinity-meta-prompt/prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Before performing an action that cannot be undone or verified afterwards — pay
**Request IDs must be globally unique.** Derive the `request_id` from your current execution ID (see the Execution Context block), e.g. `approval-{execution_id}-{short-slug}`. Never use date-serial IDs like `req-20260307-001` — a second task that picks the same ID gets the first ask's receipt instead of a new ask. Re-using your own derived ID when the same task runs again is safe and intentional: it prevents duplicate requests.

**Request types:**
- `approval` — You need a yes/no or multi-choice decision. Provide `options`, and state the exact action and its parameters in `proposal` so the operator can verify what they are approving.
- `approval` — You need a yes/no or multi-choice decision. Provide `options`, and state the exact action and its parameters in `proposal` so the operator can verify what they are approving. Never list `(something else)` as an option: the platform offers it on every approval. An answer of `(something else)` means none of your options is approved — carry out none of them; the person's instruction is in `response_text`, so re-plan from it or ask again.
- `question` — You need freeform guidance. No `options` needed.
- `alert` — You're reporting a situation. No decision needed; it goes to the operators.

Expand Down Expand Up @@ -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`. 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.
The operator's answer is written back into the entry: `status: "responded"` with `response`, `response_text` and `responded_at` — never who answered. On an approval, `response` is one of your options or the reserved value `(something else)`, which comes with the person's instruction in `response_text`. 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
2 changes: 1 addition & 1 deletion docs/TRINITY_COMPATIBLE_AGENT_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2092,7 +2092,7 @@ All human/operator communication on Trinity is **asynchronous**, mediated by the

1. **Park** the request (approval / question / alert) with `ask_operator`, or, as the fallback, by appending an entry to the queue file.
2. **End the turn.** A turn must never wait, poll, or sleep for a human response — a human may answer in minutes or days, and a blocked turn burns its entire timeout budget while pinning platform capacity. Never assume a synchronous human answer is available mid-turn.
3. **Process the outcome in a later turn.** `get_my_ask` returns how an ask ended (answered, cancelled or expired, with the answer when there is one), and the Execution Context block of the system prompt lists the agent's asks that ended in the last 24 hours. For a queue-file entry, check the file for `status: "responded"` items, act on them, then mark them `"acknowledged"`.
3. **Process the outcome in a later turn.** `get_my_ask` returns how an ask ended (answered, cancelled or expired, with the answer when there is one), and the Execution Context block of the system prompt lists the agent's asks that ended in the last 24 hours. For a queue-file entry, check the file for `status: "responded"` items, act on them, then mark them `"acknowledged"`. On an approval the answer is one of the agent's own options or the platform-reserved value `(something else)` (#3242): none of the options is approved — carry out none of them — and the person's instruction is in `response_text`. Never list `(something else)` as an option yourself; `ask_operator` refuses it.

**Ask before irreversible actions.** Before an action the platform cannot undo or verify — payments, emails/messages through the agent's own credentials, public posts, destructive deletions — park an `approval` and end the turn when uncertain. Put the exact action in the ask's `proposal`. This matters most under re-delivery: pull-mode coordination (#1081) re-runs a turn whose worker died, so a task you receive may have partially run before. Check your own records and your earlier asks before repeating an irreversible effect; do the reversible parts first and gate only the irreversible step.

Expand Down
1 change: 1 addition & 0 deletions docs/memory/architecture/backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@
- `monitoring_alerts.py` - Alert threshold configuration
- `heartbeat_service.py` - Agent push-heartbeat liveness layer — see [Heartbeat Liveness](reliability.md#heartbeat-liveness-reliability-004-307)
- `operator_queue_service.py` - Operating Room sync with agent containers (OPS-001); the agent-authored ingestion boundary enforces the per-agent depth/rate/size caps + reserved-id guard + leader lock (#1632)
- `operator_queue_choices.py` - The one rule for what an approval's `response` may be (#2376, #3242): an offered option (exact match, else `ResponseNotOfferedError` with the list) or the reserved `SOMETHING_ELSE = "(something else)"` with an instruction in `response_text` (`InstructionRequiredError` / `ReservedValueError` / `NotOffMenuError`, all `ReservedAnswerError`); import-free leaf the ask sink calls before any write; also holds `OPTIONS_DROPPED_MARKER`. Mirrored in `utils/operatorQueue.js` + MCP `types.ts` (parity-tested)
- `sync_health_service.py` - Git sync health polling — see [Git Sync Health](agent-lifecycle.md#git-sync-health-389390)
- `sync_freeze_policy.py` - The one sync-health rule (state / reason / recommendation / freeze), stdlib leaf, vendored byte-identically to `src/scheduler/` (ent#706) — see [Git Sync Health](agent-lifecycle.md#git-sync-health-389390)
- `sync_health_view.py` - `sync_view(row, config)`: the backend's single call into the policy, used by the poller and every sync-health read (ent#706)
Expand Down
2 changes: 1 addition & 1 deletion docs/memory/feature-flows/mobile-admin-pwa.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Browser (/m)

### Ops Tab
- Sub-tabs: Queue | Alerts
- Queue: operator queue items answered inline. **#2370:** controls switch on the item **type** (desktop parity) — an *approval* is select an option → restated consequence ("Sending **Deny** to <agent> — it reads this as your decision on its next run") → optional note → `Cancel` + `Send: <option>` (the safe action is first and focused on reveal, p19; nothing sends on one tap, and Enter in the note does not send); a *question* is a text answer + Send; an *alert* is `Got it`. The body comes from `utils/operatorQueue.js::buildQueueResponse` — the decision rides `response` (the option / the typed answer / `acknowledged`), a note rides `response_text` — the same builder the desktop store uses. Before #2370 every option tap POSTed a hard-coded `response: 'approved'` with the tapped option in `response_text` (a Deny was recorded as an approval, a typed answer became a note), the card answered on one tap with no note, and the type line read a nonexistent `request_type` (blank). On success the card is dropped locally before the refetch (`fetchQueue` swallows its own errors); a 5xx/transport failure renders an `InlineError` **inside the card** (p18) and keeps the selection + note for a retry; a 409/400 "no longer pending" refusal goes to the persistent page-level banner (the card is about to leave) with attribution-free copy. Per-card state (`selectedOptions`/`responseTexts`/`respondErrors`/`respondingItems`) is keyed by item id, pruned on every poll and wiped on logout — except for a card whose POST is still outstanding, which is never pruned: `respondingItems` is the in-flight guard `sendQueueResponse` checks, and the `limit: 100` window (ordered status → priority → created_at) is one a busy queue can push an item out of and back into, which would re-enable Send under a live POST and report a recorded answer as not recorded. `fetchQueue` is sequence-guarded on **every** exit — a superseded poll writes neither the list, nor `fetchError.queue`, nor `loading.queue` — so a slow poll that rejects cannot paint a stale-refresh banner over data a newer fetch just proved fresh. Pinned by `tests/unit/operatorQueueResponse.spec.js` + `e2e/mobile-admin-queue-respond.spec.js` (`@smoke`, captures the real POST body; fails on the pre-fix code with `captured POSTs: [{response:'approved',…}]`).
- Queue: operator queue items answered inline. **#2370:** controls switch on the item **type** (desktop parity) — an *approval* is select an option → restated consequence ("Sending **Deny** to <agent> — it reads this as your decision on its next run") → optional note → `Cancel` + `Send: <option>` (the safe action is first and focused on reveal, p19; nothing sends on one tap, and Enter in the note does not send); a *question* is a text answer + Send; an *alert* is `Got it`. **#3242:** an approval's options group also carries a **Something else** chip (reusing `ops-option-btn`, hidden when the item's `decided_by_options` is true — a platform-minted approval): it opens the same form with the consequence line "Sending your instruction to <agent> — it will carry out none of the options and re-plan from your text.", the placeholder "What should it do instead?" and a **Send instruction** button disabled until there is text (`maxlength=4000`); the body is `{response: '(something else)', response_text: <instruction>}` from the same builder (`tests/unit/mobileAdminSomethingElse.spec.js`). The body comes from `utils/operatorQueue.js::buildQueueResponse` — the decision rides `response` (the option / the typed answer / `acknowledged`), a note rides `response_text` — the same builder the desktop store uses. Before #2370 every option tap POSTed a hard-coded `response: 'approved'` with the tapped option in `response_text` (a Deny was recorded as an approval, a typed answer became a note), the card answered on one tap with no note, and the type line read a nonexistent `request_type` (blank). On success the card is dropped locally before the refetch (`fetchQueue` swallows its own errors); a 5xx/transport failure renders an `InlineError` **inside the card** (p18) and keeps the selection + note for a retry; a 409/400 "no longer pending" refusal goes to the persistent page-level banner (the card is about to leave) with attribution-free copy. Per-card state (`selectedOptions`/`responseTexts`/`respondErrors`/`respondingItems`) is keyed by item id, pruned on every poll and wiped on logout — except for a card whose POST is still outstanding, which is never pruned: `respondingItems` is the in-flight guard `sendQueueResponse` checks, and the `limit: 100` window (ordered status → priority → created_at) is one a busy queue can push an item out of and back into, which would re-enable Send under a live POST and report a recorded answer as not recorded. `fetchQueue` is sequence-guarded on **every** exit — a superseded poll writes neither the list, nor `fetchError.queue`, nor `loading.queue` — so a slow poll that rejects cannot paint a stale-refresh banner over data a newer fetch just proved fresh. Pinned by `tests/unit/operatorQueueResponse.spec.js` + `e2e/mobile-admin-queue-respond.spec.js` (`@smoke`, captures the real POST body; fails on the pre-fix code with `captured POSTs: [{response:'approved',…}]`).
- **Recently ended** (trinity-enterprise#611): under the Queue, the ≤ 5 items that ended most recently — answered, cancelled or expired — with who and when (`utils/operatorQueue.js::recentlyEnded` / `queueEnding`), from the same fetch the Queue already makes (it used to drop them, so an ask cancelled or expired elsewhere simply vanished from the phone). Shown whatever the pending list's state, empty included.
- Alerts: notifications with acknowledge button
- Badge counts on tab and sub-tabs
Expand Down
Loading
Loading