Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions packages/opencode/src/altimate/workspace/engine-overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,15 +436,19 @@ export async function atTurnStart<T>(sessionID: string, body: () => Promise<T>):
* another session's boundary mid-turn would otherwise be re-catalogued here;
* pinning keeps this turn on the engine its boundary read. A call through a
* pinned wrapper after a replacement reaches the closed client and fails — it
* never routes to the other workspace. */
const turnTools = new Map<string, Record<string, unknown>>()
* never routes to the other workspace.
*
* The first catalog's key order is kept too. The provider receives tools in
* record order, and a reorder between steps misses its prompt cache for the
* whole request. */
const turnTools = new Map<string, { engine: Record<string, unknown>; order: string[] }>()

export function pinTurnTools<T>(sessionID: string, firstCatalog: boolean, tools: Record<string, T>): void {
if (!isEnabled() || isServe()) return
const engine = Object.fromEntries(Object.entries(tools).filter(([key]) => key.startsWith(TOOL_PREFIX)))
if (firstCatalog) {
turnTools.delete(sessionID)
turnTools.set(sessionID, engine)
turnTools.set(sessionID, { engine, order: Object.keys(tools) })
while (turnTools.size > MAX_TRACKED_SESSIONS) {
const oldest = turnTools.keys().next().value
if (oldest === undefined) break
Expand All @@ -454,8 +458,15 @@ export function pinTurnTools<T>(sessionID: string, firstCatalog: boolean, tools:
}
const pinned = turnTools.get(sessionID)
if (!pinned) return
for (const key of Object.keys(engine)) delete tools[key]
for (const [key, tool] of Object.entries(pinned)) tools[key] = tool as T
// Own-key checks only: a tool may be named `constructor` or `toString`.
const next: Record<string, T> = Object.fromEntries(
Object.entries(tools).filter(([key]) => !Object.hasOwn(engine, key)),
)
for (const [key, tool] of Object.entries(pinned.engine)) next[key] = tool as T
// Rebuild in first-catalog order; keys new since then go last, in arrival order.
const ordered = new Set([...pinned.order.filter((key) => Object.hasOwn(next, key)), ...Object.keys(next)])
for (const key of Object.keys(tools)) delete tools[key]
for (const key of ordered) tools[key] = next[key]
Comment thread
anandgupta42 marked this conversation as resolved.
}

async function reconcile(sessionID: string, directory: string, state: DirectoryState): Promise<void> {
Expand Down
59 changes: 59 additions & 0 deletions packages/opencode/test/altimate/workspace/engine-overlay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,65 @@ describe("beforeTurn — what a turn boundary does", () => {
expect(step2).toEqual({ datamate_b: { id: "b1" } })
})

test("pinning keeps the tool order of the first catalog, so the provider's prompt cache still hits", async () => {
install({})
// The native `datamate_manager` shares the engine prefix and sits between native tools.
const catalog = () => ({
read: { id: "read" },
datamate_manager: { id: "native" },
feedback_submit: { id: "feedback" },
dbt_pr_review: { id: "review" },
datamate_add_memories: { id: "add" },
datamate_search_memory: { id: "search" },
})
const first = catalog()
pinTurnTools("s1", true, first)
const later = catalog()
pinTurnTools("s1", false, later)
expect(Object.keys(later)).toEqual(Object.keys(first))
expect(later.datamate_manager).toBe(first.datamate_manager)
})

test("pinning keeps the first catalog's order when the engine's tools change mid-turn", async () => {
install({})
pinTurnTools("s1", true, {
read: { id: "read" },
datamate_a: { id: "a1" },
datamate_b: { id: "b1" },
sql: { id: "sql" },
})
// `datamate_a` vanished, the survivor and a native tool arrive reversed, and a new engine tool appeared.
const later: Record<string, { id: string }> = {
sql: { id: "sql" },
datamate_c: { id: "c2" },
datamate_b: { id: "b2" },
read: { id: "read" },
write: { id: "write" },
}
pinTurnTools("s1", false, later)
expect(Object.keys(later)).toEqual(["read", "datamate_a", "datamate_b", "sql", "write"])
expect(later.datamate_a).toEqual({ id: "a1" })
expect(later.datamate_b).toEqual({ id: "b1" })
})

test("pinning keeps tools whose names are Object.prototype properties", async () => {
install({})
const catalog = (): Record<string, { id: string }> =>
Object.fromEntries([
["constructor", { id: "ctor" }],
["datamate_a", { id: "a1" }],
["toString", { id: "str" }],
])
pinTurnTools("s1", true, catalog())
const later = catalog()
pinTurnTools("s1", false, later)
expect(Object.entries(later)).toEqual([
["constructor", { id: "ctor" }],
["datamate_a", { id: "a1" }],
["toString", { id: "str" }],
])
})

test("pinning is a no-op with the flag off and for a session with no step-1 snapshot", async () => {
install({ flag: false })
const tools: Record<string, { id: string }> = { datamate_a: { id: "a1" } }
Expand Down
Loading