Skip to content
1 change: 1 addition & 0 deletions src/features/background-agent/session-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export async function validateSessionHasOutput(
if (type === "tool") return true
if (type === "text" && hasNonEmptyText(part.text)) return true
if (type === "reasoning" && hasNonEmptyText(part.text)) return true
if (type === "reasoning.encrypted" && hasNonEmptyText(part.text)) return true
if (type === "tool_result" && isToolResultContentNonEmpty(part.content)) return true
return false
})
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/session-recovery/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ export const OPENCODE_STORAGE = getOpenCodeStorageDir()
export const MESSAGE_STORAGE = join(OPENCODE_STORAGE, "message")
export const PART_STORAGE = join(OPENCODE_STORAGE, "part")

export const THINKING_TYPES = new Set(["thinking", "redacted_thinking", "reasoning"])
export const THINKING_TYPES = new Set(["thinking", "redacted_thinking", "reasoning", "reasoning.encrypted"])
export const META_TYPES = new Set(["step-start", "step-finish"])
export const CONTENT_TYPES = new Set(["text", "tool", "tool_use", "tool_result"])
23 changes: 23 additions & 0 deletions src/hooks/session-recovery/reasoning-encrypted.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { describe, expect, it } from "bun:test"
import { THINKING_TYPES } from "./constants"

describe("reasoning.encrypted support", () => {
//#given reasoning.encrypted is a valid thinking-family type from Grok models

it("should include reasoning.encrypted in THINKING_TYPES Set", () => {
//#when checking if reasoning.encrypted is in THINKING_TYPES
const result = THINKING_TYPES.has("reasoning.encrypted")

//#then it should return true
expect(result).toBe(true)
})

it("should recognize reasoning.encrypted as a ThinkingPartType", () => {
//#given a type assertion for reasoning.encrypted
//#when assigning to ThinkingPartType
const thinkingType: import("./types").ThinkingPartType = "reasoning.encrypted"

//#then it should compile without error
expect(thinkingType).toBe("reasoning.encrypted")
})
})
2 changes: 1 addition & 1 deletion src/hooks/session-recovery/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type ThinkingPartType = "thinking" | "redacted_thinking" | "reasoning"
export type ThinkingPartType = "thinking" | "redacted_thinking" | "reasoning" | "reasoning.encrypted"
export type MetaPartType = "step-start" | "step-finish"
export type ContentPartType = "text" | "tool" | "tool_use" | "tool_result"

Expand Down
4 changes: 2 additions & 2 deletions src/hooks/thinking-block-validator/hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ function startsWithThinkingBlock(parts: Part[]): boolean {

const firstPart = parts[0]
const type = firstPart.type as string
return type === "thinking" || type === "reasoning"
return type === "thinking" || type === "reasoning" || type === "reasoning.encrypted"
}

