Skip to content

Commit 6ecfbf9

Browse files
committed
feat(chat): attribute replies header-first by dispatch Message-ID (CL-7104)
1 parent c1710c3 commit 6ecfbf9

1 file changed

Lines changed: 57 additions & 1 deletion

File tree

‎packages/chat/src/chat-orchestrator.ts‎

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,14 @@ export type ChatOrchestrator = {
221221
* concern and is filtered out here. A missing `correlationId` means the
222222
* register co-write never ran (or hasn't landed yet) — nothing to look an
223223
* approval up by, so this is treated the same as "not an approval gate".
224+
*
225+
* Upstream ask (CL-7104 remainder, filed — do NOT implement cross-repo
226+
* here): resolving the approval by the dispatched question card's own
227+
* `Message-ID` — the answer dispatched as a reply naming it in
228+
* `In-Reply-To`, per `docs/CHAT.md` — needs the reactor's `gate.blocked`
229+
* event and the hub IPC register co-write to carry that id. That change
230+
* lives in `vendor/intx` and the hub, outside this package; until it
231+
* lands, this `correlationId` lookup stays.
224232
*/
225233
function gateBlockedCorrelationId(event: unknown): string | undefined {
226234
if (
@@ -693,6 +701,39 @@ function correlateOriginatingWorkbench(
693701
return undefined;
694702
}
695703

704+
/**
705+
* Header-first attribution (CL-7104): the dispatch mail's `Message-ID` IS
706+
* the source row's (`dispatchTurn` stamps `mailMessageIdFor` of the message
707+
* it answers), so the open bracket's mail names the row the reply answers
708+
* — and that row's own `workbenchId` is the originating room. Preferred
709+
* over the event hints and the single-running-turn fallback in `postReply`:
710+
* two turns pending against one agent are told apart because their
711+
* dispatches carry different `Message-ID`s, not because one is newer.
712+
*
713+
* Only ever narrows to a membership: a correlation naming another
714+
* workbench (or none at all — a pre-rollout mail) returns undefined and
715+
* falls through to the existing fallbacks rather than posting somewhere
716+
* the agent was never invited.
717+
*/
718+
async function correlateWorkbenchByMail(
719+
turnMailCorrelation: TurnMailCorrelationStore | undefined,
720+
tenantId: string,
721+
members: readonly ReplyMember[],
722+
mailId: string | undefined,
723+
): Promise<string | undefined> {
724+
if (turnMailCorrelation === undefined || mailId === undefined) {
725+
return undefined;
726+
}
727+
const source = await turnMailCorrelation.findTurnMailSource({
728+
tenantId,
729+
mailId,
730+
});
731+
if (source === undefined) return undefined;
732+
return members.some((member) => member.workbenchId === source.workbenchId)
733+
? source.workbenchId
734+
: undefined;
735+
}
736+
696737
async function latestTurnForAgent(
697738
agentTurns: Pick<AgentTurnStore, "listTurns"> | undefined,
698739
tenantId: string,
@@ -766,7 +807,22 @@ async function postReply(
766807
const runningIds = members
767808
.filter((member) => member.turnId !== undefined)
768809
.map((member) => member.workbenchId);
769-
const correlated = correlateOriginatingWorkbench(event, members);
810+
// Attribution order is header, hints, then the running-turn fallbacks
811+
// (CL-7104): the bracket mail's correlation names the row the reply
812+
// answers, so it wins over every heuristic below it. The
813+
// single-running-turn leg after it is deliberately retained until agent
814+
// replies arrive as mail — the sidecar `agent.event` stream carries no
815+
// threading headers today, so a header-less reply with exactly one
816+
// running turn has nowhere else to go. Retiring this bridge is CL-6534
817+
// slice 2's job, NOT this ticket: never extend the fallback to the
818+
// ambiguous (several-running) case, which stays a reported drop.
819+
const byMail = await correlateWorkbenchByMail(
820+
deps.turnMailCorrelation,
821+
resolved.tenantId,
822+
members,
823+
mailId,
824+
);
825+
const correlated = byMail ?? correlateOriginatingWorkbench(event, members);
770826
const resolvedTargets =
771827
correlated !== undefined
772828
? [correlated]

0 commit comments

Comments
 (0)