Skip to content
Open
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
99 changes: 99 additions & 0 deletions packages/provider-opencode/src/server/adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,105 @@ describe("OpenCodeAdapterV2", () => {
),
);

it.effect("continues child turn ordinals when a later Task call resumes the same session", () =>
Effect.gen(function* () {
const nativeEvents = asyncEventStream();
const push = (event: unknown) => Effect.promise(() => nativeEvents.push(event));
const root = "ses_root";
const child = "ses_child";
let promptId = "";
const harness = yield* makeOpenCodeRuntimeHarness("resumed-child", root, {
event: { subscribe: async () => ({ stream: nativeEvents.stream }) },
session: {
create: async () => ({ data: { id: root, time: { created: 1, updated: 1 } } }),
get: async () => ({
data: { id: child, parentID: root, permission: [], time: { created: 2, updated: 2 } },
}),
update: async () => ({ data: { id: child, parentID: root } }),
promptAsync: async (input: { messageID: string }) => {
promptId = input.messageID;
return { data: true };
},
abort: async () => ({ data: true }),
children: async () => ({ data: [] }),
},
});
yield* harness.startTurn();
const received = yield* harness.runtime.events.pipe(
Stream.filter((event) => event.type === "provider_turn.updated"),
Stream.map((event) => event.providerTurn),
Stream.filter(
(turn) =>
turn.providerThreadId !== harness.providerThread.id && turn.status === "completed",
),
Stream.take(2),
Stream.runCollect,
Effect.forkScoped,
);
yield* push({
type: "message.updated",
properties: {
sessionID: root,
info: { id: promptId, sessionID: root, role: "user", time: { created: 1 } },
},
});
for (const ordinal of [1, 2]) {
yield* push({
type: "message.part.updated",
properties: {
sessionID: root,
part: {
id: `prt_task_${ordinal}`,
sessionID: root,
messageID: "msg_root_assistant",
type: "tool",
tool: "task",
callID: `call_task_${ordinal}`,
state: {
status: "running",
input: {
description: "Child task",
prompt: `Child turn ${ordinal}`,
subagent_type: "general",
...(ordinal === 2 ? { task_id: child } : {}),
},
title: "Child task",
metadata: { sessionId: child },
time: { start: ordinal },
},
},
},
});
yield* push({
type: "message.updated",
properties: {
sessionID: child,
info: {
id: `msg_child_user_${ordinal}`,
sessionID: child,
role: "user",
time: { created: ordinal },
},
},
});
yield* push({
type: "session.status",
properties: { sessionID: child, status: { type: "idle" } },
});
}
const turns = yield* Fiber.join(received);
assert.deepEqual(
turns.map((turn) => turn.ordinal),
[1, 2],
);
assert.notEqual(turns[0]?.id, turns[1]?.id);
assert.equal(turns[0]?.providerThreadId, turns[1]?.providerThreadId);
}).pipe(
Effect.provide(Layer.merge(IdAllocator.layer, McpProviderSessions.layer)),
Effect.scoped,
),
);

it.effect("titles OpenCode reads and searches from their input", () =>
Effect.gen(function* () {
const nativeEvents = asyncEventStream();
Expand Down
71 changes: 30 additions & 41 deletions packages/provider-opencode/src/server/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,34 @@ export const makeOpenCodeAdapterV2 = Effect.fn("makeOpenCodeAdapterV2")(function
const subagentsByNativeItemId = new Map<string, OpenCodeSubagentContext>();
const subagentsByChildSessionId = new Map<string, OpenCodeSubagentContext>();
const relatedSessionOwners = new Map<string, OpenCodeThreadState>();
const registerThread = (
nativeSession: OpenCodeSession,
providerThread: OrchestrationV2ProviderThread,
appThread: OrchestrationV2AppThread | null,
): OpenCodeThreadState => {
const existing = threads.get(nativeSession.id);
if (existing !== undefined) {
existing.providerThread = providerThread;
if (appThread !== null) existing.appThread = appThread;
return existing;
}
const state: OpenCodeThreadState = {
nativeSessionId: nativeSession.id,
providerThread,
appThread,
activeTurn: null,
providerTurns: new Map(),
runtimeRequests: new Map(),
messageRoles: new Map(),
userMessageIds: [],
parentSubagent: subagentsByChildSessionId.get(nativeSession.id) ?? null,
nextChildTurnOrdinal: 1,
nextAdmissionGeneration: 1,
};
threads.set(nativeSession.id, state);
return state;
};

// Sessions of this runtime that OpenCode reports busy. A background
// task child keeps running after its parent turn settles, so idle
// release must not close the server under it.
Expand Down Expand Up @@ -1405,19 +1433,8 @@ export const makeOpenCodeAdapterV2 = Effect.fn("makeOpenCodeAdapterV2")(function
permission: childPermission,
}),
);
threads.set(childSessionId, {
nativeSessionId: childSessionId,
providerThread: childProviderThread,
appThread: childThread,
activeTurn: null,
providerTurns: new Map(),
runtimeRequests: new Map(),
messageRoles: new Map(),
userMessageIds: [],
parentSubagent: context,
nextChildTurnOrdinal: 1,
nextAdmissionGeneration: 1,
});
registerThread(nativeChildSession, childProviderThread, childThread).parentSubagent =
context;
yield* emitProviderEvent({
type: "app_thread.created",
driver: OPENCODE_PROVIDER,
Expand Down Expand Up @@ -2783,34 +2800,6 @@ export const makeOpenCodeAdapterV2 = Effect.fn("makeOpenCodeAdapterV2")(function
}),
);

const registerThread = (
nativeSession: OpenCodeSession,
providerThread: OrchestrationV2ProviderThread,
appThread: OrchestrationV2AppThread | null,
): OpenCodeThreadState => {
const existing = threads.get(nativeSession.id);
if (existing !== undefined) {
existing.providerThread = providerThread;
if (appThread !== null) existing.appThread = appThread;
return existing;
}
const state: OpenCodeThreadState = {
nativeSessionId: nativeSession.id,
providerThread,
appThread,
activeTurn: null,
providerTurns: new Map(),
runtimeRequests: new Map(),
messageRoles: new Map(),
userMessageIds: [],
parentSubagent: subagentsByChildSessionId.get(nativeSession.id) ?? null,
nextChildTurnOrdinal: 1,
nextAdmissionGeneration: 1,
};
threads.set(nativeSession.id, state);
return state;
};

const resolvePromptParts = (turnInput: ProviderAdapter.ProviderAdapterV2TurnInput) => {
const text = providerMessageTextWithAttachmentPaths({
text: turnInput.message.text,
Expand Down
Loading