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
11 changes: 9 additions & 2 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ export namespace SessionPrompt {
let structuredOutput: unknown | undefined

let step = 0
let naming = Promise.resolve()
const session = await Session.get(sessionID)
while (true) {
await SessionStatus.set(sessionID, { type: "busy" })
Expand Down Expand Up @@ -330,11 +331,15 @@ export namespace SessionPrompt {

step++
if (step === 1)
ensureTitle({
naming = ensureTitle({
session,
abort,
modelID: lastUser.model.modelID,
providerID: lastUser.model.providerID,
history: msgs,
}).catch((err) => {
if (err instanceof DOMException && err.name === "AbortError") return
log.error("failed to ensure title", { error: err, sessionID })
})

const model = await Provider.getModel(lastUser.model.providerID, lastUser.model.modelID).catch((e) => {
Expand Down Expand Up @@ -743,6 +748,7 @@ export namespace SessionPrompt {
}
continue
}
await naming
SessionCompaction.prune({ sessionID })
for await (const item of MessageV2.stream(sessionID)) {
if (item.info.role === "user") continue
Expand Down Expand Up @@ -1975,6 +1981,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
async function ensureTitle(input: {
session: Session.Info
history: MessageV2.WithParts[]
abort: AbortSignal
providerID: ProviderID
modelID: ModelID
}) {
Expand Down Expand Up @@ -2017,7 +2024,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
small: true,
tools: {},
model,
abort: new AbortController().signal,
abort: input.abort,
sessionID: input.session.id,
retries: 2,
messages: [
Expand Down
141 changes: 141 additions & 0 deletions packages/opencode/test/session/prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,74 @@ import { tmpdir } from "../fixture/fixture"

Log.init({ print: false })

function stream(chunks: unknown[]) {
const payload = [...chunks.map((chunk) => `data: ${JSON.stringify(chunk)}`), "data: [DONE]"].join("\n\n") + "\n\n"
const encoder = new TextEncoder()
return new ReadableStream<Uint8Array>({
start(ctrl) {
ctrl.enqueue(encoder.encode(payload))
ctrl.close()
},
})
}

function response(model: string, text: string) {
return new Response(
stream([
{
type: "response.created",
response: {
id: `${model}-response`,
created_at: Math.floor(Date.now() / 1000),
model,
service_tier: null,
},
},
{
type: "response.output_item.added",
output_index: 0,
item: {
type: "message",
id: `${model}-item`,
},
},
{
type: "response.output_text.delta",
item_id: `${model}-item`,
output_index: 0,
content_index: 0,
delta: text,
logprobs: null,
},
{
type: "response.output_item.done",
output_index: 0,
item: {
type: "message",
id: `${model}-item`,
},
},
{
type: "response.completed",
response: {
incomplete_details: null,
usage: {
input_tokens: 1,
input_tokens_details: null,
output_tokens: 1,
output_tokens_details: null,
},
service_tier: null,
},
},
]),
{
status: 200,
headers: { "Content-Type": "text/event-stream" },
},
)
}

describe("session.prompt missing file", () => {
test("does not fail the prompt when a file part is missing", async () => {
await using tmp = await tmpdir({
Expand Down Expand Up @@ -286,3 +354,76 @@ describe("session.agent-resolution", () => {
})
}, 30000)
})

describe("session title generation", () => {
test("waits for the title before returning", async () => {
const seen: string[] = []
const server = Bun.serve({
port: 0,
async fetch(req) {
const body = (await req.json()) as { model?: string }
const model = String(body.model)
seen.push(model)

if (model === "gpt-5-mini") {
await Bun.sleep(200)
return response(model, "Debugging session titles")
}

if (model === "gpt-5.2") {
return response(model, "OK")
}

return new Response(`unexpected model: ${model}`, { status: 500 })
},
})

try {
await using tmp = await tmpdir({
git: true,
config: {
enabled_providers: ["openai"],
agent: {
build: {
model: "openai/gpt-5.2",
},
title: {
model: "openai/gpt-5-mini",
},
},
provider: {
openai: {
options: {
apiKey: "test-key",
baseURL: `${server.url.origin}/v1`,
},
},
},
},
})

await Instance.provide({
directory: tmp.path,
fn: async () => {
const session = await Session.create({})
const msg = await SessionPrompt.prompt({
sessionID: session.id,
agent: "build",
parts: [{ type: "text", text: "Help debug session titles" }],
})

expect(msg.info.role).toBe("assistant")
expect(seen).toContain("gpt-5.2")
expect(seen).toContain("gpt-5-mini")

const info = await Session.get(session.id)
expect(Session.isDefaultTitle(info.title)).toBe(false)
expect(info.title).toBe("Debugging session titles")
},
})
} finally {
await Bun.sleep(250)
server.stop(true)
}
}, 30000)
})
Loading