feat(atlas): enhance session-last-agent with timestamp-based ordering
- Sort messages by creation timestamp for accurate last agent detection
- Add fallback to filename sorting for deterministic ordering
- Add JSON backend test coverage
- Update SQLite backend tests for timestamp-aware sorting
🤖 Generated with assistance of OhMyOpenCode
This commit is contained in:
@@ -0,0 +1,67 @@
|
|||||||
|
declare const require: (name: string) => any
|
||||||
|
const { afterEach, describe, expect, mock, test, afterAll } = require("bun:test")
|
||||||
|
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
|
||||||
|
import { join } from "node:path"
|
||||||
|
import { tmpdir } from "node:os"
|
||||||
|
|
||||||
|
const testDirs: string[] = []
|
||||||
|
const TEST_STORAGE_ROOT = join(tmpdir(), `atlas-session-last-agent-${Date.now()}`)
|
||||||
|
const TEST_MESSAGE_STORAGE = join(TEST_STORAGE_ROOT, "message")
|
||||||
|
|
||||||
|
mock.module("../../shared/opencode-storage-detection", () => ({
|
||||||
|
isSqliteBackend: () => false,
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module("../../shared/opencode-message-dir", () => ({
|
||||||
|
getMessageDir: (sessionID: string) => {
|
||||||
|
const directPath = join(TEST_MESSAGE_STORAGE, sessionID)
|
||||||
|
return require("node:fs").existsSync(directPath) ? directPath : null
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
afterAll(() => { mock.restore() })
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
while (testDirs.length > 0) {
|
||||||
|
const directory = testDirs.pop()
|
||||||
|
if (directory) {
|
||||||
|
rmSync(directory, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function createTempMessageDir(sessionID: string): string {
|
||||||
|
const directory = mkdtempSync(join(tmpdir(), "atlas-session-last-agent-json-"))
|
||||||
|
testDirs.push(directory)
|
||||||
|
const messageDir = join(TEST_MESSAGE_STORAGE, sessionID)
|
||||||
|
mkdirSync(messageDir, { recursive: true })
|
||||||
|
return messageDir
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("getLastAgentFromSession JSON backend", () => {
|
||||||
|
test("returns the newest non-compaction agent by message timestamp rather than filename order", async () => {
|
||||||
|
// given
|
||||||
|
const sessionID = "ses_json_last_agent"
|
||||||
|
const messageDir = createTempMessageDir(sessionID)
|
||||||
|
writeFileSync(join(messageDir, "msg_ffff0000_000001.json"), JSON.stringify({
|
||||||
|
agent: "compaction",
|
||||||
|
time: { created: 200 },
|
||||||
|
}), "utf-8")
|
||||||
|
writeFileSync(join(messageDir, "msg_00000000_000999.json"), JSON.stringify({
|
||||||
|
agent: "atlas",
|
||||||
|
time: { created: 100 },
|
||||||
|
}), "utf-8")
|
||||||
|
writeFileSync(join(messageDir, "msg_11111111_000002.json"), JSON.stringify({
|
||||||
|
agent: "sisyphus-junior",
|
||||||
|
time: { created: 50 },
|
||||||
|
}), "utf-8")
|
||||||
|
|
||||||
|
const { getLastAgentFromSession } = await import("./session-last-agent")
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = await getLastAgentFromSession(sessionID)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result).toBe("atlas")
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,56 +1,71 @@
|
|||||||
|
export {}
|
||||||
const { describe, expect, mock, test, afterAll } = require("bun:test")
|
const { describe, expect, mock, test, afterAll } = require("bun:test")
|
||||||
|
|
||||||
|
mock.module("../../shared/opencode-storage-detection", () => ({
|
||||||
|
isSqliteBackend: () => true,
|
||||||
|
}))
|
||||||
|
|
||||||
afterAll(() => { mock.restore() })
|
afterAll(() => { mock.restore() })
|
||||||
|
|
||||||
async function importFreshSessionLastAgentModule() {
|
const { getLastAgentFromSession } = await import("./session-last-agent")
|
||||||
mock.module("../../shared/opencode-message-dir", () => ({
|
|
||||||
getMessageDir: () => null,
|
|
||||||
}))
|
|
||||||
|
|
||||||
mock.module("../../shared/opencode-storage-detection", () => ({
|
describe("getLastAgentFromSession SQLite backend ordering", () => {
|
||||||
isSqliteBackend: () => true,
|
test("returns newest non-compaction agent using time.created and id tie-breaker", async () => {
|
||||||
}))
|
|
||||||
|
|
||||||
const module = await import(`./session-last-agent?test=${Date.now()}-${Math.random()}`)
|
|
||||||
mock.restore()
|
|
||||||
return module
|
|
||||||
}
|
|
||||||
|
|
||||||
const { getLastAgentFromSession } = await importFreshSessionLastAgentModule()
|
|
||||||
|
|
||||||
function createMockClient(messages: Array<{ info?: { agent?: string } }>) {
|
|
||||||
return {
|
|
||||||
session: {
|
|
||||||
messages: async () => ({ data: messages }),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("getLastAgentFromSession sqlite branch", () => {
|
|
||||||
test("should skip compaction and return the previous real agent from sqlite messages", async () => {
|
|
||||||
// given
|
// given
|
||||||
const client = createMockClient([
|
const client = {
|
||||||
{ info: { agent: "atlas" } },
|
session: {
|
||||||
{ info: { agent: "compaction" } },
|
messages: async () => ({
|
||||||
])
|
data: [
|
||||||
|
{ id: "msg_0001", info: { agent: "atlas", time: { created: 100 } } },
|
||||||
|
{ id: "msg_0003", info: { agent: "compaction", time: { created: 200 } } },
|
||||||
|
{ id: "msg_0002", info: { agent: "sisyphus-junior", time: { created: 100 } } },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const result = await getLastAgentFromSession("ses_sqlite_compaction", client)
|
const result = await getLastAgentFromSession("ses_sqlite_last_agent", client as never)
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(result).toBe("atlas")
|
expect(result).toBe("sisyphus-junior")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should return null when sqlite history contains only compaction", async () => {
|
test("handles equal timestamps with random-looking ids deterministically", async () => {
|
||||||
// given
|
// given
|
||||||
const client = createMockClient([{ info: { agent: "compaction" } }])
|
const client = {
|
||||||
|
session: {
|
||||||
|
messages: async () => ({
|
||||||
|
data: [
|
||||||
|
{ id: "msg_a91f00ab", info: { agent: "atlas", time: { created: 100 } } },
|
||||||
|
{ id: "msg_f0e1d2c3", info: { agent: "compaction", time: { created: 200 } } },
|
||||||
|
{ id: "msg_d4c3b2a1", info: { agent: "sisyphus-junior", time: { created: 100 } } },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const result = await getLastAgentFromSession("ses_sqlite_only_compaction", client)
|
const result = await getLastAgentFromSession("ses_sqlite_last_agent_equal_time", client as never)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result).toBe("sisyphus-junior")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns null instead of throwing when SQLite message lookup fails", async () => {
|
||||||
|
// given
|
||||||
|
const client = {
|
||||||
|
session: {
|
||||||
|
messages: async () => {
|
||||||
|
throw new Error("sqlite lookup failed")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = await getLastAgentFromSession("ses_sqlite_error", client as never)
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(result).toBeNull()
|
expect(result).toBeNull()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
export {}
|
|
||||||
|
|||||||
@@ -15,20 +15,27 @@ function isCompactionAgent(agent: unknown): boolean {
|
|||||||
|
|
||||||
function getLastAgentFromMessageDir(messageDir: string): string | null {
|
function getLastAgentFromMessageDir(messageDir: string): string | null {
|
||||||
try {
|
try {
|
||||||
const files = readdirSync(messageDir)
|
const messages = readdirSync(messageDir)
|
||||||
.filter((fileName) => fileName.endsWith(".json"))
|
.filter((fileName) => fileName.endsWith(".json"))
|
||||||
.sort()
|
.map((fileName) => {
|
||||||
|
try {
|
||||||
for (let i = files.length - 1; i >= 0; i--) {
|
const content = readFileSync(join(messageDir, fileName), "utf-8")
|
||||||
const fileName = files[i]
|
const parsed = JSON.parse(content) as { agent?: unknown; time?: { created?: unknown } }
|
||||||
try {
|
return {
|
||||||
const content = readFileSync(join(messageDir, fileName), "utf-8")
|
fileName,
|
||||||
const parsed = JSON.parse(content) as { agent?: unknown }
|
agent: parsed.agent,
|
||||||
if (typeof parsed.agent === "string" && !isCompactionAgent(parsed.agent)) {
|
createdAt: typeof parsed.time?.created === "number" ? parsed.time.created : Number.NEGATIVE_INFINITY,
|
||||||
return parsed.agent.toLowerCase()
|
}
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
} catch {
|
})
|
||||||
continue
|
.filter((message): message is { fileName: string; agent: unknown; createdAt: number } => message !== null)
|
||||||
|
.sort((left, right) => right.createdAt - left.createdAt || right.fileName.localeCompare(left.fileName))
|
||||||
|
|
||||||
|
for (const message of messages) {
|
||||||
|
if (typeof message.agent === "string" && !isCompactionAgent(message.agent)) {
|
||||||
|
return message.agent.toLowerCase()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -43,16 +50,30 @@ export async function getLastAgentFromSession(
|
|||||||
client?: SessionMessagesClient
|
client?: SessionMessagesClient
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
if (isSqliteBackend() && client) {
|
if (isSqliteBackend() && client) {
|
||||||
const response = await client.session.messages({ path: { id: sessionID } })
|
try {
|
||||||
const messages = normalizeSDKResponse(response, [] as Array<{ info?: { agent?: string } }>, {
|
const response = await client.session.messages({ path: { id: sessionID } })
|
||||||
preferResponseOnMissingData: true,
|
const messages = normalizeSDKResponse(response, [] as Array<{ id?: string; info?: { agent?: string; time?: { created?: number } } }>, {
|
||||||
})
|
preferResponseOnMissingData: true,
|
||||||
|
}).sort((left, right) => {
|
||||||
|
const leftTime = (left as { info?: { time?: { created?: number } } }).info?.time?.created ?? Number.NEGATIVE_INFINITY
|
||||||
|
const rightTime = (right as { info?: { time?: { created?: number } } }).info?.time?.created ?? Number.NEGATIVE_INFINITY
|
||||||
|
if (leftTime !== rightTime) {
|
||||||
|
return rightTime - leftTime
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = messages.length - 1; i >= 0; i--) {
|
const leftId = typeof left.id === "string" ? left.id : ""
|
||||||
const agent = messages[i].info?.agent
|
const rightId = typeof right.id === "string" ? right.id : ""
|
||||||
if (typeof agent === "string" && !isCompactionAgent(agent)) {
|
return rightId.localeCompare(leftId)
|
||||||
return agent.toLowerCase()
|
})
|
||||||
|
|
||||||
|
for (const message of messages) {
|
||||||
|
const agent = message.info?.agent
|
||||||
|
if (typeof agent === "string" && !isCompactionAgent(agent)) {
|
||||||
|
return agent.toLowerCase()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
|
|||||||
Reference in New Issue
Block a user