feat(pull): /chat on the durable queue for pull pilots (#3127) - #3145
obasilakis wants to merge 6 commits into
Conversation
4bf1eab to
25d2096
Compare
7b3238a to
fb70e5d
Compare
|
✅ Alembic head check clear — merging this PR into Previously flagged; resolved. Advisory — this check does not block merge. · head_sha: |
|
✅ Nightly unit-suite clean when this PR is merged into |
|
Resolve by merging |
|
merge-train (2026-10-04): not on this train. It rides the next one once fixed. This PR was not validated today because the diff will change with the rework below. The branch is 45 commits behind
I've set |
On a pull-pilot agent, POST /api/agents/{name}/chat (MCP chat_with_agent,
the connector tool and the trinity CLI) is admitted onto the durable
queue and claimed by a worker. Every interactive trigger is now
pull-owned on a pilot.
Memory on a pilot is one Claude conversation per chat_sessions row,
i.e. per (agent, user), resumed by id through run_resumable_turn
(ResumeLock, persist_session, cold retry on resume-not-found). The id is
cached on chat_sessions.cached_claude_session_id (SQLite migration +
Alembic 0084) and joins the session reaper's keep set.
The post-turn work /chat did inline runs in the caller after the
terminal: assistant message, collaboration close, response shape,
idempotency complete, timeout receipt. GET /chat/history serves the
caller's session from the database on a pilot; DELETE /chat/history
also clears the cached ids. execute_task carries chain_depth so a
cold-retry row keeps its depth.
Non-pilot agents are unchanged.
Fixes #3127
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DELETE /chat/history forgot the cached Claude ids but left the sessions active, so GET /chat/history still showed the old conversation. The sessions that carry a cached id are now closed with it; sessions /chat never used stay open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 0085 (#3127) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 0088 (#3127) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fb70e5d to
f6c0924
Compare
|
merge-train: not on today's train. It rides the next one once fixed. Merged with current The new
Adding it to Reproduce: merge |
- capacity_manager: take dev's earlier pull_exclusive placement (#2514); keep the #3127 comment that /chat is pull-owned on pilots. - Rechain the chat_sessions Alembic revision as 0090_chat_session_claude_id off dev's head 0089_supersede_queue_flood_backlog; SQLite entry ordered after supersede_queue_flood_backlog. - run_pulled_chat_turn passes request_text=request.message to the resumable turn. /chat runs no admission-seam skill gate, so the executor backstop must gate on the caller's own words (ent#751). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…3247) CP1 of the replace-a-pending-ask slice. `operator_queue` gains two nullable TEXT link columns: `replaces` on the successor (the predecessor row's uuid) and `replaced_by` on the predecessor (the successor row's uuid). Both are stamped in the one per-agent locked transaction the next checkpoint adds, so each row is self-describing on every surface that holds only one of the pair. Both schema tracks (Invariant #9): SQLite entry `operator_queue_replace` and Alembic `0090_operator_queue_replace` chained after this branch's head `0089_supersede_queue_flood_backlog` — `0090` is also taken by the independent #3246 branch and open PR #3145, so expect a renumber at merge; `check_alembic_heads.py` names the fork. `schema.py` and `tables.py` DDL updated (the `disposed_by` comment gains `agent`); `_row_to_item` / `_SELECT_COLS` carry the columns; `_MACHINE_ROW_FIELDS` and the ent#715 `MACHINE_ROW_KEYS` pin are extended in the same commit. No index. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
AndriiPasternak31
left a comment
There was a problem hiding this comment.
Thanks for this. The pull routing is clean: I couldn't find a double-execution path (run_chat_turn branches only on capacity is None, and execute_task re-decides with the same env-only predicate). The only caller-side terminal write is on a never-enqueued RUNNING row, so it can't race the token-gated sink, and the slot and idempotency accounting balance. Tier 1 is green and check_alembic_heads passes. I ran test_3127 + the flipped pins (164 passed) and the 1804/1578/2806/2842/2889/ent751/3114 guards (297 passed).
Two things I'd like fixed before merge. Both are invisible to the suite because dispatch_and_await_terminal is mocked in every turn test:
1. Skill gate runs twice on a pilot /chat (ent#751). The comment at chat_execution_service.py:1011 says "/chat runs no admission-seam gate", but admit_chat_request calls skill_gate_service.enforce at dispatch_admission_service.py:326, before the pilot branch at :387. Without gate_checked=True, execute_task's backstop gates again with a requester rebuilt from the row (requester_for_dispatch), and that requester never has is_person=True. I checked this with the real enforce: the admission requester self-approves the owner, and the backstop requester does not. So on a pilot, an approver's own gated request turns into a pending approval: the row is SKIPPED, a second ask is raised, the caller gets a 202, and the user message has no reply. The pilot branch also returns before audit_self_approved (:431). Fix: pass gate_checked=True (keep request_text), call audit_self_approved in the pilot branch, and assert gate_checked in test_pilot_turn_dispatches_through_the_queue. This is the first branch of vybe's 10-05 note.
2. Agent-to-agent /chat (trigger agent) gets no claim priority and no claim budget. "agent" is not in INTERACTIVE_TRIGGERS, so _CLAIM_WAITING_TRIGGERS (task_execution_service.py:720/811) skips the phase-1 wait. The turn queues behind batch work, and the only bound is timeout + 120s from enqueue. With a deep queue that ends in the 504 queued_timeout receipt while the row is still queued. It then runs for nobody: no assistant chat_messages row and no cached Claude id. The collaboration activity also stays started until the 120-min backstop, because build_pull_queue_payload sends collaboration_activity_id=None. That's the #1804 symptom. The a2a note in pull_pilot.py states the rule: a blocked caller earns priority and the budget. Could the pulled /chat opt into claim semantics explicitly (rather than by trigger), and could the collaboration activity id ride the payload so the sink closes it? A test with the real adapter for trigger agent would pin it.
Smaller items, non-blocking:
- Post-turn work runs in the caller, so it's lost on a backend restart or a 504. Issue scope item 3 said "moves to the terminal", and #3227 puts post-turn delivery in the sink. No textual conflict between the two PRs, but worth converging once #3227 lands (carry
chat_session_idandcollaboration_activity_idin the metadata). - Same-user concurrency: the ResumeLock waits 30s and then the call gets a 429 and the row is FAILED. Agent keys resolve to the owner, so every agent of one owner shares one session with the target, and parallel
chat_with_agentcalls fail where push serialized them. Consider a pilot lock wait of at least one turn, as rooms do. cached_uuidis read before the lock (:1005), so two concurrent first turns both run cold and the second overwrites the first's id.- Lock-busy path: the
update_execution_statusbool is ignored, noagent.task.failedis emitted (MCP's receipt goes out at 25s and this FAILED lands at 30s), and the 429 has noX-Trinity-Error-Code: capacity. - Backlog full on a pilot
/chatmaps to 503 with no code, because "Agent backlog full…" doesn't match "at capacity". Push answered 429/capacity. - The Chat tab writes into the same
chat_sessionsrow, so "New chat" in the UI resets MCP/CLI memory, and the owner reset closes Chat-tab sessions. - Docs: the
chat_with_agentdescription inmcp-server/src/tools/chat.ts:395-399still says the session is shared by every caller and restarts on a model change.persistent-chat-tracking.md,mcp-orchestration.mdandagent-to-agent-collaboration.mdaren't updated. The PR body and the CSO report still say Alembic0086and "stacked on #3124". - Alembic: #3255 and #3256 also add
0090_*off0089_supersede_queue_flood_backlog. The tables are disjoint, so this is a mechanical re-parent for whoever lands second.
Stacked on #3124 (base:
feature/3114-pull-route-interactive). Review the top 3 commits; after #3124 merges this PR is retargeted todev.Summary
The last push path on a pull-pilot agent.
POST /api/agents/{name}/chatis called by MCPchat_with_agent, the agent-to-agent connector tool, and thetrinity chatCLI. On a pilot it is now admitted onto the durable queue and claimed by a worker, so every interactive trigger is pull-owned on a pilot. The web UI chat panel already went throughPOST /task, which #3124 routes.session_turn_service.run_resumable_turnwith keysession:chat:<chat_sessions.id>, resumingchat_sessions.cached_claude_session_id. That gives the ResumeLock,persist_session, and a cold retry on resume-not-found. Off a pilot/chatkeeps the agent container's single shared session./chatdid inline runs in the caller after the terminal: assistantchat_messagesrow (secrets scrubbed), activity closes, the same response shape, idempotency complete, a timeout receipt + 504, andResumeLockBusy→ FAILED + 429.GET /chat/historyon a pilot serves the caller's session from the database.DELETE /chat/historycloses the sessions/chatused and forgets their ids.chat_sessions.cached_claude_session_id(SQLite migration + Alembic0086), which joins the session reaper's keep set.execute_task(chain_depth=)keeps the depth on a cold-retry row./chathandler is not cancelled when the client goes away. The MCP 25s abort therefore leaves the queued turn running and its receipt valid.Live verification (isolated sibling stack, real Claude turns, 3-worker pilot + non-pilot)
/chaton the pilot: 3 turns from one user were pulled on one Claude session and recalled earlier numbers ("17, 29"). A second user got "NONE" on their own key and session. History returned each caller's own messages. A reset made the next turn start cold, and history is empty after it./chatwas pushed with memory kept, as before.a04d6e8ce) and here (883656752).Known gaps
/chatturn gets the task-mode platform prompt, where push gives it the chat mode.output_tokensis not recorded on pulled/chatmessages.GET /chat/sessionstill reads the agent's shared session.Test Plan
cd tests && pytest unit/test_3127_pull_chat.py -v(21 tests; 11 key-behaviour mutations + the reset mutation go red)test_1766,test_2391,test_3114_pull_route_interactive,test_2610check_alembic_heads: one head (0086)/cso --diff: 0 findingsFixes #3127
🤖 Generated with Claude Code