fix: address 8-domain Oracle review findings (C1, C2, M1-M4)
- C1: thinking-prepend unique part IDs per message (global PK collision) - C2: recover-thinking-disabled-violation try/catch guard on SDK call - M1: remove non-schema truncated/originalSize fields from SDK interfaces - M2: messageHasContentFromSDK treats thinking-only messages as non-empty - M3: syncAllTasksToTodos persists finalTodos + no-id rename dedup guard - M4: AbortSignal.timeout(30s) on HTTP fetch calls in opencode-http-api All 2739 tests pass, typecheck clean.
This commit is contained in:
@@ -20,10 +20,15 @@ function messageHasContentFromSDK(message: SDKMessage): boolean {
|
||||
const parts = message.parts
|
||||
if (!parts || parts.length === 0) return false
|
||||
|
||||
let hasIgnoredParts = false
|
||||
|
||||
for (const part of parts) {
|
||||
const type = part.type
|
||||
if (!type) continue
|
||||
if (IGNORE_TYPES.has(type)) continue
|
||||
if (IGNORE_TYPES.has(type)) {
|
||||
hasIgnoredParts = true
|
||||
continue
|
||||
}
|
||||
|
||||
if (type === "text") {
|
||||
if (part.text?.trim()) return true
|
||||
@@ -31,9 +36,12 @@ function messageHasContentFromSDK(message: SDKMessage): boolean {
|
||||
}
|
||||
|
||||
if (TOOL_TYPES.has(type)) return true
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
// Messages with only thinking/meta parts are NOT empty — they have content
|
||||
return hasIgnoredParts
|
||||
}
|
||||
|
||||
function getSdkMessages(response: unknown): SDKMessage[] {
|
||||
|
||||
@@ -31,10 +31,15 @@ function messageHasContentFromSDK(message: SDKMessage): boolean {
|
||||
const parts = message.parts
|
||||
if (!parts || parts.length === 0) return false
|
||||
|
||||
let hasIgnoredParts = false
|
||||
|
||||
for (const part of parts) {
|
||||
const type = part.type
|
||||
if (!type) continue
|
||||
if (IGNORE_TYPES.has(type)) continue
|
||||
if (IGNORE_TYPES.has(type)) {
|
||||
hasIgnoredParts = true
|
||||
continue
|
||||
}
|
||||
|
||||
if (type === "text") {
|
||||
if (part.text?.trim()) return true
|
||||
@@ -42,9 +47,12 @@ function messageHasContentFromSDK(message: SDKMessage): boolean {
|
||||
}
|
||||
|
||||
if (TOOL_TYPES.has(type)) return true
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
// Messages with only thinking/meta parts are NOT empty — they have content
|
||||
return hasIgnoredParts
|
||||
}
|
||||
|
||||
async function findEmptyMessageIdsFromSDK(
|
||||
|
||||
@@ -24,9 +24,7 @@ interface SDKToolPart {
|
||||
type: string
|
||||
callID?: string
|
||||
tool?: string
|
||||
state?: { output?: string }
|
||||
truncated?: boolean
|
||||
originalSize?: number
|
||||
state?: { output?: string; time?: { compacted?: number } }
|
||||
}
|
||||
|
||||
interface SDKMessage {
|
||||
@@ -120,7 +118,7 @@ async function truncateToolOutputsByCallIdFromSDK(
|
||||
for (const part of msg.parts) {
|
||||
if (part.type !== "tool" || !part.callID) continue
|
||||
if (!callIds.has(part.callID)) continue
|
||||
if (!part.state?.output || part.truncated) continue
|
||||
if (!part.state?.output || part.state?.time?.compacted) continue
|
||||
|
||||
const result = await truncateToolResultAsync(client, sessionID, messageID, part.id, part)
|
||||
if (result.success) {
|
||||
|
||||
@@ -19,8 +19,6 @@ interface SDKToolPart {
|
||||
error?: string
|
||||
time?: { start?: number; end?: number; compacted?: number }
|
||||
}
|
||||
truncated?: boolean
|
||||
originalSize?: number
|
||||
}
|
||||
|
||||
interface SDKMessage {
|
||||
@@ -42,7 +40,7 @@ export async function findToolResultsBySizeFromSDK(
|
||||
if (!messageID || !msg.parts) continue
|
||||
|
||||
for (const part of msg.parts) {
|
||||
if (part.type === "tool" && part.state?.output && !part.truncated && part.tool) {
|
||||
if (part.type === "tool" && part.state?.output && !part.state?.time?.compacted && part.tool) {
|
||||
results.push({
|
||||
partPath: "",
|
||||
partId: part.id,
|
||||
@@ -74,8 +72,6 @@ export async function truncateToolResultAsync(
|
||||
|
||||
const updatedPart: Record<string, unknown> = {
|
||||
...part,
|
||||
truncated: true,
|
||||
originalSize,
|
||||
state: {
|
||||
...part.state,
|
||||
output: TRUNCATION_MESSAGE,
|
||||
@@ -108,7 +104,7 @@ export async function countTruncatedResultsFromSDK(
|
||||
for (const msg of messages) {
|
||||
if (!msg.parts) continue
|
||||
for (const part of msg.parts) {
|
||||
if (part.truncated === true) count++
|
||||
if (part.state?.time?.compacted) count++
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { findMessagesWithThinkingBlocks, stripThinkingParts } from "./storage"
|
||||
import { isSqliteBackend } from "../../shared/opencode-storage-detection"
|
||||
import { stripThinkingPartsAsync } from "./storage/thinking-strip"
|
||||
import { THINKING_TYPES } from "./constants"
|
||||
import { log } from "../../shared/logger"
|
||||
|
||||
type Client = ReturnType<typeof createOpencodeClient>
|
||||
|
||||
@@ -35,31 +36,39 @@ async function recoverThinkingDisabledViolationFromSDK(
|
||||
client: Client,
|
||||
sessionID: string
|
||||
): Promise<boolean> {
|
||||
const response = await client.session.messages({ path: { id: sessionID } })
|
||||
const messages = (response.data ?? []) as MessageData[]
|
||||
try {
|
||||
const response = await client.session.messages({ path: { id: sessionID } })
|
||||
const messages = (response.data ?? []) as MessageData[]
|
||||
|
||||
const messageIDsWithThinking: string[] = []
|
||||
for (const msg of messages) {
|
||||
if (msg.info?.role !== "assistant") continue
|
||||
if (!msg.info?.id) continue
|
||||
if (!msg.parts) continue
|
||||
const messageIDsWithThinking: string[] = []
|
||||
for (const msg of messages) {
|
||||
if (msg.info?.role !== "assistant") continue
|
||||
if (!msg.info?.id) continue
|
||||
if (!msg.parts) continue
|
||||
|
||||
const hasThinking = msg.parts.some((part) => THINKING_TYPES.has(part.type))
|
||||
if (hasThinking) {
|
||||
messageIDsWithThinking.push(msg.info.id)
|
||||
const hasThinking = msg.parts.some((part) => THINKING_TYPES.has(part.type))
|
||||
if (hasThinking) {
|
||||
messageIDsWithThinking.push(msg.info.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (messageIDsWithThinking.length === 0) {
|
||||
if (messageIDsWithThinking.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
let anySuccess = false
|
||||
for (const messageID of messageIDsWithThinking) {
|
||||
if (await stripThinkingPartsAsync(client, sessionID, messageID)) {
|
||||
anySuccess = true
|
||||
}
|
||||
}
|
||||
|
||||
return anySuccess
|
||||
} catch (error) {
|
||||
log("[session-recovery] recoverThinkingDisabledViolationFromSDK failed", {
|
||||
sessionID,
|
||||
error: String(error),
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
let anySuccess = false
|
||||
for (const messageID of messageIDsWithThinking) {
|
||||
if (await stripThinkingPartsAsync(client, sessionID, messageID)) {
|
||||
anySuccess = true
|
||||
}
|
||||
}
|
||||
|
||||
return anySuccess
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ export function prependThinkingPart(sessionID: string, messageID: string): boole
|
||||
|
||||
const previousThinking = findLastThinkingContent(sessionID, messageID)
|
||||
|
||||
const partId = "prt_0000000000_thinking"
|
||||
const partId = `prt_0000000000_${messageID}_thinking`
|
||||
const part = {
|
||||
id: partId,
|
||||
sessionID,
|
||||
@@ -104,7 +104,7 @@ export async function prependThinkingPartAsync(
|
||||
): Promise<boolean> {
|
||||
const previousThinking = await findLastThinkingContentFromSDK(client, sessionID, messageID)
|
||||
|
||||
const partId = "prt_0000000000_thinking"
|
||||
const partId = `prt_0000000000_${messageID}_thinking`
|
||||
const part: Record<string, unknown> = {
|
||||
id: partId,
|
||||
sessionID,
|
||||
|
||||
Reference in New Issue
Block a user