refactor: migrate delegate_task to task tool with metadata fixes

- Rename delegate_task tool to task across codebase (100 files)
- Update model references: claude-opus-4-6 → 4-5, gpt-5.3-codex → 5.2-codex
- Add tool-metadata-store to restore metadata overwritten by fromPlugin()
- Add session ID polling for BackgroundManager task sessions
- Await async ctx.metadata() calls in tool executors
- Add ses_ prefix guard to getMessageDir for performance
- Harden BackgroundManager with idle deferral and error handling
- Fix duplicate task key in sisyphus-junior test object literals
- Fix unawaited showOutputToUser in ast_grep_replace
- Fix background=true → run_in_background=true in ultrawork prompt
- Fix duplicate task/task references in docs and comments
This commit is contained in:
YeonGyu-Kim
2026-02-06 16:01:54 +09:00
parent f1c794e63e
commit a691a3ac0a
78 changed files with 1182 additions and 403 deletions
@@ -0,0 +1,111 @@
import { describe, test, expect, beforeEach } from "bun:test"
import {
storeToolMetadata,
consumeToolMetadata,
getPendingStoreSize,
clearPendingStore,
} from "./index"
describe("tool-metadata-store", () => {
beforeEach(() => {
clearPendingStore()
})
describe("storeToolMetadata", () => {
test("#given metadata with title and metadata, #when stored, #then store size increases", () => {
//#given
const sessionID = "ses_abc123"
const callID = "call_001"
const data = {
title: "Test Task",
metadata: { sessionId: "ses_child", agent: "oracle" },
}
//#when
storeToolMetadata(sessionID, callID, data)
//#then
expect(getPendingStoreSize()).toBe(1)
})
})
describe("consumeToolMetadata", () => {
test("#given stored metadata, #when consumed, #then returns the stored data", () => {
//#given
const sessionID = "ses_abc123"
const callID = "call_001"
const data = {
title: "My Task",
metadata: { sessionId: "ses_sub", run_in_background: true },
}
storeToolMetadata(sessionID, callID, data)
//#when
const result = consumeToolMetadata(sessionID, callID)
//#then
expect(result).toEqual(data)
})
test("#given stored metadata, #when consumed twice, #then second call returns undefined", () => {
//#given
const sessionID = "ses_abc123"
const callID = "call_001"
storeToolMetadata(sessionID, callID, { title: "Task" })
//#when
consumeToolMetadata(sessionID, callID)
const second = consumeToolMetadata(sessionID, callID)
//#then
expect(second).toBeUndefined()
expect(getPendingStoreSize()).toBe(0)
})
test("#given no stored metadata, #when consumed, #then returns undefined", () => {
//#given
const sessionID = "ses_nonexistent"
const callID = "call_999"
//#when
const result = consumeToolMetadata(sessionID, callID)
//#then
expect(result).toBeUndefined()
})
})
describe("isolation", () => {
test("#given multiple entries, #when consuming one, #then others remain", () => {
//#given
storeToolMetadata("ses_1", "call_a", { title: "Task A" })
storeToolMetadata("ses_1", "call_b", { title: "Task B" })
storeToolMetadata("ses_2", "call_a", { title: "Task C" })
//#when
const resultA = consumeToolMetadata("ses_1", "call_a")
//#then
expect(resultA?.title).toBe("Task A")
expect(getPendingStoreSize()).toBe(2)
expect(consumeToolMetadata("ses_1", "call_b")?.title).toBe("Task B")
expect(consumeToolMetadata("ses_2", "call_a")?.title).toBe("Task C")
expect(getPendingStoreSize()).toBe(0)
})
})
describe("overwrite", () => {
test("#given existing entry, #when stored again with same key, #then overwrites", () => {
//#given
storeToolMetadata("ses_1", "call_a", { title: "Old" })
//#when
storeToolMetadata("ses_1", "call_a", { title: "New", metadata: { updated: true } })
//#then
const result = consumeToolMetadata("ses_1", "call_a")
expect(result?.title).toBe("New")
expect(result?.metadata).toEqual({ updated: true })
})
})
})
+84
View File
@@ -0,0 +1,84 @@
/**
* Pending tool metadata store.
*
* OpenCode's `fromPlugin()` wrapper always replaces the metadata returned by
* plugin tools with `{ truncated, outputPath }`, discarding any sessionId,
* title, or custom metadata set during `execute()`.
*
* This store captures metadata written via `ctx.metadata()` inside execute(),
* then the `tool.execute.after` hook consumes it and merges it back into the
* result *before* the processor writes the final part to the session store.
*
* Flow:
* execute() → storeToolMetadata(sessionID, callID, data)
* fromPlugin() → overwrites metadata with { truncated }
* tool.execute.after → consumeToolMetadata(sessionID, callID) → merges back
* processor → Session.updatePart(status:"completed", metadata: result.metadata)
*/
export interface PendingToolMetadata {
title?: string
metadata?: Record<string, unknown>
}
const pendingStore = new Map<string, PendingToolMetadata & { storedAt: number }>()
const STALE_TIMEOUT_MS = 15 * 60 * 1000
function makeKey(sessionID: string, callID: string): string {
return `${sessionID}:${callID}`
}
function cleanupStaleEntries(): void {
const now = Date.now()
for (const [key, entry] of pendingStore) {
if (now - entry.storedAt > STALE_TIMEOUT_MS) {
pendingStore.delete(key)
}
}
}
/**
* Store metadata to be restored after fromPlugin() overwrites it.
* Called from tool execute() functions alongside ctx.metadata().
*/
export function storeToolMetadata(
sessionID: string,
callID: string,
data: PendingToolMetadata,
): void {
cleanupStaleEntries()
pendingStore.set(makeKey(sessionID, callID), { ...data, storedAt: Date.now() })
}
/**
* Consume stored metadata (one-time read, removes from store).
* Called from tool.execute.after hook.
*/
export function consumeToolMetadata(
sessionID: string,
callID: string,
): PendingToolMetadata | undefined {
const key = makeKey(sessionID, callID)
const stored = pendingStore.get(key)
if (stored) {
pendingStore.delete(key)
const { storedAt: _, ...data } = stored
return data
}
return undefined
}
/**
* Get current store size (for testing/debugging).
*/
export function getPendingStoreSize(): number {
return pendingStore.size
}
/**
* Clear all pending metadata (for testing).
*/
export function clearPendingStore(): void {
pendingStore.clear()
}