Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions packages/opencode/src/cli/cmd/run/stream.transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,8 +400,12 @@ const collectDescendantSessions = Effect.fn("RunStreamTransport.collectDescendan
const levels = yield* Effect.all(
frontier.map((sessionID) =>
Effect.promise(() => sdk.session.children({ sessionID })).pipe(
Effect.map((item) => item.data ?? []),
Effect.orElseSucceed(() => []),
Effect.flatMap((item) => {
if (item.error) {
return Effect.fail(new Error(`failed to list child sessions for ${sessionID}`, { cause: item.error }))
}
return Effect.succeed(item.data ?? [])
}),
),
),
{ concurrency: "unbounded" },
Expand Down
34 changes: 34 additions & 0 deletions packages/opencode/test/cli/run/stream.transport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1525,6 +1525,40 @@ describe("run stream transport", () => {
}
})

test("fails bootstrap when descendant discovery fails", async () => {
const src = eventFeed()
const ui = footer()
const children = (async ({ sessionID }: { sessionID: string }) => {
if (sessionID === "session-1") return ok([child("child-1")])
return {
data: undefined,
error: {
name: "NotFoundError",
data: { message: "child lookup failed" },
},
request: new Request("https://opencode.test"),
response: new Response(undefined, { status: 404 }),
}
}) as OpencodeClient["session"]["children"]
const result = createSessionTransport({
sdk: sdk({
stream: src.stream,
children,
}),
sessionID: "session-1",
thinking: true,
limits: () => ({}),
footer: ui.api,
})

try {
await expect(result).rejects.toThrow("failed to list child sessions for child-1")
} finally {
src.close()
await result.then((transport) => transport.close()).catch(() => undefined)
}
})

test("bootstraps child session output before selection", async () => {
const ui = footer()
const transport = await createSessionTransport({
Expand Down
2 changes: 1 addition & 1 deletion packages/tui/src/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ export function Session() {
: [],
)
const subtree = createMemo(() => {
// NOTE: 仅根会话收集整棵子树的权限/问题;子会话自身不重复收集(避免与父视图双重展示)
// Only root sessions collect subtree requests; child views do not duplicate them.
const s = session()
if (!s || s.parentID) return []
return collectSubtree(sync.data.session, s.id)
Expand Down
28 changes: 20 additions & 8 deletions packages/tui/test/routes/session/collect-subtree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,25 @@ describe("collectSubtree", () => {
s({ id: "child-2", parentID: "root" }),
s({ id: "grandchild-2", parentID: "child-2" }),
]
expect(collectSubtree(sessions, "root").map((x) => x.id).sort()).toEqual(
["child-1", "child-2", "grandchild-1", "grandchild-2", "root"],
)
expect(collectSubtree(sessions, "child-1").map((x) => x.id).sort()).toEqual(["child-1", "grandchild-1"])
expect(
collectSubtree(sessions, "root")
.map((x) => x.id)
.sort(),
).toEqual(["child-1", "child-2", "grandchild-1", "grandchild-2", "root"])
expect(
collectSubtree(sessions, "child-1")
.map((x) => x.id)
.sort(),
).toEqual(["child-1", "grandchild-1"])
})

test("preserves breadth-first order and session identity", () => {
const root = s({ id: "root" })
const first = s({ id: "first", parentID: "root" })
const grandchild = s({ id: "grandchild", parentID: "first" })
const second = s({ id: "second", parentID: "root" })

expect(collectSubtree([root, first, grandchild, second], "root")).toEqual([root, first, second, grandchild])
})

test("includes root itself in the result", () => {
Expand All @@ -58,10 +73,7 @@ describe("collectSubtree", () => {
})

test("does not loop forever on a parentID cycle", () => {
const sessions = [
s({ id: "root", parentID: "child" }),
s({ id: "child", parentID: "root" }),
]
const sessions = [s({ id: "root", parentID: "child" }), s({ id: "child", parentID: "root" })]
const ids = collectSubtree(sessions, "root").map((x) => x.id)
expect(ids).toContain("root")
expect(ids).toContain("child")
Expand Down
Loading