Skip to content
Merged
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
22 changes: 12 additions & 10 deletions packages/core/src/session/compaction.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ import { makeLocationNode } from "../effect/app-node"
import { llmClient } from "../effect/app-node-platform"
import { SessionEvent } from "./event"
import type { SessionMessage } from "./message"
import { SessionModelHeaders } from "./model-headers"
import { SessionRunnerModel } from "./runner/model"
import { SessionSchema } from "./schema"
import { toSessionError } from "./to-session-error"
Expand DownExpand Up@@ -66,7 +67,7 @@ type Dependencies = {
}

export type AutoInput = {
readonly sessionID: SessionSchema.ID
readonly session: SessionSchema.Info
readonly messages: readonly SessionMessage.Info[]
readonly model: Model
}
Expand All@@ -78,7 +79,7 @@ export type ManualInput = {
}

type Plan = {
readonly sessionID: SessionSchema.ID
readonly session: SessionSchema.Info
readonly model: Model
readonly reason: SessionMessage.Compaction["reason"]
readonly prompt: string
Expand DownExpand Up@@ -230,7 +231,7 @@ const make = (dependencies: Dependencies) => {
})
const execute = Effect.fn("SessionCompaction.execute")(function* (plan: Plan) {
yield* dependencies.events.publish(SessionEvent.Compaction.Started, {
sessionID: plan.sessionID,
sessionID: plan.session.id,
reason: plan.reason,
recent: plan.recent,
inputID: plan.inputID,
Expand All@@ -242,6 +243,7 @@ const make = (dependencies: Dependencies) => {
.stream(
LLM.request({
model: plan.model,
http: { headers: SessionModelHeaders.make(plan.session) },
messages: [Message.user(plan.prompt)],
tools: [],
}),
Expand All@@ -256,7 +258,7 @@ const make = (dependencies: Dependencies) => {
if (LLMEvent.is.textDelta(event)) {
chunks.push(event.text)
return dependencies.events.publish(SessionEvent.Compaction.Delta, {
sessionID: plan.sessionID,
sessionID: plan.session.id,
text: event.text,
})
}
Expand All@@ -270,7 +272,7 @@ const make = (dependencies: Dependencies) => {
Effect.onInterrupt(() =>
plan.reason === "auto"
? failed({
sessionID: plan.sessionID,
sessionID: plan.session.id,
reason: plan.reason,
error: { type: "compaction.interrupted", message: "Compaction was interrupted" },
inputID: plan.inputID,
Expand All@@ -282,14 +284,14 @@ const make = (dependencies: Dependencies) => {
if (failure || !summary.trim()) {
const error = failure ?? { type: "compaction.failed" as const, message: "Compaction produced no summary" }
return yield* failed({
sessionID: plan.sessionID,
sessionID: plan.session.id,
reason: plan.reason,
error,
inputID: plan.inputID,
})
}
yield* dependencies.events.publish(SessionEvent.Compaction.Ended, {
sessionID: plan.sessionID,
sessionID: plan.session.id,
reason: plan.reason,
text: summary,
recent: plan.recent,
Expand All@@ -300,14 +302,14 @@ const make = (dependencies: Dependencies) => {
const content = planContent(input.messages, config.tokens)
if (content)
return yield* execute({
sessionID: input.sessionID,
session: input.session,
model: input.model,
reason: "auto",
...content,
})
const error = { type: "compaction.unavailable" as const, message: "Nothing to compact yet" }
return yield* failed({
sessionID: input.sessionID,
sessionID: input.session.id,
reason: "auto",
error,
})
Expand DownExpand Up@@ -348,7 +350,7 @@ const make = (dependencies: Dependencies) => {
)
if ("status" in resolved) return resolved
return yield* execute({
sessionID: input.session.id,
session: input.session,
model: resolved.model,
reason: "manual",
inputID: input.inputID,
Expand Down
15 changes: 15 additions & 0 deletions packages/core/src/session/model-headers.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
export * as SessionModelHeaders from "./model-headers"

import { Flag } from "../flag/flag"
import { InstallationVersion } from "../installation/version"
import { SessionSchema } from "./schema"

export const make = (session: Pick<SessionSchema.Info, "id" | "parentID" | "projectID">) => ({
"x-session-affinity": session.id,
"X-Session-Id": session.id,
...(session.parentID ? { "x-parent-session-id": session.parentID } : {}),
"User-Agent": `opencode/${InstallationVersion}`,
"x-opencode-project": session.projectID,
"x-opencode-session": session.id,
"x-opencode-client": Flag.OPENCODE_CLIENT,
})
12 changes: 4 additions & 8 deletions packages/core/src/session/runner/llm.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -51,7 +51,7 @@ import { AgentNotFoundError, StepFailedError } from "../error"
import { toSessionError } from "../to-session-error"
import { SessionRunnerRetry } from "./retry"
import { PluginSupervisor } from "../../plugin/supervisor"
import { Flag } from "../../flag/flag"
import { SessionModelHeaders } from "../model-headers"

type StepTokens = {
readonly input: number
Expand DownExpand Up@@ -187,7 +187,7 @@ const layer = Layer.effect(
const providerMetadataKey = model.route.providerMetadataKey ?? model.provider
const history = yield* SessionHistory.entriesForRunner(db, session.id, instructions)
const context = history.entries.map((entry) => entry.message)
const compactionInput = { sessionID: session.id, messages: context, model }
const compactionInput = { session, messages: context, model }
if (compaction.required(compactionInput) && !(yield* SessionPending.compaction(db, session.id))) {
const compacted = yield* compaction.compact(compactionInput)
if (compacted.status === "completed") return { _tag: "RestartAfterCompaction", step: currentStep } as const
Expand All@@ -199,11 +199,7 @@ const layer = Layer.effect(
const request = LLM.request({
model,
http: {
headers: {
"x-opencode-project": session.projectID,
"x-opencode-session": session.id,
"x-opencode-client": Flag.OPENCODE_CLIENT,
},
headers: SessionModelHeaders.make(session),
},
providerOptions: { openai: { promptCacheKey } },
system: [agentInfo.system ? agentInfo.system : SessionRunnerSystemPrompt.provider(model), history.initial]
Expand DownExpand Up@@ -341,7 +337,7 @@ const layer = Layer.effect(
recoverOverflow &&
!publisher.hasRetryEvidence() &&
isContextOverflowFailure(overflowFailure ?? streamFailure) &&
(yield* restore(recoverOverflow({ sessionID: session.id, messages: context, model }))).status ===
(yield* restore(recoverOverflow({ session, messages: context, model }))).status ===
"completed"
)
return { _tag: "RestartAfterOverflowCompaction", step: currentStep } as const
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/session/title.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ import { makeLocationNode } from "../effect/app-node"
import { llmClient } from "../effect/app-node-platform"
import { SessionEvent } from "./event"
import { SessionHistory } from "./history"
import { SessionModelHeaders } from "./model-headers"
import { SessionRunnerModel } from "./runner/model"
import { SessionSchema } from "./schema"

Expand DownExpand Up@@ -54,6 +55,7 @@ const make = (dependencies: Dependencies) => {
.stream(
LLM.request({
model: resolved.model,
http: { headers: SessionModelHeaders.make(session) },
system: agent.system,
messages: [Message.user(firstUser.text)],
tools: [],
Expand Down
13 changes: 13 additions & 0 deletions packages/core/test/session-compaction.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,8 @@ import { SessionStore } from "@opencode-ai/core/session/store"
import { SessionV2 } from "@opencode-ai/core/session"
import { Project } from "@opencode-ai/core/project"
import { ProjectTable } from "@opencode-ai/core/project/sql"
import { Flag } from "@opencode-ai/core/flag/flag"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { DateTime, Effect, Fiber, Layer, Stream } from "effect"
import { asc, eq } from "drizzle-orm"
Expand DownExpand Up@@ -104,6 +106,7 @@ it.effect("manual compaction summarizes short context instead of no-op", () =>
const events = yield* EventV2.Service
const store = yield* SessionStore.Service
const sessionID = SessionV2.ID.make("ses_manual_compaction")
const parentID = SessionV2.ID.make("ses_manual_compaction_parent")
const userMessage = {
id: SessionMessage.ID.create(),
type: "user" as const,
Expand All@@ -121,6 +124,7 @@ it.effect("manual compaction summarizes short context instead of no-op", () =>
.values({
id: sessionID,
project_id: Project.ID.global,
parent_id: parentID,
slug: "manual-compaction",
directory: "/project",
title: "Manual compaction",
Expand DownExpand Up@@ -151,6 +155,15 @@ it.effect("manual compaction summarizes short context instead of no-op", () =>
expect(Array.from(yield* Fiber.join(delta)).map((event) => event.data.text)).toEqual(["manual summary"])

expect(requests).toHaveLength(1)
expect(requests[0]?.http?.headers).toEqual({
"x-session-affinity": sessionID,
"X-Session-Id": sessionID,
"x-parent-session-id": parentID,
"User-Agent": `opencode/${InstallationVersion}`,
"x-opencode-project": Project.ID.global,
"x-opencode-session": sessionID,
"x-opencode-client": Flag.OPENCODE_CLIENT,
})
expect(requests[0]?.generation).toBeUndefined()
expect(JSON.stringify(requests[0]?.messages)).toContain("Manual compaction should include this short conversation.")
expect(yield* store.context(sessionID)).toMatchObject([
Expand Down
23 changes: 23 additions & 0 deletions packages/core/test/session-runner.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,7 @@ import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { EventV2 } from "@opencode-ai/core/event"
import { Flag } from "@opencode-ai/core/flag/flag"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { PermissionV2 } from "@opencode-ai/core/permission"
import { EventTable } from "@opencode-ai/core/event/sql"
import { Project } from "@opencode-ai/core/project"
Expand DownExpand Up@@ -3086,13 +3087,35 @@ describe("SessionRunnerLLM", () => {
yield* session.resume(sessionID)

expect(requests[0]?.http?.headers).toEqual({
"x-session-affinity": sessionID,
"X-Session-Id": sessionID,
"User-Agent": `opencode/${InstallationVersion}`,
"x-opencode-project": Project.ID.global,
"x-opencode-session": sessionID,
"x-opencode-client": Flag.OPENCODE_CLIENT,
})
}),
)

it.effect("adds the parent session header to child model requests", () =>
Effect.gen(function* () {
const session = yield* setup
const parentID = SessionV2.ID.make("ses_runner_parent")
const { db } = yield* Database.Service
yield* db
.update(SessionTable)
.set({ parent_id: parentID })
.where(eq(SessionTable.id, sessionID))
.run()
.pipe(Effect.orDie)
yield* admit(session, "Run child request")

yield* session.resume(sessionID)

expect(requests[0]?.http?.headers?.["x-parent-session-id"]).toBe(parentID)
}),
)

it.effect("runs different sessions concurrently", () =>
Effect.gen(function* () {
const session = yield* setup
Expand Down
10 changes: 10 additions & 0 deletions packages/core/test/session-title.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,8 @@ import { SessionTitle } from "@opencode-ai/core/session/title"
import { SessionV2 } from "@opencode-ai/core/session"
import { Project } from "@opencode-ai/core/project"
import { ProjectTable } from "@opencode-ai/core/project/sql"
import { Flag } from "@opencode-ai/core/flag/flag"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { DateTime, Effect, Layer, Stream } from "effect"
import { testEffect } from "./lib/effect"
Expand DownExpand Up@@ -117,6 +119,14 @@ it.effect("generates a title from the sole user message and renames the session"
yield* title.generateForFirstPrompt(session)

expect(requests).toHaveLength(1)
expect(requests[0]?.http?.headers).toEqual({
"x-session-affinity": sessionID,
"X-Session-Id": sessionID,
"User-Agent": `opencode/${InstallationVersion}`,
"x-opencode-project": Project.ID.global,
"x-opencode-session": sessionID,
"x-opencode-client": Flag.OPENCODE_CLIENT,
})
expect(JSON.stringify(requests[0]?.messages)).toContain("Help me debug the failing build")
const renamed = yield* store.get(sessionID)
expect(renamed?.title).toBe("Generated Title")
Expand Down
Loading