/**
Expand All @@ -90,7 +90,7 @@ function findPreviousThinkingContent(
if (!msg.parts) continue
for (const part of msg.parts) {
const type = part.type as string
if (type === "thinking" || type === "reasoning") {
if (type === "thinking" || type === "reasoning" || type === "reasoning.encrypted") {
const thinking = (part as any).thinking || (part as any).text
if (thinking && typeof thinking === "string" && thinking.trim().length > 0) {
return thinking
Expand Down
58 changes: 58 additions & 0 deletions src/hooks/thinking-block-validator/reasoning-encrypted.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { describe, expect, it } from "bun:test"
import { createThinkingBlockValidatorHook } from "./hook"

describe("reasoning.encrypted in thinking-block-validator", () => {
const hook = createThinkingBlockValidatorHook()
const transform = hook["experimental.chat.messages.transform"]!

function makeMessages(msgs: Array<{ role: string; parts: Array<Record<string, unknown>>; modelID?: string }>) {
return msgs.map((m, i) => ({
info: { id: `msg_${i}`, role: m.role, modelID: m.modelID } as Record<string, unknown>,
parts: m.parts.map((p, j) => ({ id: `prt_${i}_${j}`, sessionID: "ses_test", messageID: `msg_${i}`, ...p })),
}))
}

it("should not prepend thinking when message starts with reasoning.encrypted", async () => {
//#given an assistant message that starts with a reasoning.encrypted block
const messages = makeMessages([
{ role: "user", parts: [{ type: "text", text: "test" }], modelID: "claude-opus-4" },
{ role: "assistant", parts: [
{ type: "reasoning.encrypted", text: "encrypted reasoning" },
{ type: "text", text: "response" },
]},
])

//#when the hook processes the messages
const output = { messages }
await transform({} as Record<string, never>, output)

//#then no synthetic thinking block should be prepended
const assistantParts = output.messages[1].parts
expect(assistantParts[0].type).toBe("reasoning.encrypted")
expect(assistantParts).toHaveLength(2)
})

it("should find previous reasoning.encrypted content for messages missing thinking", async () => {
//#given a previous message with reasoning.encrypted and a current message without thinking
const messages = makeMessages([
{ role: "user", parts: [{ type: "text", text: "test" }], modelID: "claude-opus-4" },
{ role: "assistant", parts: [
{ type: "reasoning.encrypted", text: "previous encrypted reasoning" },
{ type: "text", text: "first response" },
]},
{ role: "user", parts: [{ type: "text", text: "follow up" }], modelID: "claude-opus-4" },
{ role: "assistant", parts: [
{ type: "tool_use", name: "bash" },
]},
])

//#when the hook processes the messages
const output = { messages }
await transform({} as Record<string, never>, output)

//#then the second assistant message should have a prepended thinking block with previous reasoning content
const secondAssistantParts = output.messages[3].parts
expect(secondAssistantParts[0].type).toBe("thinking")
expect((secondAssistantParts[0] as Record<string, unknown>).thinking).toBe("previous encrypted reasoning")
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ async function getThinkingSummary(ctx: BabysitterContext, sessionID: string): Pr
if (part.type === "reasoning" && part.text) {
chunks.push(part.text)
}
if (part.type === "reasoning.encrypted" && part.text) {
chunks.push(part.text)
}
}
}

Expand Down
4 changes: 3 additions & 1 deletion src/tools/background-task/full-session-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export async function formatFullSession(
const normalizedMessages: BackgroundOutputMessage[] = []
for (const message of filteredMessages) {
const parts = (message.parts ?? []).filter((part) => {
if (part.type === "thinking" || part.type === "reasoning") {
if (part.type === "thinking" || part.type === "reasoning" || part.type === "reasoning.encrypted") {
return includeThinking
}
if (part.type === "tool_result") {
Expand Down Expand Up @@ -135,6 +135,8 @@ export async function formatFullSession(
lines.push(`[thinking] ${truncateText(part.thinking, thinkingMaxChars)}`)
} else if (part.type === "reasoning" && part.text) {
lines.push(`[thinking] ${truncateText(part.text, thinkingMaxChars)}`)
} else if (part.type === "reasoning.encrypted" && part.text) {
lines.push(`[thinking] ${truncateText(part.text, thinkingMaxChars)}`)
} else if (part.type === "tool_result") {
const toolTexts = extractToolResultText(part)
for (const toolText of toolTexts) {
Expand Down
4 changes: 3 additions & 1 deletion src/tools/background-task/modules/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ export async function formatFullSession(
const normalizedMessages: FullSessionMessage[] = []
for (const message of filteredMessages) {
const parts = (message.parts ?? []).filter((part) => {
if (part.type === "thinking" || part.type === "reasoning") {
if (part.type === "thinking" || part.type === "reasoning" || part.type === "reasoning.encrypted") {
return includeThinking
}
if (part.type === "tool_result") {
Expand Down Expand Up @@ -302,6 +302,8 @@ export async function formatFullSession(
lines.push(`[thinking] ${truncateText(part.thinking, thinkingMaxChars)}`)
} else if (part.type === "reasoning" && part.text) {
lines.push(`[thinking] ${truncateText(part.text, thinkingMaxChars)}`)
} else if (part.type === "reasoning.encrypted" && part.text) {
lines.push(`[thinking] ${truncateText(part.text, thinkingMaxChars)}`)
} else if (part.type === "tool_result") {
const toolTexts = extractToolResultText(part)
for (const toolText of toolTexts) {
Expand Down
2 changes: 1 addition & 1 deletion src/tools/background-task/modules/message-processing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export function extractToolResultText(part: FullSessionMessagePart): string[] {

if (Array.isArray(part.content)) {
const blocks = part.content
.filter((block) => (block.type === "text" || block.type === "reasoning") && block.text)
.filter((block) => (block.type === "text" || block.type === "reasoning" || block.type === "reasoning.encrypted") && block.text)
.map((block) => block.text as string)
if (blocks.length > 0) return blocks
}
Expand Down
49 changes: 49 additions & 0 deletions src/tools/background-task/reasoning-encrypted-extraction.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { describe, expect, it } from "bun:test"

describe("reasoning.encrypted content extraction", () => {
//#given content parts with reasoning.encrypted type

it("should extract text from reasoning.encrypted part when text is present", () => {
//#given a part with reasoning.encrypted type and text
const part = { type: "reasoning.encrypted" as const, text: "decrypted reasoning content" }

//#when filtering for text or reasoning.encrypted with text guard
const shouldInclude = !!((part.type === "text" || part.type === "reasoning" || part.type === "reasoning.encrypted") && part.text)

//#then it should be included
expect(shouldInclude).toBe(true)
})

it("should skip reasoning.encrypted part when text is missing", () => {
//#given a part with reasoning.encrypted type but no text
const part = { type: "reasoning.encrypted" as const }

//#when filtering for text or reasoning.encrypted with text guard
const shouldInclude = !!((part.type === "text" || part.type === "reasoning" || part.type === "reasoning.encrypted") && (part as any).text)

//#then it should be excluded
expect(shouldInclude).toBe(false)
})

it("should extract text from reasoning part when text is present", () => {
//#given a part with reasoning type and text
const part = { type: "reasoning" as const, text: "reasoning content" }

//#when filtering for text or reasoning with text guard
const shouldInclude = !!((part.type === "text" || part.type === "reasoning" || part.type === "reasoning.encrypted") && part.text)

//#then it should be included
expect(shouldInclude).toBe(true)
})

it("should extract text from text part", () => {
//#given a part with text type
const part = { type: "text" as const, text: "text content" }

//#when filtering for text or reasoning with text guard
const shouldInclude = !!((part.type === "text" || part.type === "reasoning" || part.type === "reasoning.encrypted") && part.text)

//#then it should be included
expect(shouldInclude).toBe(true)
})
})
2 changes: 1 addition & 1 deletion src/tools/delegate-task/sync-result-fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export async function fetchSyncResult(
return { ok: false, error: `No assistant response found.\n\nSession ID: ${sessionID}` }
}

const textParts = lastMessage?.parts?.filter((p) => p.type === "text" || p.type === "reasoning") ?? []
const textParts = lastMessage?.parts?.filter((p) => p.type === "text" || p.type === "reasoning" || p.type === "reasoning.encrypted") ?? []
const textContent = textParts.map((p) => p.text ?? "").filter(Boolean).join("\n")

return { ok: true, textContent }
Expand Down
2 changes: 1 addition & 1 deletion src/tools/delegate-task/sync-session-poller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export async function pollSyncSession(
if (m.info?.role !== "assistant") return false
const parts = m.parts ?? []
return parts.some((p) => {
if (p.type !== "text" && p.type !== "reasoning") return false
if (p.type !== "text" && p.type !== "reasoning" && p.type !== "reasoning.encrypted") return false
const text = (p.text ?? "").trim()
return text.length > 0
})
Expand Down
2 changes: 1 addition & 1 deletion src/tools/delegate-task/unstable-agent-task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export async function executeUnstableAgentTask(
return `No assistant response found (task ran in background mode).\n\nSession ID: ${sessionID}`
}

const textParts = lastMessage?.parts?.filter((p) => p.type === "text" || p.type === "reasoning") ?? []
const textParts = lastMessage?.parts?.filter((p) => p.type === "text" || p.type === "reasoning" || p.type === "reasoning.encrypted") ?? []
const textContent = textParts.map((p) => p.text ?? "").filter(Boolean).join("\n")
const duration = formatDuration(startTime)

Expand Down