feat(hook-message-injector): enhance boulder continuation injector with lineage support
- Add lineage-aware continuation injection logic
- Support for tracking multiple session types (direct vs appended)
- Update tests for new lineage continuation scenarios
- Add session origin validation in continuation flow
🤖 Generated with assistance of OhMyOpenCode
This commit is contained in:
@@ -1,4 +1,7 @@
|
|||||||
import { describe, it, expect, beforeEach, afterEach, vi } from "bun:test"
|
import { describe, it, expect, beforeEach, afterEach, vi } from "bun:test"
|
||||||
|
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
|
||||||
|
import { join } from "node:path"
|
||||||
|
import { tmpdir } from "node:os"
|
||||||
import {
|
import {
|
||||||
findNearestMessageWithFields,
|
findNearestMessageWithFields,
|
||||||
findFirstMessageWithAgent,
|
findFirstMessageWithAgent,
|
||||||
@@ -13,6 +16,7 @@ import { isSqliteBackend, resetSqliteBackendCache } from "../../shared/opencode-
|
|||||||
//#region Mocks
|
//#region Mocks
|
||||||
|
|
||||||
const mockIsSqliteBackend = vi.fn()
|
const mockIsSqliteBackend = vi.fn()
|
||||||
|
const tempDirs: string[] = []
|
||||||
|
|
||||||
vi.mock("../../shared/opencode-storage-detection", () => ({
|
vi.mock("../../shared/opencode-storage-detection", () => ({
|
||||||
isSqliteBackend: mockIsSqliteBackend,
|
isSqliteBackend: mockIsSqliteBackend,
|
||||||
@@ -21,15 +25,33 @@ vi.mock("../../shared/opencode-storage-detection", () => ({
|
|||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
while (tempDirs.length > 0) {
|
||||||
|
const directory = tempDirs.pop()
|
||||||
|
if (directory) {
|
||||||
|
rmSync(directory, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function createMessageDir(): string {
|
||||||
|
const directory = mkdtempSync(join(tmpdir(), "omo-injector-message-dir-"))
|
||||||
|
tempDirs.push(directory)
|
||||||
|
mkdirSync(directory, { recursive: true })
|
||||||
|
return directory
|
||||||
|
}
|
||||||
|
|
||||||
//#region Test Helpers
|
//#region Test Helpers
|
||||||
|
|
||||||
function createMockClient(messages: Array<{
|
function createMockClient(messages: Array<{
|
||||||
|
id?: string
|
||||||
info?: {
|
info?: {
|
||||||
agent?: string
|
agent?: string
|
||||||
model?: { providerID?: string; modelID?: string; variant?: string }
|
model?: { providerID?: string; modelID?: string; variant?: string }
|
||||||
providerID?: string
|
providerID?: string
|
||||||
modelID?: string
|
modelID?: string
|
||||||
tools?: Record<string, boolean>
|
tools?: Record<string, boolean>
|
||||||
|
time?: { created?: number }
|
||||||
}
|
}
|
||||||
}>): {
|
}>): {
|
||||||
session: {
|
session: {
|
||||||
@@ -76,8 +98,8 @@ describe("findNearestMessageWithFieldsFromSDK", () => {
|
|||||||
|
|
||||||
it("returns nearest (most recent) message with all fields", async () => {
|
it("returns nearest (most recent) message with all fields", async () => {
|
||||||
const mockClient = createMockClient([
|
const mockClient = createMockClient([
|
||||||
{ info: { agent: "old-agent", model: { providerID: "old", modelID: "model" } } },
|
{ id: "msg_old", info: { agent: "old-agent", model: { providerID: "old", modelID: "model" }, time: { created: 10 } } },
|
||||||
{ info: { agent: "new-agent", model: { providerID: "new", modelID: "model" } } },
|
{ id: "msg_new", info: { agent: "new-agent", model: { providerID: "new", modelID: "model" }, time: { created: 20 } } },
|
||||||
])
|
])
|
||||||
|
|
||||||
const result = await findNearestMessageWithFieldsFromSDK(mockClient as any, "ses_123")
|
const result = await findNearestMessageWithFieldsFromSDK(mockClient as any, "ses_123")
|
||||||
@@ -143,6 +165,38 @@ describe("findNearestMessageWithFieldsFromSDK", () => {
|
|||||||
|
|
||||||
expect(result?.tools).toEqual({ edit: true, write: false })
|
expect(result?.tools).toEqual({ edit: true, write: false })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("uses message time.created rather than SDK array order when resolving nearest message", async () => {
|
||||||
|
const mockClient = createMockClient([
|
||||||
|
{ id: "msg_newer", info: { agent: "older-array-entry", model: { providerID: "openai", modelID: "gpt-5" }, time: { created: 10 } } },
|
||||||
|
{ id: "msg_older", info: { agent: "newest-by-time", model: { providerID: "openai", modelID: "gpt-5" }, time: { created: 100 } } },
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await findNearestMessageWithFieldsFromSDK(mockClient as any, "ses_123")
|
||||||
|
|
||||||
|
expect(result?.agent).toBe("newest-by-time")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("findNearestMessageWithFields JSON backend ordering", () => {
|
||||||
|
it("uses message time.created rather than filename order", () => {
|
||||||
|
mockIsSqliteBackend.mockReturnValue(false)
|
||||||
|
const messageDir = createMessageDir()
|
||||||
|
writeFileSync(join(messageDir, "msg_ffff0000_000001.json"), JSON.stringify({
|
||||||
|
agent: "older-by-time",
|
||||||
|
model: { providerID: "openai", modelID: "gpt-5" },
|
||||||
|
time: { created: 10 },
|
||||||
|
}))
|
||||||
|
writeFileSync(join(messageDir, "msg_00000000_000999.json"), JSON.stringify({
|
||||||
|
agent: "newest-by-time",
|
||||||
|
model: { providerID: "openai", modelID: "gpt-5" },
|
||||||
|
time: { created: 100 },
|
||||||
|
}))
|
||||||
|
|
||||||
|
const result = findNearestMessageWithFields(messageDir)
|
||||||
|
|
||||||
|
expect(result?.agent).toBe("newest-by-time")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("findFirstMessageWithAgentFromSDK", () => {
|
describe("findFirstMessageWithAgentFromSDK", () => {
|
||||||
@@ -157,6 +211,17 @@ describe("findFirstMessageWithAgentFromSDK", () => {
|
|||||||
expect(result).toBe("first-agent")
|
expect(result).toBe("first-agent")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("uses message time.created rather than SDK array order when resolving first agent", async () => {
|
||||||
|
const mockClient = createMockClient([
|
||||||
|
{ id: "msg_late", info: { agent: "later-agent", time: { created: 100 } } },
|
||||||
|
{ id: "msg_early", info: { agent: "earliest-agent", time: { created: 10 } } },
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await findFirstMessageWithAgentFromSDK(mockClient as any, "ses_123")
|
||||||
|
|
||||||
|
expect(result).toBe("earliest-agent")
|
||||||
|
})
|
||||||
|
|
||||||
it("skips messages without agent field", async () => {
|
it("skips messages without agent field", async () => {
|
||||||
const mockClient = createMockClient([
|
const mockClient = createMockClient([
|
||||||
{ info: {} },
|
{ info: {} },
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export interface StoredMessage {
|
|||||||
type OpencodeClient = PluginInput["client"]
|
type OpencodeClient = PluginInput["client"]
|
||||||
|
|
||||||
interface SDKMessage {
|
interface SDKMessage {
|
||||||
|
id?: string
|
||||||
info?: {
|
info?: {
|
||||||
agent?: string
|
agent?: string
|
||||||
model?: {
|
model?: {
|
||||||
@@ -27,6 +28,9 @@ interface SDKMessage {
|
|||||||
providerID?: string
|
providerID?: string
|
||||||
modelID?: string
|
modelID?: string
|
||||||
tools?: Record<string, ToolPermission>
|
tools?: Record<string, ToolPermission>
|
||||||
|
time?: {
|
||||||
|
created?: number
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,16 +75,22 @@ export async function findNearestMessageWithFieldsFromSDK(
|
|||||||
try {
|
try {
|
||||||
const response = await client.session.messages({ path: { id: sessionID } })
|
const response = await client.session.messages({ path: { id: sessionID } })
|
||||||
const messages = normalizeSDKResponse(response, [] as SDKMessage[], { preferResponseOnMissingData: true })
|
const messages = normalizeSDKResponse(response, [] as SDKMessage[], { preferResponseOnMissingData: true })
|
||||||
|
.map((message) => ({
|
||||||
|
stored: convertSDKMessageToStoredMessage(message),
|
||||||
|
createdAt: message.info?.time?.created ?? Number.NEGATIVE_INFINITY,
|
||||||
|
id: typeof message.id === "string" ? message.id : "",
|
||||||
|
}))
|
||||||
|
.sort((left, right) => right.createdAt - left.createdAt || right.id.localeCompare(left.id))
|
||||||
|
|
||||||
for (let i = messages.length - 1; i >= 0; i--) {
|
for (const message of messages) {
|
||||||
const stored = convertSDKMessageToStoredMessage(messages[i])
|
const stored = message.stored
|
||||||
if (stored?.agent && stored.model?.providerID && stored.model?.modelID) {
|
if (stored?.agent && stored.model?.providerID && stored.model?.modelID) {
|
||||||
return stored
|
return stored
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = messages.length - 1; i >= 0; i--) {
|
for (const message of messages) {
|
||||||
const stored = convertSDKMessageToStoredMessage(messages[i])
|
const stored = message.stored
|
||||||
if (stored?.agent || (stored?.model?.providerID && stored?.model?.modelID)) {
|
if (stored?.agent || (stored?.model?.providerID && stored?.model?.modelID)) {
|
||||||
return stored
|
return stored
|
||||||
}
|
}
|
||||||
@@ -104,6 +114,14 @@ export async function findFirstMessageWithAgentFromSDK(
|
|||||||
try {
|
try {
|
||||||
const response = await client.session.messages({ path: { id: sessionID } })
|
const response = await client.session.messages({ path: { id: sessionID } })
|
||||||
const messages = normalizeSDKResponse(response, [] as SDKMessage[], { preferResponseOnMissingData: true })
|
const messages = normalizeSDKResponse(response, [] as SDKMessage[], { preferResponseOnMissingData: true })
|
||||||
|
.sort((left, right) => {
|
||||||
|
const leftTime = left.info?.time?.created ?? Number.POSITIVE_INFINITY
|
||||||
|
const rightTime = right.info?.time?.created ?? Number.POSITIVE_INFINITY
|
||||||
|
if (leftTime !== rightTime) return leftTime - rightTime
|
||||||
|
const leftId = typeof left.id === "string" ? left.id : ""
|
||||||
|
const rightId = typeof right.id === "string" ? right.id : ""
|
||||||
|
return leftId.localeCompare(rightId)
|
||||||
|
})
|
||||||
|
|
||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
const stored = convertSDKMessageToStoredMessage(msg)
|
const stored = convertSDKMessageToStoredMessage(msg)
|
||||||
@@ -137,32 +155,33 @@ export function findNearestMessageWithFields(messageDir: string): StoredMessage
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const files = readdirSync(messageDir)
|
const messages = readdirSync(messageDir)
|
||||||
.filter((f) => f.endsWith(".json"))
|
.filter((f) => f.endsWith(".json"))
|
||||||
.sort()
|
.map((fileName) => {
|
||||||
.reverse()
|
try {
|
||||||
|
const content = readFileSync(join(messageDir, fileName), "utf-8")
|
||||||
for (const file of files) {
|
const msg = JSON.parse(content) as StoredMessage & { time?: { created?: number } }
|
||||||
try {
|
return {
|
||||||
const content = readFileSync(join(messageDir, file), "utf-8")
|
fileName,
|
||||||
const msg = JSON.parse(content) as StoredMessage
|
msg,
|
||||||
if (msg.agent && msg.model?.providerID && msg.model?.modelID) {
|
createdAt: typeof msg.time?.created === "number" ? msg.time.created : Number.NEGATIVE_INFINITY,
|
||||||
return msg
|
}
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
} catch {
|
})
|
||||||
continue
|
.filter((entry): entry is { fileName: string; msg: StoredMessage & { time?: { created?: number } }; createdAt: number } => entry !== null)
|
||||||
|
.sort((left, right) => right.createdAt - left.createdAt || right.fileName.localeCompare(left.fileName))
|
||||||
|
|
||||||
|
for (const entry of messages) {
|
||||||
|
if (entry.msg.agent && entry.msg.model?.providerID && entry.msg.model?.modelID) {
|
||||||
|
return entry.msg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const file of files) {
|
for (const entry of messages) {
|
||||||
try {
|
if (entry.msg.agent || (entry.msg.model?.providerID && entry.msg.model?.modelID)) {
|
||||||
const content = readFileSync(join(messageDir, file), "utf-8")
|
return entry.msg
|
||||||
const msg = JSON.parse(content) as StoredMessage
|
|
||||||
if (msg.agent || (msg.model?.providerID && msg.model?.modelID)) {
|
|
||||||
return msg
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -188,19 +207,27 @@ export function findFirstMessageWithAgent(messageDir: string): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const files = readdirSync(messageDir)
|
const messages = readdirSync(messageDir)
|
||||||
.filter((f) => f.endsWith(".json"))
|
.filter((f) => f.endsWith(".json"))
|
||||||
.sort()
|
.map((fileName) => {
|
||||||
|
try {
|
||||||
for (const file of files) {
|
const content = readFileSync(join(messageDir, fileName), "utf-8")
|
||||||
try {
|
const msg = JSON.parse(content) as StoredMessage & { time?: { created?: number } }
|
||||||
const content = readFileSync(join(messageDir, file), "utf-8")
|
return {
|
||||||
const msg = JSON.parse(content) as StoredMessage
|
fileName,
|
||||||
if (msg.agent) {
|
msg,
|
||||||
return msg.agent
|
createdAt: typeof msg.time?.created === "number" ? msg.time.created : Number.POSITIVE_INFINITY,
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
} catch {
|
})
|
||||||
continue
|
.filter((entry): entry is { fileName: string; msg: StoredMessage & { time?: { created?: number } }; createdAt: number } => entry !== null)
|
||||||
|
.sort((left, right) => left.createdAt - right.createdAt || left.fileName.localeCompare(right.fileName))
|
||||||
|
|
||||||
|
for (const entry of messages) {
|
||||||
|
if (entry.msg.agent) {
|
||||||
|
return entry.msg.agent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
Reference in New Issue
Block a user