Repository navigation
feat(operator-queue): agents raise asks natively — ask_operator, receipts, re-asks (abilityai/trinity-enterprise#611, PR B) - #3028
Conversation
/review ReportBranch: Scope
Execution coverage (Step 2.5)
Fix mutations: not a fix PR. The three fixes this review made each went red first; see below. Fixed in this review (each with a failing test first)
Informational (accepted, not fixed)
Clean categories
Verification
After the review:
|
|
merge-train (2026-09-27): not on this run. #3023 and #3026 are on 1. An operator approving a native ask never sees 2. Smaller items:
What holds: the agent-callable write is fenced by Retarget: |
…ipts, re-asks (Abilityai/trinity-enterprise#611, PR B) An agent raises an ask through ONE platform call instead of appending to ~/.trinity/operator-queue.json: POST /api/agents/{name}/operator-queue and the MCP tool ask_operator, both self-acting (the agent comes from the key; any other principal gets 403 agent_identity_required). - ask_service.raise_ask is the create entry point of the ask sink, the seam the gate path (trinity-enterprise#164) calls with raised_by="gate". Static validation with named 422s; replay BEFORE every time-dependent check; the 15-minute deadline floor; the own-expired-predecessor link and the reask_requires_link refusal; role addressing; the #1632 rate buckets; an atomic per-agent create (replay, depth cap and insert under one lock: a PG advisory transaction lock, SQLite BEGIN IMMEDIATE); one `raised` audit row and a thin operator_queue_new trigger. - The receipt names the role, never an email. A replay returns the first receipt with `differs`; `wakes_on_ending` says whether the owner's wake opt-in is on. - Native rows sit outside the file contract everywhere. The poller skips a file entry that re-uses a native id before any branch reads it (logged once), creates through create_operator_queue_item_with_outcome and counts only what it inserted, and names the file channel's deprecation once per agent per process. - The Operator Communication prompt leads with ask_operator by its bare name; the file is the fallback for two releases (the meta-prompt copy and the agent guide mirror it). - The Operations cards link a re-ask and the expired ask it re-raises. Rebuilt on dev after #3023 squash-merged (f6dfb00): this commit is the net delta of the former PR B commits 19b2e53..3a2dffd (the feat above, the two review fixes: the `raised` audit row is the agent's, and reads never name a native ask's resolved person; plus the docs), replayed onto dev unchanged. Same 47 files and the same added/removed lines as B's old diff against #3023's final head dfcb7c0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3a2dffd to
8312311
Compare
|
Closed and reopened only to start CI on the |
…Abilityai/trinity-enterprise#611, PR B) The native ask path reads OPERATOR_QUEUE_PROPOSAL_MAX_BYTES (default 8192), but no compose file passed it into the backend container, so setting it in .env had no effect on a deployed instance. The 14 sibling operator-queue caps are all in the three compose files and .env.example; this one now is too. It sits right after OPTIONS_MAX_BYTES, the same position as the constant in operator_queue_service.py. So prod and hosted keep identical environment lists, and the #2280 parity guard compares them in order. The deploy guide's env table and requirement §26.10 name it as well. Found by /validate-pr §4.9. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AndriiPasternak31
left a comment
There was a problem hiding this comment.
Request changes. The plumbing is good: the per-agent atomic create, the rowcount-based inserted, the file-contract exclusion and the named refusals. Suites are green locally (245). Three contract items, two of them about the #164 seam I'll build on:
proposalis shown to nobody. The prompt (platform_prompt_service.py:156, the meta-prompt, the agent guide) now tells agents to put the exact action inproposalinstead ofcontext.contextrenders on QueueCard (:90);proposalrenders nowhere (Ops cards, Workspace). On merge, approvers approve without seeing the amount or target. Please render it read-only on the cards, or keep "mirror it intocontext" until you do.- The replay namespace is shared across
raised_by.raise_askreplays any(agent, request_id)row. I checked: an agent-raisedgate-call-abccomes back asreplayedto a laterraised_by="gate"call with a different proposal, and the receipt has noraised_by. The gate also can't use a reserved prefix, because_validated_askrefusesreserved_request_idregardless of caller. That is the #1632 pre-squat class aimed at the approval gate. Suggested fix:- reserved-prefix refusal for
raised_by == "agent"only, plus one prefix reserved for platform raises; - refuse a replay whose stored
raised_bydiffers; - put
raised_byon the receipt.
- reserved-prefix refusal for
people_for→[]forprimarygoes to the owner.resolve_role_peopledocuments[]as "nobody fills it", but_addressdoesif people:and falls through to the owner, withresolved: true. ent#606 says primary falls back to operator when no primary is assigned; owner = primary only without a provider. Please branch onis not Noneand add the[]test.
Smaller: create_native_item's INSERT has no conflict clause. On PG, a file ingest of the same id racing the native raise gives a 500 instead of replayed, because the poller doesn't take the advisory lock. One on_conflict_do_nothing + re-read fixes it.
On the #164 seam (answering your ent#164 question): one raise, one end sink, observers that only see CAS winners — agreed. For the gate to need no #611 edits:
-
raise_askshould take the caller's limits, not the agent's:- the 8 KB
proposalcap refuses a frozen SKILL.md, and skills are in the v1 class; - C6
reask_requires_linkfires on a gate re-raise after a timeout, and the agent can't supply "what's new" through a tool call; - the agent's rate/depth bucket applies to the gate too.
I'd scope C6 and the agent caps to
raised_by == "agent". - the 8 KB
-
_wake_filershould only wakeraised_by == "agent"rows. Otherwise, on a gate approval, the agent is woken to redo the action while the gate resumes it. -
Observers are in-process at-most-once; fine, #164 will keep its own durable effect_guard reconciliation keyed on ask id. Worth one line in the seam docstring.
-
Later, additive: an end-side "may this person end this ask" predicate against
to_role/resolved_to. Today any person with access to the agent can answer an approval addressed toapprover.
Happy to pair on the F2 shape so #164 can start against it.
|
Correction to my review's seam note: C6 does not block the gate. After a timeout, a gate re-raise that sets The full seam reply is on trinity-enterprise#164. What still needs to land in #611 before merge:
|
…ed prefix, raised_by on the receipt, primary's empty answer, rate check first (Abilityai/trinity-enterprise#611, PR B) From the changes-requested review and the merge-train note on #3028: - `gate-` is a platform-reserved id prefix. A gate raise (`raised_by="gate"`, `channel="gate"`) must use it, and an agent's raise may not. So an agent can no longer pre-create the gate's id and have the gate's raise answered as a replay of the agent's own proposal. And it is no longer woken to redo an action the gate resumes itself, because `is_platform_minted` keys on the reserved tuple and gates both wakes. The receipt now says who raised the ask (`raised_by`), and an unknown raiser or channel is a ValueError. - A provider that answers "nobody" (`[]`) for `primary` now sends the ask to the operators with `resolved: false` (the ent#606 fallback). The owner stands in only when no provider answers at all. - The rate check runs right after the replay, before the re-ask scan and the owner lookup read the database. A refused ask now spends a token; a replay still spends none. - The native insert takes ON CONFLICT DO NOTHING. The file poller does not take the per-agent lock, so on PostgreSQL a file entry re-using the id can land between the replay check and the insert. That now comes back as a replay of the file row, not a 500. - `get_my_ask` names a re-ask's predecessor by request_id, as the receipt does. The row still stores its uuid. - Docstrings: a gate raise shares the agent's rate and depth budget (it fails closed), and ending observers run in-process and at most once. `channel` is documented as file|mcp|gate. Tests came first: 18 went red before their fixes. New tests also run the real owner lookup, and a real minted key through the route: key resolution, the identity check and the ephemeral fence. Each of 13 mutations of the fixed lines turns a test red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ision is made (Abilityai/trinity-enterprise#611, PR B) The prompt tells agents to put the exact action in `proposal` "so the operator can verify what they are approving", but nothing rendered it. An agent that followed the prompt moved the one thing an approver must check out of view (review on #3028). - `components/operator/QueueProposal.vue` renders it read-only. The rows come from `utils/operatorQueue.js::proposalRows`: top-level fields in the agent's order, text verbatim, anything else as compact JSON. It is agent-authored, so the values are text only (never v-html). The component uses gray tokens only, and the list scrolls inside its own box. - It appears on every card that offers the decision: the open Operations card (above the collapsed context, never behind "Show details"), the /m ops card and the Workspace ask. - The Workspace projection (`WorkspaceAsk`) now includes `proposal`; `context` stays out. An ended ask keeps its proposal, so the person sees what was decided. - `QueueItemDetail.vue` has no importer and reads store state that no longer exists, so it is left alone. Mounted specs cover the three surfaces, the rule and the text-only rendering (12 tests), with two projection tests beside them. Each of 7 mutations of the wiring turns a test red. Checked by eye on a local instance in light and dark. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@AndriiPasternak31 @vybe, thanks both. Everything below is on the branch (head Before merge (Andrii's list)
Smaller
vybe's merge-train note
Andrii's questions (ent#164)
Evidence:
|
AndriiPasternak31
left a comment
There was a problem hiding this comment.
Approve. Thanks for the fast turn. I checked each item at a13880487:
- Prefix: fixed. An agent
gate-x→ 422reserved_request_id, and the agent can't reach the replay branch afterwards. A gate raise without the prefix →invalid_request_id, which is stricter than I asked and correct. The file poller now refusesgate-/GATE-entries too, and gate rows stay off both wakes and the prompt line.raised_byis on the receipt. proposal: rendered on the open Ops card,/mand the Workspace ask, as text, bounded. The mounted specs and the ratchets are green.[]for primary: now goes to Ops withresolved: false, andapprover[]→role_unassigned.- PG race:
ON CONFLICT→replayed, green on postgres in schema-parity.
One test fix before merge; see my separate comment.
Cheap now; otherwise I'll take them in #164's seam PR:
_receipt(ask_service.py:710) reportswakes_on_ending: truefor a gate row when the agent opted in, but_wake_filerskips it. Addand not is_platform_minted(row).- C6 crosses raisers. An agent's expired ask with the gate's exact proposal makes the gate's raise 422
reask_requires_linkpointing at the agent's row, and linking it badges the gate approval "Re-ask of" the agent's ask; the reverse link works too. Filteringlist_expired_proposals_for_agentand_predecessoron the caller'sraised_bycloses both.
Nits:
- A legacy file row named
gate-…still replays to the gate (raised_by: agent). The receipt makes it detectable, and the gate will assert on it, but a 409 on araised_bymismatch at:340would make it structural. - Pin
channel == "gate"iffraised_by == "gate". ResolvedCarddoesn't showproposal, while the Workspace keeps it on ended asks.
On #164: the four seam hooks (end policy, Actor.interactive, resolve_addressees, per-caller cap) all fit this shape without touching anything you changed. I'll re-stack on this head.
|
@webmixgamer one fix before merge:
|
…est, gate rows' receipts, replay and re-asks stay with their raiser, proposal on Resolved (Abilityai/trinity-enterprise#611, PR B) From the approving review on #3028: - `test_an_agent_may_not_author_a_gate_id` now has its own agent and ids. A sibling test creates `gate-call-abc` on the same agent, and the unit database keeps rows between tests. The test failed on 2 of the 3 nightly seeds and passed on the one seed PR CI uses. It now passes under 20 seeds, the nightly three included. - A gate row's receipt no longer promises a wake: `wakes_on_ending` is false on a platform-minted row, which `_wake_filer` skips. - `channel="gate"` if and only if `raised_by="gate"`; anything else is a ValueError. - A gate raise is never answered as a replay of a row it did not raise, at either replay site (409 `request_id_taken`). Example: a file row named `gate-…` written before the prefix was reserved. - The re-ask guard (C6) and the re-ask link compare with the SAME raiser's expired asks. An agent's expired ask no longer blocks, or gets linked by, a gate raise, and the reverse. `list_expired_proposals_for_agent` gains a `raised_by` filter. `_raiser_of` treats a row older than the column as the agent's own, and a platform alarm as nobody's. - The Operations Resolved card keeps the proposal too, so the record says what was decided (the Workspace already did). Tests came first: 9 went red before their fixes, and 2 more pin the legacy rows. Each of 12 mutations of the fixed lines, both replay call sites included, turns a test red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s globals (Abilityai/trinity-enterprise#611, PR B) The full unit island at nightly seed 67890 failed the staged-race test on PostgreSQL's sqlite leg with `created` where `replayed` was expected. The holds were installed with `monkeypatch.setattr(dbq, "make_insert", …)` after `import db.operator_queue as dbq`. In a full-suite run a sibling test can leave a stand-in for `db.operator_queue` that re-exports the real class. The patch lands on the stand-in, while the real method reads its own module dict, so the hold never fires. Reproduced with a plugin that installs such a stand-in: - the committed staged-race tests fail; - the concurrent-cap test passes even with the lock removed. All four holds now go through `create_native_item.__globals__` (test_1632's `_patch_engine`), and the table object comes from the same dict. With the stand-in installed, the fixed tests pass, and removing the lock turns the cap test red again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@AndriiPasternak31, thanks for the approval and for the seed repro. All of it is on the branch now (head The test fix
The items you'd have taken into #164 (
Each of 12 mutations of those lines turns a test red, both replay call sites included. Full unit island on the branch merged with |
|
merge-train: changed Non-blocking, for a follow-up: |
…n — reports and messages (Abilityai/trinity-enterprise#606) The ent#606 ruling (prds/permission-layers.md §5a): every outbound object an agent produces for a human carries `to:` — primary | approver | viewer | operator — and the platform resolves the person through the agent's assignments. #611 (#3028) built it for asks; this extends it to reports and messages through ONE shared rule. - services/role_addressing.resolve: a provider's people_for answer first, else the core defaults (primary → owner, operator → nobody, approver/viewer refused). No primary assigned → operator. Refusals are named (invalid_to, role_unassigned). ask_service._address now delegates to it; asks are unchanged (#3028's tests pass as they were). - Reports: ReportCreate.to. - operator, nobody, or the owner → operator-only (the owner reads the operator surface). - one other person → the checked audience; off the roster → 422 role_unreachable. - several people → 422 role_resolves_to_several. - to + audience_email → 422 addressing_conflict. - Neither → operator-only, unchanged. - Messages: exactly one of `to` (primary | approver | viewer) or recipient_email. A role nobody fills or several fill is a named 422; operator is not a message recipient (use an ask_operator alert). - MCP report / send_message take `to` and echo the role, never the resolved person. - Agent-supplied emails still work for two releases, logged as deprecated. The provider side (people_for over role assignments) ships separately in the private module. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in 58 commits from dev. Eight conflicts resolved:
- client_portal/asks/{service,router}.py — #3059's paged read (limit/cursor,
X-Total-Count, SQL-side roster set) combined with A0's fail-loud contract:
the roster check stays strict, and an unreadable queue or roster raises
AsksUnavailable -> 503 asks_unavailable instead of dev's empty page (which
the client would render as "nothing is waiting"). Mutation-checked: dev's
`return AsksPage()` turns 4 A0 tests red.
- PortalAsks.vue — the markdown body (sign-off) plus #3028's QueueProposal.
- PortalConversation.vue — useStickToBottom's `enabled` option (7c1c6dc)
plus the Inbox anchor's `detach`.
- Portal.vue — #3060's railSizedOpen, with the Inbox placeholder rail counted
as visible.
- api-endpoints.md / operating-room.md / tests/registry.json — both sides kept;
the asks row now states the 503.
Test updates the merge requires:
- test_ent610_asks_unavailable.py faults all three queue reads #3059 split
the read into (the first runs before any early return).
- test_3064_portal_unread_index.py builds agent_reports too: ent#610 D3's
unread count reads it, so the fresh track could not run the real query.
All 4 pass, including its negative control.
Verified: backend unit 19074 passed / 42 failed — 40 are the known
order-dependent set (ent477 x2, ent666, retention_floor; files identical to
dev, 119/119 in isolation) and the 2 test_3064 are fixed above; frontend 4090
passed + build; alembic single head; live stack: every sign-off behaviour
re-probed, a reply's quote confirmed in the agent's prompt.
…n — reports and messages (Abilityai/trinity-enterprise#606) (#3050) * feat(addressing): agents name a role, the platform resolves the person — reports and messages (Abilityai/trinity-enterprise#606) The ent#606 ruling (prds/permission-layers.md §5a): every outbound object an agent produces for a human carries `to:` — primary | approver | viewer | operator — and the platform resolves the person through the agent's assignments. #611 (#3028) built it for asks; this extends it to reports and messages through ONE shared rule. - services/role_addressing.resolve: a provider's people_for answer first, else the core defaults (primary → owner, operator → nobody, approver/viewer refused). No primary assigned → operator. Refusals are named (invalid_to, role_unassigned). ask_service._address now delegates to it; asks are unchanged (#3028's tests pass as they were). - Reports: ReportCreate.to. - operator, nobody, or the owner → operator-only (the owner reads the operator surface). - one other person → the checked audience; off the roster → 422 role_unreachable. - several people → 422 role_resolves_to_several. - to + audience_email → 422 addressing_conflict. - Neither → operator-only, unchanged. - Messages: exactly one of `to` (primary | approver | viewer) or recipient_email. A role nobody fills or several fill is a named 422; operator is not a message recipient (use an ask_operator alert). - MCP report / send_message take `to` and echo the role, never the resolved person. - Agent-supplied emails still work for two releases, logged as deprecated. The provider side (people_for over role assignments) ships separately in the private module. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(addressing): role-only delivery errors, symmetric deprecation log (Abilityai/trinity-enterprise#606) Review follow-ups on PR #3050: - A role-addressed message that fails authorization (403) or delivery (404) now returns a role-only detail (role_not_opted_in / role_no_channel); the service's text names the resolved email, so it passes through only for email-addressed sends. - The message-side email-addressing deprecation log fires only for agent keys, matching reports. - Drop the unreachable invalid_to mapping in ask_service._address; its only caller passes a role already validated against ASK_ROLES. - Cite trinity-enterprise#606 only, not a private design file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(reports): withhold the resolved person from a role-addressed agent's create response (ent#606) merge-train mechanical fix: POST /api/agents/{name}/reports returned addressed_to (the resolved person's email) to an agent-key caller. Route it through _hide_audience like the read routes, and assert the response body. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Eugene Vyborov <1073874+vybe@users.noreply.github.com>
Description
An agent now asks a person for a decision through one platform call instead of appending to
~/.trinity/operator-queue.json. The call is validated at once, stored, announced and answered with a receipt. This is PR B of the ent#611 ask object; PR A (#3023, merged todev) recorded how an ask ends.POST /api/agents/{name}/operator-queueand the MCP toolask_operatorare self-acting: the agent comes from the key, never from a parameter. Every other principal gets 403agent_identity_required. Ephemeral agents stay fenced and keep the file.ask_service.raise_askis the create entry of PR A's sink, and the seam the gate path (abilityai/trinity-enterprise#164) will call withraised_by="gate",channel="gate"and agate-id. In order:gate-included), and a gate raise must usegate-;request_idreturns the first receipt (status: replayed, HTTP 200, plusdiffers), before every time-dependent check;supersedes_expiredmust name the agent's own expired ask, and repeating an expiredproposalwithout it is 422reask_requires_link;people_for, where an empty answer means nobody fills the role (aprimaryask then goes to the operators); with no provider,primary→ the owner andoperator→ no person.approver/viewerare refused until someone fills them;pg_advisory_xact_lock, SQLiteBEGIN IMMEDIATE), and a file entry that races the insert comes back as a replay;raisedaudit row attributed to the agent, plus a thinoperator_queue_new.status,id,request_id,raised_by,channel,type,to_role,resolved,ask_status,disposition,disposed_at,expires_at,wakes_on_endingandsupersedes_expired, plusdifferson a replay. It names the role, never an email. The reads keep the same rule for machine keys.foreign, and the poller skips a file entry that re-uses a native id before any branch reads it. The write-backs and sweeps never touch a native row, and Clear All hides one once it is answered.create_operator_queue_item_with_outcome, and never counts, announces or audits a row it did not insert.ask_operator.ask_operator/get_my_askby their bare names, with the file as the fallback. The meta-prompt reference copy and the agent guide mirror it.proposal, read-only and expanded: the Operations card, the/mops card and the Workspace ask (theWorkspaceAskprojection now namesproposal;contextstays off).Design decisions beyond the approved plan
differsis computed in the sink, on the native path only (the plan's MCP-specific comparator). Deadlines are compared after the store's own normalisation, whatever offset the agent used.resolvedis true for an operator ask and for an ask resolved to a person. It is false when the owner has no email, in which case the ask goes to the operators.resolved_to, but none of them becomes the single Workspace addressee.supersedes_expiredis the predecessor'srequest_id; the column stores the predecessor's uuid.context/proposalare declared object-or-null in the MCP tool. fastmcp publishes every tool through xsschema'sstrictJsonSchema, which would publish a plainz.recordas an object that accepts no keys.nullreads as absent.Changed in review on this PR (2026-09-28, each with a failing test first)
d929c00f8):gate-joins_RESERVED_ID_PREFIXES. A gate raise must use it (422invalid_request_idotherwise), and an agent's raise may not (422reserved_request_id). So no agent can pre-create a gate id, andis_platform_mintedkeeps both wakes off gate rows. The receipt carriesraised_by; an unknown raiser or channel is aValueError.primarygoes to the operators withresolved: false(the ent#606 fallback), not to the owner.ON CONFLICT DO NOTHING, and the call returnsreplayed, not a 500. Pinned on both backends.get_my_asknames a re-ask's predecessor byrequest_id, like the receipt.OPERATOR_QUEUE_PROPOSAL_MAX_BYTESis wired into all three compose files and.env.example(a4e77c974).proposalis rendered on every card that offers the decision (a13880487):components/operator/QueueProposal.vuewith rows fromutils/operatorQueue.js::proposalRows, as text only.Second pass, after the approval (
06737d749,df1ea4cd1):test_an_agent_may_not_author_a_gate_idhas its own agent and ids. It was order-dependent and failed on 2 of the 3 nightly seeds. The file now passes under 20 seeds.create_native_item.__globals__. A sibling test's stand-in fordb.operator_queuehad been disabling them in full-suite runs, where the cap test passed even without the lock.wakes_on_endingis false on gate rows.channel == "gate"if and only ifraised_by == "gate".request_id_taken).raised_byfilter; a row older than the column is the agent's, a platform alarm is nobody's).Fixed during
/reviewand/cso --diff(each with a failing test first)raisedrow recorded the agent's raise as the owner acting in person, because the resolver ranks a user first. It now follows the ent#614 rule: noactor_user, and the key and the owner's email carried explicitly. The test asserts the real resolver's answer,("agent", <agent>, None).requires_postgresmodule pins the cap, concurrent retries, per-agent isolation and the outcome accessor, andschema-parityCI runs it on PostgreSQL.addressed_to_emailon a native row holds the person the platform resolved, and machine keys could read it back.GET /api/users/mealready returns the owner's email to the same key.resolved_to. This closes the path apeople_forprovider opens, once a role can resolve to someone other than the owner.context/proposalaccept no keys. They are now object-or-null, pinned overlistToolson the real transport.Security report:
docs/security-reports/cso-diff-2026-09-25-ent611-native-ask.md(no open findings).Related Issue
Refs abilityai/trinity-enterprise#611 (PR B of 2; PR A is #3023). The issue's gate-initiated criterion is delivered by abilityai/trinity-enterprise#164 through the
raise_ask(raised_by="gate")seam.Journey Impact
Journey Impact: extends: J05
Type of Change
Testing
Backend.
tests/unit/test_ent611_native_ask.py(83 tests, on real SQLite wherever a writer is involved):raise_askrefusals, addressing, replay and re-asks;tests/unit/test_ent611_native_ask_pg.py(4 tests per backend)._with_outcomedelegates to the existing create mock, so their "never re-ingested" assertions still bite. The suites are bug: operator-queue create path has no rate limit or size caps — flooding/operator-fatigue surface (blocking pull default-ON) #1632, bug: operator-queue create_item KeyError on missing created_at causes an infinite sync-retry loop #1525, bug: operator_queue.id is a global primary key — cross-agent id collision silently swallows requests #1631, bug(operator-queue): a pending approval can sit unseen for days — the human's card goes stale while the container↔platform sync stays silent #2915 and PR A's.df1ea4cd1merged withdev(ebc2bfb4f), run the way CI runs it (-n 4 --dist loadfile), at the nightly seeds: 0 failed at 67890 (18,627 passed) and 0 failed at 99999 (18,628 passed). A cleandevat seed 67890: 0 failed / 18,505 passed. The difference is this PR's tests; the PostgreSQL module skips without a database.MCP.
operator_queue.test.tsandaccess-wiring.test.ts(31 and 15 tests) cover:The whole suite is 567 passed, and
tscis clean.Frontend.
operatorQueueReask.spec.js(mounted, 9 tests), andoperatorQueueProposal.spec.js,portalAskProposal.spec.jsandmobileAdminProposal.spec.js(mounted, 12 tests). The whole unit suite is 3,684 passed. Two unrelated specs cannot load on the host, because@vue/compiler-sfcis not installed there; CI has it. The raw-colour, loading-gate and source-text ratchets are green with no baseline edits.Mutations (each red, and each file restored byte-identically):
insertedignored;differs.gate-reservation, the gate branch, and the call site that passesraised_by;[]fallback, the receipt field, the rate order and the raiser/channel check;proposalRowsandv-html;proposaland forwardingcontext.Live walk-through
Done on a local instance before this PR opened:
ask_operatorover the real MCP server with an agent's own key: an approval toprimarywas created; an alert went to the operators; a deadline under the floor was refusedinvalid_expires_at.timeout.reask_requires_link; with the link it was created; an identical retry replayed withdiffers: [].ask_operatorwas refused before any backend call.Merge notes
dev. feat(operator-queue): ask endings — one sink, endings ledger, person-only endings, wake on any ending, self-readback (abilityai/trinity-enterprise#611, PR A of 2) #3023 squash-merged, and this PR edits lines feat(operator-queue): ask endings — one sink, endings ledger, person-only endings, wake on any ending, self-readback (abilityai/trinity-enterprise#611, PR A of 2) #3023 introduced, so mergingdevin would have conflicted in about 20 files. The rebuild is one commit,831231102: this PR's net delta against feat(operator-queue): ask endings — one sink, endings ledger, person-only endings, wake on any ending, self-readback (abilityai/trinity-enterprise#611, PR A of 2) #3023's final headdfcb7c07b, replayed ontodevwith no conflicts. It has the same 47 files and the same added and removed lines. The earlier commits (19b2e53ee..3a2dffdf1) are in the force-push history./validate-pr, fixed ina4e77c974: the code readOPERATOR_QUEUE_PROPOSAL_MAX_BYTES(default 8192), but no compose file passed it into the backend, so.envcould not change it on a deploy. It is now in all three compose files and.env.example, right afterOPERATOR_QUEUE_OPTIONS_MAX_BYTESlike its 14 siblings, and the deploy guide and §26.10 name it. Every CI compose file set renders, and the 35 unit files that read the compose files or.env.examplepass, feat: prebuilt images + pull-only hosted compose + unattended cloud-init first boot — the gate for every one-click / marketplace channel #2280 hosted parity included.status-in-dev, and set it by hand if the promotion workflow did not.emit_event.payload,deploy_local_agent.files) as objects that accept no keys.Checklist
security.md§26.10 OPS-001-RAISE (§26.1, §26.6 and §26.9 point to it);api-endpoints.md,mcp-server.md,background-services.md,backend.md,database.md,frontend.md,workspace.md;🤖 Generated with Claude Code