authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-09-04 11:04:05-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-04 11:04:05-07:00
logc8f77e0d441264efb0acfac312e852c81ae3da83
tree03d09d6070deb71d5b0e633bcf24915f24b2ecd2
parent088cc3f95599b7b933289d719fa1e9b0608cb4cd
signature Signed by PGP key B5690EEEBB952194

perf(server): stop caching unused OpenCode tool parts (#9738)

Do not retain OpenCode tool parts after emitting their runtime events. The remaining cache readers need text, reasoning, or step-usage parts, not tool input and output. Keep tool lifecycle events, output bytes, late-role assistant text, and usage handling unchanged. Add focused lifecycle coverage and verify retained memory through the real adapter. Created with GPT-6 Astra (preview) in Codex.

2 files changed, 132 insertions(+), 1 deletions(-)

apps/server/src/provider/Layers/OpenCodeAdapter.test.ts+128
...@@ -21,6 +21,7 @@ import type {...@@ -21,6 +21,7 @@ import type {
21 Event as OpenCodeEvent,21 Event as OpenCodeEvent,
22 PermissionRequest,22 PermissionRequest,
23 QuestionRequest,23 QuestionRequest,
24 ToolPart,
24} from "@opencode-ai/sdk/v2";25} from "@opencode-ai/sdk/v2";
2526
26import {27import {
...@@ -6536,6 +6537,133 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => {...@@ -6536,6 +6537,133 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => {
6536 }),6537 }),
6537 );6538 );
65386539
6540 it.effect("emits tool lifecycle events before late assistant metadata", () =>
6541 Effect.gen(function* () {
6542 const adapter = yield* OpenCodeAdapter;
6543 const threadId = asThreadId("thread-tool-lifecycle");
6544 const sessionID = "http://127.0.0.1:9999/session";
6545 const messageID = "msg-tools-before-role";
6546 const start = promiseWithResolvers<OpenCodeEvent>();
6547 const input = { command: "pwd" };
6548 const states = [
6549 { status: "pending", input, raw: "" },
6550 { status: "running", input, title: "Working directory", time: { start: 1 } },
6551 {
6552 status: "completed",
6553 input,
6554 output: "/repo\n",
6555 title: "Working directory",
6556 metadata: {},
6557 time: { start: 1, end: 2 },
6558 },
6559 { status: "error", input, error: "Command failed", time: { start: 3, end: 4 } },
6560 ] satisfies ReadonlyArray<ToolPart["state"]>;
6561 runtimeMock.state.subscribedEvents = [
6562 start.promise,
6563 ...states.map(
6564 (state) =>
6565 ({
6566 id: `evt-tool-${state.status}`,
6567 type: "message.part.updated",
6568 properties: {
6569 sessionID,
6570 time: 4,
6571 part: {
6572 id: state.status === "error" ? "part-failed" : "part-working",
6573 sessionID,
6574 messageID,
6575 type: "tool",
6576 callID: state.status === "error" ? "call-failed" : "call-working",
6577 tool: "bash",
6578 state,
6579 },
6580 },
6581 }) satisfies OpenCodeEvent,
6582 ),
6583 {
6584 id: "evt-text-before-role",
6585 type: "message.part.updated",
6586 properties: {
6587 sessionID,
6588 time: 4,
6589 part: {
6590 id: "part-late-text",
6591 sessionID,
6592 messageID,
6593 type: "text",
6594 text: "Tool results received",
6595 time: { start: 4 },
6596 },
6597 },
6598 },
6599 {
6600 id: "evt-late-assistant-role",
6601 type: "message.updated",
6602 properties: { sessionID, info: { id: messageID, role: "assistant" } },
6603 },
6604 {
6605 id: "evt-tool-lifecycle-drained",
6606 type: "session.compacted",
6607 properties: { sessionID },
6608 },
6609 ];
6610 const eventsFiber = yield* adapter.streamEvents.pipe(
6611 Stream.filter((event) => event.threadId === threadId),
6612 Stream.takeUntil((event) => event.type === "thread.state.changed"),
6613 Stream.runCollect,
6614 Effect.forkChild,
6615 );
6616 yield* adapter.startSession({
6617 provider: ProviderDriverKind.make("opencode"),
6618 threadId,
6619 runtimeMode: "full-access",
6620 });
6621 yield* adapter.sendTurn({
6622 threadId,
6623 input: "Read the working directory",
6624 modelSelection: createModelSelection(
6625 ProviderInstanceId.make("opencode"),
6626 "opencode/kimi-k3",
6627 ),
6628 });
6629 start.resolve({
6630 id: "evt-tool-lifecycle-started",
6631 type: "session.status",
6632 properties: { sessionID, status: { type: "busy" } },
6633 });
6634 const events = yield* Fiber.join(eventsFiber);
6635 const tools = events.filter(
6636 (event) =>
6637 event.type === "item.started" ||
6638 event.type === "item.updated" ||
6639 event.type === "item.completed",
6640 );
6641 NodeAssert.deepEqual(
6642 tools.map((event) => [event.type, event.itemId, event.payload.status]),
6643 [
6644 ["item.started", "call-working", "inProgress"],
6645 ["item.updated", "call-working", "inProgress"],
6646 ["item.completed", "call-working", "completed"],
6647 ["item.completed", "call-failed", "failed"],
6648 ],
6649 );
6650 NodeAssert.partialDeepStrictEqual(tools[2]?.payload.data, {
6651 command: "pwd",
6652 result: "/repo\n",
6653 });
6654 NodeAssert.partialDeepStrictEqual(tools[3]?.payload.data, {
6655 command: "pwd",
6656 state: { error: "Command failed" },
6657 });
6658 NodeAssert.deepEqual(
6659 events
6660 .filter((event) => event.type === "content.delta")
6661 .map((event) => event.payload.delta),
6662 ["Tool results received"],
6663 );
6664 }),
6665 );
6666
6539 it.effect("maps native task progress only while a turn is active", () =>6667 it.effect("maps native task progress only while a turn is active", () =>
6540 Effect.gen(function* () {6668 Effect.gen(function* () {
6541 const adapter = yield* OpenCodeAdapter;6669 const adapter = yield* OpenCodeAdapter;
apps/server/src/provider/Layers/OpenCodeAdapter.ts+4-1
...@@ -2408,7 +2408,10 @@ export function makeOpenCodeAdapter(...@@ -2408,7 +2408,10 @@ export function makeOpenCodeAdapter(
24082408
2409 case "message.part.updated": {2409 case "message.part.updated": {
2410 const part = event.properties.part;2410 const part = event.properties.part;
2411 context.partById.set(part.id, part);2411 // Tool events use the incoming part and do not need a cached copy.
2412 if (part.type !== "tool") {
2413 context.partById.set(part.id, part);
2414 }
2412 const messageRole = messageRoleForPart(context, part);2415 const messageRole = messageRoleForPart(context, part);
24132416
2414 if (turnId && part.type === "step-finish" && context.turnTokenUsage) {2417 if (turnId && part.type === "step-finish" && context.turnTokenUsage) {