Repository navigation
plat 648
| Field | Value |
|---|---|
| State | fixed on main |
| Priority | P2 |
| Product | code |
| Area | chat |
| Summary | A Code chat can list the other chats (tabs) of its Code and ask one; the ask is a function call (call_id, saved record, structured result) that runs there as a visible turn, and the result comes back as the standard call auto-notification |
Owner, 2026-10-07: "we have multiple tabs in code.. but right now tabs cannot talk to each.. we should enable like we have communication across products.. same for across tabs inside a code". Side chats are PLAT-571.
- Two tools in every Code chat (owner turns, not read-only):
list_project_chats(main chat + side chats with id, name, working now, which is this chat) andmessage_project_chat(chat, message, reply=true)(agent_go/cmd/server/code_chat_messages.go, registered inregisterAgentProfileTools). - Delivery reuses the cross-product path of an owner's MCP
ask_crewinto their own chat (crew_own_chat_ask.go):productBotTurnRequest+startSessionInternalWithResult, so the message runs as a turn in the target chat, queued behind its current turn (durable turn queue), visible in its tab, attributed "[Message from "Chat 2" ...]" withtriggered_by=code_chat(interactive origin: no CLI relaunch, never steered into a running turn). - The reply comes back like a
call_functionresult: a background execution in the sending chat that resumes it with an [AUTO-NOTIFICATION] holding the target's final reply.reply=falseis a hand-off with no answer. - Who and where come only from the trusted turn: the turn's user, its session and its verified workspace. The
sending chat must be a registry entry of that Code; targets are read from the same user's conversation registry
(
projectChats: the bare project key and<project>:chat:<id>only, never trigger/schedule/Slack chats, never another Code). A Code is owner-only, so no other person's chat is reachable. - Loops: a message chain cannot return to a chat already in it (the reply goes back on its own), is at most 3 chats long, and one Code sends at most 20 messages per hour between its chats.
- A new side chat now also saves its tab name ("Chat 2") on the server, so the other chats can address it by name.
- Code's system prompt says the tools exist and when to use them.
Owner: messaging between chats must be a proper call, like the cross-product calls. The first version (above) had no call record: the reply was the target's last text, held in memory, with its own loop guard and rate limit.
-
Target kind. A sibling chat is a
triggerTargetof the caller's own Code withChatset (and the caller carries its ownChat), both built only from the trusted turn bycodeChatsFor.startCrewFunctionCallrecords it like any call and runs it withrunCodeChatCall(code_chat_messages.go): a turn in that chat's existing conversation (productBotTurnRequest+startSessionInternalWithResult,triggered_by=code_chat, label "From "), queued behind its current turn and visible in its tab. No trigger binding is created. -
Tool surface.
list_project_chatsstays.message_project_chatis replaced byask_project_chat(chat, message, reply=true, submission_id, timeout_minutes), shaped like the other asks (ask_crew,call_functionwithask): it returns the call snapshot (call_id,status) at once, and the result arrives as the standard function-call [AUTO-NOTIFICATION] (startCrewFunctionWatch).get_function_call(call_id)gives status, progress, pending inputs and the target chat's recent activity;reply_function_callanswers a question the target chat asks.ask_function_updateis refused for chats (they take no mid-turn messages). -
reply=false is
notify=falseofcall_function: the call is still created and recorded, the target is told it is a hand-off, and no notification is started; the sender can still read it withget_function_call. -
Callee contract. The target chat gets the standard
[Function call fn-...]task withreport_function_progress/return_function_result(call_id, result={"answer": ...}). The result is what it returns there; only if its turn ends without one is its final reply used as the answer (as for any free-text ask), so a human-visible chat never gets an extra "result missing" retry turn. -
Call features.
fn-<uuid>call_id, saved record + call index (chat_history/code-peer-calls, survives a restart as "interrupted"), lookup by call_id, progress, structured result or error, timeout from the target chat's last activity (default 60 min, late answers still delivered, hard cap 4x),submission_ididempotency (scoped to the sending chat) and joining of identical in-flight asks (same sender chat, target chat and arguments). -
Only those two chats. For a chat call,
get_function_call/reply_function_callaccept only the sending chat's session andreport_function_progress/return_function_resultonly the target chat's session; the Code-peer authorization is replaced by "same Code, same folder" for these calls. -
Loops: the shared chain guards. Chain keys are scoped per chat (
<code key>#chat:<conversation key>), so the chats of one Code are separate participants: no revisits (a chat, or a chat's Code, already in the chain),crewFunctionMaxDepth(4) andcrewFunctionChainBudget(20 calls per chain). A Code's plaincall_functionjoins the chain of a call into any of its chats. Removed: PLAT-648'scodeChatRelaychain tracking, the 3-chat hop limit and the 20-messages-per-hour rate limit. - Security rules unchanged: sender and project only from the trusted turn; targets only the same user's main chat and
side chats of the same Code (bare project key or
<project>:chat:<id>), never trigger, schedule or Slack chats, other Codes or other users; a non-chat caller cannot target a chat. No tokens are involved.
Not covered by the shared guards (as for Crews): a chat that, in each turn resumed by an answer, asks again starts a
fresh chain each time. Owner, 2026-10-07: cap it at 20 asks an hour between one Code's chats
(codeChatAsksPerHour, counted per call id so a resubmitted or joined ask counts once; in memory, so it resets on
restart). The 21st ask is refused with a message telling the agent to stop and tell the person.
Verification: TestCodeChatAskIsAFunctionCall (replaces TestCodeChatSiblingsAndLoopGuard): sibling listing,
refusal of self, of a chat outside the Code and of a non-chat caller; an ask gets an fn- call_id and a saved index,
the callee sees the call instructions, the sender cannot answer its own call, an ask back to the sender is refused by
the shared chain guard, progress and return_function_result settle it, the sender reads it with
get_function_call, and the same submission_id returns the same call. Related server tests pass; the failures in
the package also fail on origin/main. Not verified live (no isolated server was started: its startup updates the
machine's global coding CLIs); delivery into the chat, queueing and the notification were checked by reading the code
they reuse.
-
TestCodeChatSiblingsAndLoopGuard: sibling listing excludes isolated automation chats and other Codes; echo, hop limit and rate limit are refused. -
go build,go vet,tsc -b, Work frontend tests pass. Unrelated failures already on main:TestPrivateCodeCallerIsSeparateFromCrewWithSameProjectID,TestCodePreparedSystemPrompt,internal/codeproductTestCodeSkillOptionsStayPrivateAndRefreshWithoutMutatingBuiltins. - Not verified live: no server was started (a test server's startup updates the machine's global coding CLIs). Delivery, queueing and the reply notification were checked by reading the code they reuse.
- Live check: open a side chat, have the main chat
ask_project_chatit, see the[Function call ...]turn in the side tab and the result back in the main chat; ask a busy chat and see it queue;reply=falsehand-off. - A closed side chat stays in the registry and is still listed and reachable; a message reopens its conversation (visible from history). If the owner wants closed tabs excluded, closing must mark the slot on the server.
- Side chats opened before this change are listed as "Chat " until renamed.
Auto-synced from docs/ on main. Edit there, not here.