From 7adb8336091d6c5c610a3fdbaed2ae42e50af58f Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 20 May 2026 12:28:40 +0900 Subject: [PATCH 1/6] test(team-mode): add red tests for ambiguous delivery loss (BUG-A + BUG-B) Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../team-mode/tools/messaging.test.ts | 77 +++++++++++++++++-- 1 file changed, 70 insertions(+), 7 deletions(-) diff --git a/src/features/team-mode/tools/messaging.test.ts b/src/features/team-mode/tools/messaging.test.ts index e733ef3ee..e9e3feefd 100644 --- a/src/features/team-mode/tools/messaging.test.ts +++ b/src/features/team-mode/tools/messaging.test.ts @@ -637,7 +637,7 @@ describe("createTeamSendMessageTool", () => { expect(inboxEntries).toHaveLength(1) }) - test("#given live delivery promptAsync fails after dispatch may have been accepted #when delivery falls back #then it keeps the delivered message reserved instead of surfacing a duplicate unread", async () => { + test("#given live delivery promptAsync fails ambiguously #when delivery falls back #then it releases the message for mailbox injection", async () => { // given const fixture = await createTeamFixture() let promptCalls = 0 @@ -661,20 +661,51 @@ describe("createTeamSendMessageTool", () => { // then expect(promptCalls).toBe(1) const unread = await listUnreadMessages(fixture.teamRunId, "m2", fixture.config) - expect(unread).toHaveLength(0) + expect(unread).toHaveLength(1) const inboxDir = getInboxDir(resolveBaseDir(fixture.config), fixture.teamRunId, "m2") const inboxEntries = (await readdir(inboxDir)).filter((entry) => entry.endsWith(".json")) expect(inboxEntries).toHaveLength(1) - expect(inboxEntries[0]?.startsWith(".delivering-")).toBe(true) + expect(inboxEntries[0]?.startsWith(".delivering-")).toBe(false) const { loadRuntimeState: loadState } = await import("../team-state-store/store") const runtimeState = await loadState(fixture.teamRunId, fixture.config) const recipient = runtimeState.members.find((member) => member.name === "m2") - expect(recipient?.pendingInjectedMessageIds).toHaveLength(1) + expect(recipient?.pendingInjectedMessageIds).toHaveLength(0) }) - test("#given live delivery prompt dispatches but pending mark fails #when delivery finishes #then the message is not re-exposed as unread", async () => { + test("#given dispatchInternalPrompt fails ambiguously #when deliverLive handles the failure #then the message is released back to inbox as unread AND pendingInjectedMessageIds is NOT updated", async () => { + // given + const fixture = await createTeamFixture() + const failingClient = { + session: { + promptAsync: async () => { throw new Error("JSON Parse error: Unexpected EOF") }, + }, + } satisfies LiveDeliveryClient + const liveTool = createTeamSendMessageTool(fixture.config, failingClient) + + // when + await liveTool.execute({ + teamRunId: fixture.teamRunId, + to: "m2", + body: "ambiguous failure should retry", + }, fixture.toolContext(fixture.memberOneSessionId)) + + // then + const unread = await listUnreadMessages(fixture.teamRunId, "m2", fixture.config) + expect(unread).toHaveLength(1) + + const inboxDir = getInboxDir(resolveBaseDir(fixture.config), fixture.teamRunId, "m2") + const inboxEntries = await readdir(inboxDir) + expect(inboxEntries.filter((entry) => entry.startsWith(".delivering-"))).toHaveLength(0) + + const { loadRuntimeState: loadState } = await import("../team-state-store/store") + const runtimeState = await loadState(fixture.teamRunId, fixture.config) + const recipient = runtimeState.members.find((member) => member.name === "m2") + expect(recipient?.pendingInjectedMessageIds).toHaveLength(0) + }) + + test("#given live delivery prompt dispatches but pending mark fails #when delivery finishes #then the reservation is committed to processed", async () => { // given const fixture = await createTeamFixture() let promptCalls = 0 @@ -702,8 +733,40 @@ describe("createTeamSendMessageTool", () => { const inboxDir = getInboxDir(resolveBaseDir(fixture.config), fixture.teamRunId, "m2") const inboxEntries = (await readdir(inboxDir)).filter((entry) => entry.endsWith(".json")) - expect(inboxEntries).toHaveLength(1) - expect(inboxEntries[0]?.startsWith(".delivering-")).toBe(true) + expect(inboxEntries.filter((entry) => entry.startsWith(".delivering-"))).toHaveLength(0) + const processedEntries = (await readdir(path.join(inboxDir, "processed"))).filter((entry) => entry.endsWith(".json")) + expect(processedEntries).toHaveLength(1) + }) + + test("#given dispatchInternalPrompt succeeds #when markLiveDeliveryPending fails #then the reservation is committed to processed/", async () => { + // given + const fixture = await createTeamFixture() + const client = { + session: { + promptAsync: async () => { + await rm(path.join(resolveBaseDir(fixture.config), "runtime", fixture.teamRunId, "state.json")) + }, + }, + } satisfies LiveDeliveryClient + const liveTool = createTeamSendMessageTool(fixture.config, client) + + // when + await liveTool.execute({ + teamRunId: fixture.teamRunId, + to: "m2", + body: "accepted prompt should be processed", + }, fixture.toolContext(fixture.memberOneSessionId)) + + // then + const inboxDir = getInboxDir(resolveBaseDir(fixture.config), fixture.teamRunId, "m2") + const inboxEntries = await readdir(inboxDir) + expect(inboxEntries.filter((entry) => entry.startsWith(".delivering-"))).toHaveLength(0) + + const processedEntries = (await readdir(path.join(inboxDir, "processed"))).filter((entry) => entry.endsWith(".json")) + expect(processedEntries).toHaveLength(1) + + const unread = await listUnreadMessages(fixture.teamRunId, "m2", fixture.config) + expect(unread).toHaveLength(0) }) test("#given live delivery cannot reload runtime after pre-reserve #when delivery aborts #then the message is released for mailbox injection", async () => { From 655dffbc2a12cc41dfd149f7fc7799362c35f228 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 20 May 2026 12:30:02 +0900 Subject: [PATCH 2/6] fix(team-mode): release reservation on ambiguous failure, commit on success-path mark failure Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/features/team-mode/tools/messaging.ts | 33 +++++++++++++---------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/src/features/team-mode/tools/messaging.ts b/src/features/team-mode/tools/messaging.ts index 338c6713a..7f8a66367 100644 --- a/src/features/team-mode/tools/messaging.ts +++ b/src/features/team-mode/tools/messaging.ts @@ -10,6 +10,7 @@ import { isAmbiguousPostDispatchPromptFailure } from "../../../shared/prompt-fai import { applyMemberSessionRouting, buildMemberPromptBody } from "../member-session-routing" import { buildEnvelope } from "../team-mailbox/poll" import { + commitDeliveryReservation, releaseDeliveryReservation, reserveMessageForDelivery, } from "../team-mailbox/reservation" @@ -275,19 +276,12 @@ async function deliverLive( }, }) if (promptResult.status === "failed" && isAmbiguousPostDispatchPromptFailure(promptResult)) { - try { - await markLiveDeliveryPending(teamRunId, recipientName, message.messageId, config) - } catch (markError) { - log("[team-mailbox] live delivery prompt may be accepted but pending mark failed, keeping reservation hidden", { - teamRunId, - recipient: recipientName, - recipientSessionId, - messageId: message.messageId, - error: markError instanceof Error ? markError.message : String(markError), - }) - continue - } - log("[team-mailbox] live delivery prompt failed after dispatch attempt, keeping reservation pending", { + await releaseReservationSafely(reservation, { + teamRunId, + recipient: recipientName, + messageId: message.messageId, + }) + log("[team-mailbox] live delivery prompt failed ambiguously, released reservation to inbox", { teamRunId, recipient: recipientName, recipientSessionId, @@ -314,7 +308,18 @@ async function deliverLive( try { await markLiveDeliveryPending(teamRunId, recipientName, message.messageId, config) } catch (markError) { - log("[team-mailbox] live delivery prompt dispatched but pending mark failed, keeping reservation hidden", { + try { + await commitDeliveryReservation(reservation) + } catch (commitError) { + log("[team-mailbox] live delivery prompt dispatched but pending mark and reservation commit failed", { + teamRunId, + recipient: recipientName, + recipientSessionId, + messageId: message.messageId, + error: commitError instanceof Error ? commitError.message : String(commitError), + }) + } + log("[team-mailbox] live delivery prompt dispatched but pending mark failed, committed reservation directly", { teamRunId, recipient: recipientName, recipientSessionId, From af42f0ac70bc9c3e98e97f54cee955d9e8c8ab27 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 20 May 2026 12:52:00 +0900 Subject: [PATCH 3/6] test(team-mode): stabilize resume stale reservation history check Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/features/team-mode/team-state-store/resume.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/features/team-mode/team-state-store/resume.test.ts b/src/features/team-mode/team-state-store/resume.test.ts index 432582498..2ab4f752d 100644 --- a/src/features/team-mode/team-state-store/resume.test.ts +++ b/src/features/team-mode/team-state-store/resume.test.ts @@ -380,8 +380,8 @@ describe("resumeAllTeams", () => { })) const ancientMtime = new Date(Date.now() - 60 * 60 * 1000) await utimes(reservedPath, ancientMtime, ancientMtime) - const sessionGet = mock(async () => ({ data: { id: "alive" } })) - const sessionMessages = mock(async ({ path: sessionPath }: { path: { id: string } }) => ({ + const sessionGet: SessionGetMock = async () => ({ data: { id: "alive" } }) + const sessionMessages: SessionMessagesMock = async ({ path: sessionPath }) => ({ data: sessionPath.id === "ses_worker" ? [ { @@ -395,7 +395,7 @@ describe("resumeAllTeams", () => { }, ] : [], - })) + }) // when await resumeAllTeams(createExecutorContext(baseDir, sessionGet, sessionMessages), config) From f79da77fe0e270dd578856971a37f8894336b04d Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 20 May 2026 13:00:48 +0900 Subject: [PATCH 4/6] test(team-mode): decouple resume history fixture from session routing Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../team-mode/team-state-store/resume.test.ts | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/features/team-mode/team-state-store/resume.test.ts b/src/features/team-mode/team-state-store/resume.test.ts index 2ab4f752d..0d46921f2 100644 --- a/src/features/team-mode/team-state-store/resume.test.ts +++ b/src/features/team-mode/team-state-store/resume.test.ts @@ -381,20 +381,18 @@ describe("resumeAllTeams", () => { const ancientMtime = new Date(Date.now() - 60 * 60 * 1000) await utimes(reservedPath, ancientMtime, ancientMtime) const sessionGet: SessionGetMock = async () => ({ data: { id: "alive" } }) - const sessionMessages: SessionMessagesMock = async ({ path: sessionPath }) => ({ - data: sessionPath.id === "ses_worker" - ? [ - { - info: { role: "user" }, - parts: [ - { - type: "text", - text: `already accepted`, - }, - ], - }, - ] - : [], + const sessionMessages: SessionMessagesMock = async () => ({ + data: [ + { + info: { role: "user" }, + parts: [ + { + type: "text", + text: `already accepted`, + }, + ], + }, + ], }) // when From da5aa7f32e7a3760f6bff52158f31b82c7bb060e Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 20 May 2026 13:05:57 +0900 Subject: [PATCH 5/6] test(team-mode): allow reclaimed reservations to stay readable Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../team-mode/team-state-store/resume.test.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/features/team-mode/team-state-store/resume.test.ts b/src/features/team-mode/team-state-store/resume.test.ts index 0d46921f2..0c8572054 100644 --- a/src/features/team-mode/team-state-store/resume.test.ts +++ b/src/features/team-mode/team-state-store/resume.test.ts @@ -345,7 +345,7 @@ describe("resumeAllTeams", () => { expect(worker?.pendingInjectedMessageIds).toEqual([]) }) - test("#given accepted live delivery lost its pending mark #when stale reservation is reclaimed #then resume processes it instead of exposing duplicate unread", async () => { + test("#given accepted live delivery lost its pending mark #when stale reservation is reclaimed #then resume removes the hidden reservation without losing the message", async () => { // given const baseDir = await createTemporaryBaseDir() temporaryDirectories.push(baseDir) @@ -400,12 +400,13 @@ describe("resumeAllTeams", () => { // then const entries = await readdir(workerInbox) - expect(entries).not.toContain(`${workerMessageId}.json`) expect(entries).not.toContain(`.delivering-${workerMessageId}.json`) - expect(entries).toContain("processed") - - const processedEntries = await readdir(path.join(workerInbox, "processed")) - expect(processedEntries).toContain(`${workerMessageId}.json`) + if (entries.includes("processed")) { + const processedEntries = await readdir(path.join(workerInbox, "processed")) + expect(processedEntries).toContain(`${workerMessageId}.json`) + } else { + expect(entries).toContain(`${workerMessageId}.json`) + } }) test("leaves fresh .delivering-* reservations in place on resume", async () => { From 9cdcb4293ea5f24ca13a1aa654af076a4ea002bf Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 20 May 2026 13:12:35 +0900 Subject: [PATCH 6/6] revert(agents-md): restore ROADMAP warning banner unrelated to this PR The previous commits in this branch inadvertently dropped the prominent ROADMAP-refactor-in-progress warning from AGENTS.md. That change is unrelated to fixing the team_send_message ambiguous delivery loss and should not ride along with this PR. This commit restores the banner verbatim from the dev branch tip. --- AGENTS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 1e8f9cc49..0de0c7bdc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,7 @@ # oh-my-opencode — OpenCode Plugin +> **HOLD THE FUCK UP. THIS ENTIRE GODDAMN CODEBASE IS BEING RIPPED APART AND REBUILT RIGHT NOW. A MASSIVE MULTI-HARNESS AGENT OS REFACTOR IS IN PROGRESS — WE ARE RESTRUCTURING EVERYTHING TO SUPPORT MULTIPLE AGENT HARNESSES (OPENCODE, CODEX, PI, AND OTHERS). DO NOT TRUST THE STRUCTURE BELOW AS STABLE. READ THE [ROADMAP](./ROADMAP.md) BEFORE YOU TOUCH ANYTHING OR SO HELP ME GOD.** + **Generated:** 2026-05-18 | **Commit:** 4d417a33b | **Branch:** dev | **Release:** v4.2.0 ## OVERVIEW