test(boulder-state): batch 44 (4 files)
This commit is contained in:
@@ -166,5 +166,35 @@ describe("TaskHistory", () => {
|
||||
expect(result).not.toContain("\n\n")
|
||||
expect(result).toContain("Line1 Line2 Line3")
|
||||
})
|
||||
|
||||
it("bounds compaction output when many task descriptions are long", () => {
|
||||
//#given
|
||||
const history = new TaskHistory()
|
||||
const longDescription = "Inspect the same lengthy failure context. ".repeat(200)
|
||||
|
||||
//#when
|
||||
for (let i = 0; i < 100; i++) {
|
||||
history.record("parent-1", {
|
||||
id: `t${i}`,
|
||||
agent: "explore",
|
||||
description: `${longDescription} task ${i}`,
|
||||
status: "completed",
|
||||
category: "quick",
|
||||
sessionID: `ses_child_${i}`,
|
||||
})
|
||||
}
|
||||
const result = history.formatForCompaction("parent-1")
|
||||
|
||||
//#then
|
||||
expect(result).not.toBeNull()
|
||||
if (result === null) {
|
||||
throw new Error("Expected compaction history")
|
||||
}
|
||||
expect(result.length).toBeLessThanOrEqual(6_000)
|
||||
expect(result).toContain("older delegated sessions omitted")
|
||||
expect(result).toContain("[truncated]")
|
||||
expect(result).toContain("`t99`")
|
||||
expect(result).not.toContain("`t0`")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { BackgroundTaskStatus } from "./types"
|
||||
|
||||
const MAX_ENTRIES_PER_PARENT = 100
|
||||
const MAX_COMPACTION_ENTRIES = 20
|
||||
const MAX_COMPACTION_DESCRIPTION_CHARS = 240
|
||||
const MAX_COMPACTION_TOTAL_CHARS = 6_000
|
||||
|
||||
export interface TaskHistoryEntry {
|
||||
id: string
|
||||
@@ -62,18 +65,80 @@ export class TaskHistory {
|
||||
const list = this.getByParentSession(parentSessionID)
|
||||
if (list.length === 0) return null
|
||||
|
||||
const lines = list.map((e) => {
|
||||
const desc = e.description?.replace(/[\n\r]+/g, " ").trim() ?? ""
|
||||
const parts = [
|
||||
`- **${e.agent}**`,
|
||||
e.category ? `[${e.category}]` : null,
|
||||
`(${e.status})`,
|
||||
`: ${desc}`,
|
||||
e.sessionID ? ` | session: \`${e.sessionID}\`` : null,
|
||||
]
|
||||
return parts.filter(Boolean).join("")
|
||||
})
|
||||
const recent = list.slice(-MAX_COMPACTION_ENTRIES)
|
||||
const olderOmittedCount = list.length - recent.length
|
||||
const lines: string[] = []
|
||||
|
||||
if (olderOmittedCount > 0) {
|
||||
lines.push(`- ${olderOmittedCount} older delegated sessions omitted from compaction summary.`)
|
||||
}
|
||||
|
||||
let budgetOmittedCount = 0
|
||||
for (let i = recent.length - 1; i >= 0; i--) {
|
||||
const entry = recent[i]
|
||||
if (!entry) continue
|
||||
|
||||
const line = formatCompactionEntry(entry)
|
||||
if (!appendWithinBudget(lines, line, MAX_COMPACTION_TOTAL_CHARS)) {
|
||||
budgetOmittedCount = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (budgetOmittedCount > 0) {
|
||||
appendBudgetSummary(lines, budgetOmittedCount)
|
||||
}
|
||||
|
||||
return lines.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
function formatCompactionEntry(entry: TaskHistoryEntry): string {
|
||||
const description = compactInline(entry.description, MAX_COMPACTION_DESCRIPTION_CHARS)
|
||||
const parts = [
|
||||
`- **${compactInline(entry.agent, 80)}**`,
|
||||
entry.category ? `[${compactInline(entry.category, 60)}]` : "",
|
||||
`(${entry.status})`,
|
||||
` task_id: \`${compactInline(entry.id, 120)}\``,
|
||||
description ? `: ${description}` : "",
|
||||
entry.sessionID ? ` | session: \`${compactInline(entry.sessionID, 120)}\`` : "",
|
||||
]
|
||||
return parts.filter((part) => part.length > 0).join("")
|
||||
}
|
||||
|
||||
function compactInline(value: string, maxChars: number): string {
|
||||
const normalized = value.replace(/[\n\r]+/g, " ").replace(/\s+/g, " ").replace(/`/g, "'").trim()
|
||||
if (normalized.length <= maxChars) {
|
||||
return normalized
|
||||
}
|
||||
|
||||
const suffix = "... [truncated]"
|
||||
const keepChars = Math.max(0, maxChars - suffix.length)
|
||||
return `${normalized.slice(0, keepChars).trimEnd()}${suffix}`
|
||||
}
|
||||
|
||||
function appendWithinBudget(lines: string[], line: string, maxChars: number): boolean {
|
||||
const currentLength = joinedLength(lines)
|
||||
const separatorLength = lines.length === 0 ? 0 : 1
|
||||
if (currentLength + separatorLength + line.length > maxChars) {
|
||||
return false
|
||||
}
|
||||
|
||||
lines.push(line)
|
||||
return true
|
||||
}
|
||||
|
||||
function appendBudgetSummary(lines: string[], omittedCount: number): void {
|
||||
const summary = `- ${omittedCount} delegated sessions omitted to stay within compaction budget.`
|
||||
if (appendWithinBudget(lines, summary, MAX_COMPACTION_TOTAL_CHARS)) {
|
||||
return
|
||||
}
|
||||
|
||||
while (lines.length > 0 && !appendWithinBudget(lines, summary, MAX_COMPACTION_TOTAL_CHARS)) {
|
||||
lines.pop()
|
||||
}
|
||||
}
|
||||
|
||||
function joinedLength(lines: readonly string[]): number {
|
||||
return lines.reduce((total, line, index) => total + line.length + (index === 0 ? 0 : 1), 0)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
getPlanProgress,
|
||||
getPlanName,
|
||||
createBoulderState,
|
||||
findPrometheusPlans,
|
||||
getTaskSessionState,
|
||||
resolveBoulderPlanPath,
|
||||
resolveBoulderPlanPathForWork,
|
||||
@@ -74,7 +73,7 @@ describe("boulder-state", () => {
|
||||
expect(writeSucceeded).toBe(true)
|
||||
expect(roundTripState?.active_plan).toBe(legacyRawState.active_plan)
|
||||
expect(roundTripState?.started_at).toBe(legacyRawState.started_at)
|
||||
expect(roundTripState?.session_ids).toEqual(legacyRawState.session_ids)
|
||||
expect(roundTripState?.session_ids).toEqual(["opencode:legacy-session"])
|
||||
expect(roundTripState?.plan_name).toBe(legacyRawState.plan_name)
|
||||
})
|
||||
|
||||
@@ -172,7 +171,7 @@ describe("boulder-state", () => {
|
||||
const result = readBoulderState(TEST_DIR)
|
||||
|
||||
// then
|
||||
expect(result?.session_origins).toEqual({ "session-1": "direct" })
|
||||
expect(result?.session_origins).toEqual({ "opencode:session-1": "direct" })
|
||||
})
|
||||
|
||||
test("should keep missing origins empty when multiple sessions are tracked", () => {
|
||||
@@ -207,7 +206,7 @@ describe("boulder-state", () => {
|
||||
// then
|
||||
expect(result).not.toBeNull()
|
||||
expect(result?.active_plan).toBe("/path/to/plan.md")
|
||||
expect(result?.session_ids).toEqual(["session-1", "session-2"])
|
||||
expect(result?.session_ids).toEqual(["opencode:session-1", "opencode:session-2"])
|
||||
expect(result?.plan_name).toBe("my-plan")
|
||||
})
|
||||
|
||||
@@ -267,7 +266,7 @@ describe("boulder-state", () => {
|
||||
|
||||
// then
|
||||
expect(result).not.toBeNull()
|
||||
expect(result?.session_ids).toEqual(["session-1", "session-2"])
|
||||
expect(result?.session_ids).toEqual(["opencode:session-1", "opencode:session-2"])
|
||||
})
|
||||
|
||||
test("should not duplicate existing session id", () => {
|
||||
@@ -285,7 +284,7 @@ describe("boulder-state", () => {
|
||||
const result = readBoulderState(TEST_DIR)
|
||||
|
||||
// then
|
||||
expect(result?.session_ids).toEqual(["session-1"])
|
||||
expect(result?.session_ids).toEqual(["opencode:session-1"])
|
||||
})
|
||||
|
||||
test("should return null when no state exists", () => {
|
||||
@@ -310,7 +309,7 @@ describe("boulder-state", () => {
|
||||
|
||||
//#then - should not crash and should contain the new session
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.session_ids).toContain("ses-new")
|
||||
expect(result!.session_ids).toContain("opencode:ses-new")
|
||||
})
|
||||
|
||||
test("should persist appended session origin when provided", () => {
|
||||
@@ -328,8 +327,8 @@ describe("boulder-state", () => {
|
||||
|
||||
// then
|
||||
expect(result?.session_origins).toEqual({
|
||||
"session-1": "direct",
|
||||
"session-2": "appended",
|
||||
"opencode:session-1": "direct",
|
||||
"opencode:session-2": "appended",
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -387,7 +386,7 @@ describe("boulder-state", () => {
|
||||
|
||||
// then
|
||||
expect(result).not.toBeNull()
|
||||
expect(result?.session_id).toBe("ses_task_123")
|
||||
expect(result?.session_id).toBe("opencode:ses_task_123")
|
||||
expect(result?.task_title).toBe("Implement auth flow")
|
||||
expect(result?.agent).toBe("sisyphus-junior")
|
||||
expect(result?.category).toBe("deep")
|
||||
@@ -422,7 +421,7 @@ describe("boulder-state", () => {
|
||||
const result = getTaskSessionState(TEST_DIR, "todo:1")
|
||||
|
||||
// then
|
||||
expect(result?.session_id).toBe("ses_new")
|
||||
expect(result?.session_id).toBe("opencode:ses_new")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -542,7 +541,7 @@ describe("boulder-state", () => {
|
||||
// then
|
||||
expect(updated).not.toBeNull()
|
||||
const taskSession = updated?.works?.[workId]?.task_sessions?.["todo:1"]
|
||||
expect(taskSession?.session_id).toBe("task-session-b")
|
||||
expect(taskSession?.session_id).toBe("opencode:task-session-b")
|
||||
expect(taskSession?.started_at).toBe("2026-01-01T00:00:00.000Z")
|
||||
})
|
||||
})
|
||||
@@ -993,7 +992,7 @@ describe("boulder-state", () => {
|
||||
|
||||
// then
|
||||
expect(state.active_plan).toBe(planPath)
|
||||
expect(state.session_ids).toEqual([sessionId])
|
||||
expect(state.session_ids).toEqual(["opencode:ses-abc123"])
|
||||
expect(state.plan_name).toBe("auth-refactor")
|
||||
expect(state.started_at).toBeDefined()
|
||||
})
|
||||
@@ -1010,7 +1009,7 @@ describe("boulder-state", () => {
|
||||
//#then - state should include the agent field
|
||||
expect(state.agent).toBe("atlas")
|
||||
expect(state.active_plan).toBe(planPath)
|
||||
expect(state.session_ids).toEqual([sessionId])
|
||||
expect(state.session_ids).toEqual(["opencode:ses-xyz789"])
|
||||
expect(state.plan_name).toBe("feature")
|
||||
})
|
||||
|
||||
@@ -1023,7 +1022,7 @@ describe("boulder-state", () => {
|
||||
const state = createBoulderState(planPath, sessionId)
|
||||
|
||||
// then
|
||||
expect(state.session_origins).toEqual({ [sessionId]: "direct" })
|
||||
expect(state.session_origins).toEqual({ "opencode:ses-origin": "direct" })
|
||||
})
|
||||
|
||||
test("should allow agent to be undefined", () => {
|
||||
@@ -1080,4 +1079,79 @@ describe("boulder-state", () => {
|
||||
expect(resolvedPath).toBe(planPath)
|
||||
})
|
||||
})
|
||||
|
||||
describe("platform-prefixed session ids", () => {
|
||||
test("#given a fresh state with raw session id #when read back #then opencode prefix is stored", () => {
|
||||
// given
|
||||
const planPath = join(TEST_DIR, ".omo", "plans", "raw-session.md")
|
||||
|
||||
// when
|
||||
const state = createBoulderState(planPath, "raw-sess", "atlas", undefined)
|
||||
writeBoulderState(TEST_DIR, state)
|
||||
const readBack = readBoulderState(TEST_DIR)
|
||||
|
||||
// then
|
||||
expect(readBack?.session_ids).toEqual(["opencode:raw-sess"])
|
||||
})
|
||||
|
||||
test("#given a fresh state with codex session id #when read back #then codex prefix is preserved", () => {
|
||||
// given
|
||||
const planPath = join(TEST_DIR, ".omo", "plans", "codex-session.md")
|
||||
|
||||
// when
|
||||
const state = createBoulderState(planPath, "codex:raw-sess", "atlas", undefined)
|
||||
writeBoulderState(TEST_DIR, state)
|
||||
const readBack = readBoulderState(TEST_DIR)
|
||||
|
||||
// then
|
||||
expect(readBack?.session_ids).toEqual(["codex:raw-sess"])
|
||||
})
|
||||
|
||||
test("#given a legacy boulder file with bare session id #when read #then opencode prefix is migrated", () => {
|
||||
// given
|
||||
const boulderFile = join(OMO_DIR, "boulder.json")
|
||||
writeFileSync(boulderFile, JSON.stringify({
|
||||
active_plan: "/path/to/legacy.md",
|
||||
started_at: "2026-01-01T00:00:00Z",
|
||||
session_ids: ["legacy-bare-id"],
|
||||
plan_name: "legacy",
|
||||
}))
|
||||
|
||||
// when
|
||||
const state = readBoulderState(TEST_DIR)
|
||||
|
||||
// then
|
||||
expect(state?.session_ids).toEqual(["opencode:legacy-bare-id"])
|
||||
})
|
||||
|
||||
test("#given existing prefixed session #when appending raw session #then appended id receives opencode prefix", () => {
|
||||
// given
|
||||
writeBoulderState(TEST_DIR, {
|
||||
active_plan: "/path/to/plan.md",
|
||||
started_at: "2026-01-01T00:00:00Z",
|
||||
session_ids: ["opencode:first"],
|
||||
plan_name: "plan",
|
||||
})
|
||||
|
||||
// when
|
||||
appendSessionId(TEST_DIR, "another-raw")
|
||||
const state = readBoulderState(TEST_DIR)
|
||||
|
||||
// then
|
||||
expect(state?.session_ids).toEqual(["opencode:first", "opencode:another-raw"])
|
||||
})
|
||||
|
||||
test("#given stored work with prefixed session #when looking up by raw id #then matching work is returned", () => {
|
||||
// given
|
||||
const planPath = join(TEST_DIR, ".omo", "plans", "lookup.md")
|
||||
const state = createBoulderState(planPath, "opencode:raw-id", "atlas", undefined)
|
||||
writeBoulderState(TEST_DIR, state)
|
||||
|
||||
// when
|
||||
const work = getWorkForSession(TEST_DIR, "raw-id")
|
||||
|
||||
// then
|
||||
expect(work?.session_ids).toEqual(["opencode:raw-id"])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -18,6 +18,7 @@ export {
|
||||
getWorkByPlanName,
|
||||
getWorkForSession,
|
||||
getWorkResumeOptions,
|
||||
normalizeSessionId,
|
||||
readBoulderState,
|
||||
resolveBoulderPlanPath,
|
||||
resolveBoulderPlanPathForWork,
|
||||
|
||||
Reference in New Issue
Block a user