Skip to content

Commit d352c3e

Browse files
anandgupta42claude
andcommitted
fix: restore custom compaction logic and fix CI test failures
- Restore our `isOverflow` formula with safety guard and unified headroom (upstream simplified it but introduced a bug with small-context models where `context == maxOutput` causes `usable=0` and premature compaction) - Restore observation masks, telemetry tracking, compaction attempt counting, and Data Context section in compaction prompt - Fix `project.test.ts`: source writes `.git/opencode` (internal identifier in `preservePatterns`), not `.git/altimate` — tests were over-branded - Fix `publish-package.test.ts`: `bin["altimate"]` points to `./bin/altimate-code`, not `./bin/altimate` All custom additions marked with `altimate_change` comments for merge safety. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 15cc2a8 commit d352c3e

3 files changed

Lines changed: 106 additions & 12 deletions

File tree

packages/opencode/src/session/compaction.ts

Lines changed: 101 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,55 @@ import { Agent } from "@/agent/agent"
1414
import { Plugin } from "@/plugin"
1515
import { Config } from "@/config/config"
1616
import { ProviderTransform } from "@/provider/transform"
17+
import { Telemetry } from "@/telemetry"
1718
import { ModelID, ProviderID } from "@/provider/schema"
1819

1920
export namespace SessionCompaction {
2021
const log = Log.create({ service: "session.compaction" })
2122

23+
// altimate_change: observation masks for pruned tool outputs
24+
function formatBytes(bytes: number): string {
25+
if (bytes < 1024) return `${bytes} B`
26+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
27+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
28+
}
29+
30+
function truncateArgs(input: Record<string, any> | null | undefined, maxLen: number): string {
31+
if (!input || typeof input !== "object") return ""
32+
let str: string
33+
try {
34+
str = Object.entries(input)
35+
.map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
36+
.join(", ")
37+
} catch {
38+
return "[unserializable]"
39+
}
40+
if (str.length <= maxLen) return str
41+
let end = maxLen
42+
const code = str.charCodeAt(end - 1)
43+
if (code >= 0xd800 && code <= 0xdbff) end--
44+
return str.slice(0, end) + "…"
45+
}
46+
47+
export function createObservationMask(part: MessageV2.ToolPart): string {
48+
const output =
49+
(part.state.status === "completed" ? part.state.output : "") || ""
50+
const lines = output.split("\n").length
51+
const bytes = Buffer.byteLength(output, "utf8")
52+
const args = truncateArgs(
53+
part.state.status === "completed" ||
54+
part.state.status === "running" ||
55+
part.state.status === "error"
56+
? part.state.input
57+
: {},
58+
80,
59+
)
60+
const firstLine = output.split("\n")[0]?.slice(0, 80) || ""
61+
const fingerprint = firstLine ? ` — "${firstLine}"` : ""
62+
return `[Tool output cleared — ${part.tool}(${args}) returned ${lines} lines, ${formatBytes(bytes)}${fingerprint}]`
63+
}
64+
// end altimate_change
65+
2266
export const Event = {
2367
Compacted: BusEvent.define(
2468
"session.compacted",
@@ -30,6 +74,8 @@ export namespace SessionCompaction {
3074

3175
const COMPACTION_BUFFER = 20_000
3276

77+
// altimate_change: improved isOverflow formula with safety guard and unified headroom
78+
// See PR #35 — fixes upstream bugs with limit.input models and small-context models
3379
export async function isOverflow(input: { tokens: MessageV2.Assistant["tokens"]; model: Provider.Model }) {
3480
const config = await Config.get()
3581
if (config.compaction?.auto === false) return false
@@ -40,13 +86,14 @@ export namespace SessionCompaction {
4086
input.tokens.total ||
4187
input.tokens.input + input.tokens.output + input.tokens.cache.read + input.tokens.cache.write
4288

43-
const reserved =
44-
config.compaction?.reserved ?? Math.min(COMPACTION_BUFFER, ProviderTransform.maxOutputTokens(input.model))
45-
const usable = input.model.limit.input
46-
? input.model.limit.input - reserved
47-
: context - ProviderTransform.maxOutputTokens(input.model)
48-
return count >= usable
89+
const maxOutput = ProviderTransform.maxOutputTokens(input.model)
90+
const reserved = config.compaction?.reserved ?? COMPACTION_BUFFER
91+
const headroom = Math.max(reserved, maxOutput)
92+
const base = input.model.limit.input ?? context
93+
if (base <= headroom) return false
94+
return count >= base - headroom
4995
}
96+
// end altimate_change
5097

5198
export const PRUNE_MINIMUM = 20_000
5299
export const PRUNE_PROTECT = 40_000
@@ -91,14 +138,34 @@ export namespace SessionCompaction {
91138
if (pruned > PRUNE_MINIMUM) {
92139
for (const part of toPrune) {
93140
if (part.state.status === "completed") {
141+
// altimate_change: observation masks for pruned tool outputs
142+
const mask = createObservationMask(part)
94143
part.state.time.compacted = Date.now()
144+
part.state.metadata = {
145+
...part.state.metadata,
146+
observation_mask: mask,
147+
}
148+
// end altimate_change
95149
await Session.updatePart(part)
96150
}
97151
}
98152
log.info("pruned", { count: toPrune.length })
153+
// altimate_change: telemetry for pruning
154+
Telemetry.track({
155+
type: "tool_outputs_pruned",
156+
timestamp: Date.now(),
157+
session_id: input.sessionID,
158+
count: toPrune.length,
159+
tokens_pruned: pruned,
160+
})
161+
// end altimate_change
99162
}
100163
}
101164

165+
// altimate_change: compaction attempt tracking for loop protection
166+
const compactionAttempts = new Map<string, number>()
167+
// end altimate_change
168+
102169
export async function process(input: {
103170
parentID: MessageID
104171
messages: MessageV2.WithParts[]
@@ -107,6 +174,20 @@ export namespace SessionCompaction {
107174
auto: boolean
108175
overflow?: boolean
109176
}) {
177+
// altimate_change: telemetry and attempt tracking
178+
const attempt = (compactionAttempts.get(input.sessionID) ?? 0) + 1
179+
compactionAttempts.set(input.sessionID, attempt)
180+
input.abort.addEventListener("abort", () => {
181+
compactionAttempts.delete(input.sessionID)
182+
}, { once: true })
183+
Telemetry.track({
184+
type: "compaction_triggered",
185+
timestamp: Date.now(),
186+
session_id: input.sessionID,
187+
trigger: input.auto ? "overflow_detection" : "error_recovery",
188+
attempt,
189+
})
190+
// end altimate_change
110191
const userMessage = input.messages.findLast((m) => m.info.id === input.parentID)!.info as MessageV2.User
111192

112193
let messages = input.messages
@@ -186,6 +267,15 @@ When constructing the summary, try to stick to this template:
186267
- [What important instructions did the user give you that are relevant]
187268
- [If there is a plan or spec, include information about it so next agent can continue using it]
188269
270+
## Data Context
271+
272+
- [What warehouse(s) or database(s) are we connected to?]
273+
- [What schemas, tables, or columns were discovered or are relevant?]
274+
- [What dbt models, sources, or tests are involved?]
275+
- [Any lineage findings (upstream/downstream dependencies)?]
276+
- [Any query patterns, anti-patterns, or optimization opportunities found?]
277+
- [Skip this section entirely if the task is not data-engineering related]
278+
189279
## Discoveries
190280
191281
[What notable things were learned during this conversation that would be useful for the next agent to know when continuing the work]
@@ -289,8 +379,12 @@ When constructing the summary, try to stick to this template:
289379
})
290380
}
291381
}
292-
if (processor.message.error) return "stop"
382+
if (processor.message.error) {
383+
compactionAttempts.delete(input.sessionID) // altimate_change
384+
return "stop"
385+
}
293386
Bus.publish(Event.Compacted, { sessionID: input.sessionID })
387+
compactionAttempts.delete(input.sessionID) // altimate_change
294388
return "continue"
295389
}
296390

packages/opencode/test/install/publish-package.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ describe("publish package validation", () => {
4242
test("bin entries are correct", () => {
4343
const pkg = JSON.parse(fs.readFileSync(path.join(REPO_PKG_DIR, "package.json"), "utf-8"))
4444
expect(pkg.bin).toBeDefined()
45-
expect(pkg.bin["altimate"]).toBe("./bin/altimate")
45+
expect(pkg.bin["altimate"]).toBe("./bin/altimate-code")
4646
expect(pkg.bin["altimate-code"]).toBe("./bin/altimate-code")
4747
})
4848

packages/opencode/test/project/project.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ describe("Project.fromDirectory", () => {
8080
expect(project.vcs).toBe("git")
8181
expect(project.worktree).toBe(tmp.path)
8282

83-
const opencodeFile = path.join(tmp.path, ".git", "altimate")
83+
const opencodeFile = path.join(tmp.path, ".git", "opencode")
8484
const fileExists = await Filesystem.exists(opencodeFile)
8585
expect(fileExists).toBe(false)
8686
})
@@ -96,7 +96,7 @@ describe("Project.fromDirectory", () => {
9696
expect(project.vcs).toBe("git")
9797
expect(project.worktree).toBe(tmp.path)
9898

99-
const opencodeFile = path.join(tmp.path, ".git", "altimate")
99+
const opencodeFile = path.join(tmp.path, ".git", "opencode")
100100
const fileExists = await Filesystem.exists(opencodeFile)
101101
expect(fileExists).toBe(true)
102102
})
@@ -105,11 +105,11 @@ describe("Project.fromDirectory", () => {
105105
const p = await loadProject()
106106
await using tmp = await tmpdir({ git: true })
107107

108-
// First call creates .git/altimate with the project id
108+
// First call creates .git/opencode with the project id
109109
const { project: first } = await p.fromDirectory(tmp.path)
110110
expect(first.id).not.toBe("global")
111111

112-
const newFile = path.join(tmp.path, ".git", "altimate")
112+
const newFile = path.join(tmp.path, ".git", "opencode")
113113
const legacyFile = path.join(tmp.path, ".git", "altimate-code")
114114

115115
// Move the new file to the legacy location to simulate an old installation

0 commit comments

Comments
 (0)