Skip to content

Commit f5e8580

Browse files
committed
fix(dashboard-agent): fail a watch investigation whose card refuses to close
Only a deleted chat is swallowed now. A refused close left the card in_progress while the action reported success, so the panel span forever with nothing to retry it.
1 parent b71a752 commit f5e8580

2 files changed

Lines changed: 72 additions & 7 deletions

File tree

internal-packages/dashboard-agent/src/watch-actions.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -921,6 +921,68 @@ describe("watch investigation", () => {
921921
).toEqual([]);
922922
});
923923

924+
/** A store whose atomic close refuses rather than throws, with the reason it refuses for. */
925+
function refusingStore(error: "not_found" | "context_mismatch" | "chat_missing") {
926+
const { store, calls } = fakeStore({
927+
openInvestigation: { id: "inv_seeded", projectRef: "proj_abc", environmentRef: "env_abc" },
928+
});
929+
const wrapped: DashboardAgentStore = {
930+
...store,
931+
settleInvestigationCard: async (args) => {
932+
calls.settleInvestigationCard.push(args);
933+
return { ok: false as const, error };
934+
},
935+
};
936+
return { store: wrapped, calls };
937+
}
938+
939+
async function investigateAgainst(store: DashboardAgentStore, chatId: string) {
940+
const { model } = recordingModel([
941+
renderStep(inProgress, "inv_seeded", "tc_open"),
942+
textStep("still looking"),
943+
]);
944+
harness = mockChatAgent(dashboardAgent, {
945+
chatId,
946+
clientData: CLIENT_DATA_WITH_TOKEN,
947+
setupLocals: ({ set }) => {
948+
set(dashboardAgentStoreKey, store);
949+
set(dashboardAgentModelKey, model);
950+
},
951+
});
952+
return harness.sendAction(INVESTIGATE);
953+
}
954+
955+
function erroredWith(turn: { chunks: unknown[] }, pattern: RegExp) {
956+
return turn.chunks.some(
957+
(chunk) =>
958+
(chunk as { type?: string }).type === "error" &&
959+
pattern.test((chunk as { errorText?: string }).errorText ?? "")
960+
);
961+
}
962+
963+
/**
964+
* A refused close is the same failure as a thrown one: the card never landed, so the
965+
* panel spins until the action is retried, and only a thrown error gets it retried.
966+
*/
967+
it.each(["not_found", "context_mismatch"] as const)(
968+
"fails the action when the close is refused with %s",
969+
async (error) => {
970+
const { store, calls } = refusingStore(error);
971+
const turn = await investigateAgainst(store, `chat_investigate_refused_${error}`);
972+
973+
expect(calls.settleInvestigationCard).toHaveLength(1);
974+
expect(erroredWith(turn, new RegExp(error))).toBe(true);
975+
}
976+
);
977+
978+
it("reports success when the close is refused because the chat is gone", async () => {
979+
const { store, calls } = refusingStore("chat_missing");
980+
const turn = await investigateAgainst(store, "chat_investigate_refused_chat_missing");
981+
982+
expect(calls.settleInvestigationCard).toHaveLength(1);
983+
expect(erroredWith(turn, /chat_missing|couldn't close/)).toBe(false);
984+
});
985+
924986
it("says nothing when the kick carries no tenancy to scope a card with", async () => {
925987
const { store, calls } = fakeStore();
926988
harness = mockChatAgent(dashboardAgent, {

internal-packages/dashboard-agent/src/watch-actions.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -602,8 +602,8 @@ async function resolveInvestigationId(args: {
602602
* Close the card this lane opened: its terminal revision and the closing card in one
603603
* transaction, so a terminal row whose card never landed cannot exist.
604604
*
605-
* Nothing is caught. The row settles only if the card lands, and the failure has to
606-
* reach the action for the task's retry to be a real retry.
605+
* Only a deleted chat is swallowed. The row settles only if the card lands, and every other
606+
* failure has to reach the action for the task's retry to be a real retry.
607607
*/
608608
async function closeCardInTranscript(args: {
609609
store: DashboardAgentStore;
@@ -633,13 +633,16 @@ async function closeCardInTranscript(args: {
633633
messageId: args.messageId,
634634
});
635635
if (!result.ok) {
636-
// A chat deleted mid-investigation is a race, not a fault: nothing settled, and
637-
// there is no transcript left to close the card in.
638636
const message = "dashboard-agent watch investigation couldn't close its card";
639637
const details = { chatId, investigationId, error: result.error };
640-
if (result.error === "chat_missing") logger.warn(message, details);
641-
else logger.error(message, details);
642-
return;
638+
// A chat deleted mid-investigation is a race, not a fault: nothing settled, and there
639+
// is no transcript left to close the card in.
640+
if (result.error === "chat_missing") {
641+
logger.warn(message, details);
642+
return;
643+
}
644+
logger.error(message, details);
645+
throw new Error(`${message}: ${result.error}`);
643646
}
644647

645648
chat.history.set([...uiMessages, result.card as UIMessage]);

0 commit comments

Comments
 (0)