fix: resolve #3124 #3125 #3127 session tools, cache priming, and compaction loop

- #3124: Session tools now merge SDK and file-backed sessions for SQLite backend
- #3125: Cache priming fixed for OpenCode >=1.3.14 empty workspace
- #3127: Activity-based progress detection prevents infinite compaction on Kimi/Minimax

All 29 new tests pass, 4885 total tests passing.
This commit is contained in:
YeonGyu-Kim
2026-04-05 09:30:19 +09:00
parent aeb9c97c30
commit 130f4ac080
22 changed files with 538 additions and 114 deletions
@@ -2,6 +2,7 @@ import { describe, test, expect, beforeEach, afterEach } from "bun:test"
import * as fs from "node:fs"
import * as path from "node:path"
import * as os from "node:os"
import { PACKAGE_NAME } from "../constants"
import { updatePinnedVersion, revertPinnedVersion } from "./pinned-version-updater"
describe("pinned-version-updater", () => {
@@ -21,18 +22,18 @@ describe("pinned-version-updater", () => {
test("updates pinned version in config", () => {
//#given
const config = JSON.stringify({
plugin: ["oh-my-openagent@3.1.8"],
plugin: [`${PACKAGE_NAME}@3.1.8`],
})
fs.writeFileSync(configPath, config)
//#when
const result = updatePinnedVersion(configPath, "oh-my-openagent@3.1.8", "3.4.0")
const result = updatePinnedVersion(configPath, `${PACKAGE_NAME}@3.1.8`, "3.4.0")
//#then
expect(result).toBe(true)
const updated = fs.readFileSync(configPath, "utf-8")
expect(updated).toContain("oh-my-openagent@3.4.0")
expect(updated).not.toContain("oh-my-openagent@3.1.8")
expect(updated).toContain(`${PACKAGE_NAME}@3.4.0`)
expect(updated).not.toContain(`${PACKAGE_NAME}@3.1.8`)
})
test("returns false when entry not found", () => {
@@ -43,7 +44,7 @@ describe("pinned-version-updater", () => {
fs.writeFileSync(configPath, config)
//#when
const result = updatePinnedVersion(configPath, "oh-my-openagent@3.1.8", "3.4.0")
const result = updatePinnedVersion(configPath, `${PACKAGE_NAME}@3.1.8`, "3.4.0")
//#then
expect(result).toBe(false)
@@ -55,7 +56,7 @@ describe("pinned-version-updater", () => {
fs.writeFileSync(configPath, config)
//#when
const result = updatePinnedVersion(configPath, "oh-my-openagent@3.1.8", "3.4.0")
const result = updatePinnedVersion(configPath, `${PACKAGE_NAME}@3.1.8`, "3.4.0")
//#then
expect(result).toBe(false)
@@ -66,46 +67,46 @@ describe("pinned-version-updater", () => {
test("reverts from failed version back to original entry", () => {
//#given
const config = JSON.stringify({
plugin: ["oh-my-openagent@3.4.0"],
plugin: [`${PACKAGE_NAME}@3.4.0`],
})
fs.writeFileSync(configPath, config)
//#when
const result = revertPinnedVersion(configPath, "3.4.0", "oh-my-openagent@3.1.8")
const result = revertPinnedVersion(configPath, "3.4.0", `${PACKAGE_NAME}@3.1.8`)
//#then
expect(result).toBe(true)
const reverted = fs.readFileSync(configPath, "utf-8")
expect(reverted).toContain("oh-my-openagent@3.1.8")
expect(reverted).not.toContain("oh-my-openagent@3.4.0")
expect(reverted).toContain(`${PACKAGE_NAME}@3.1.8`)
expect(reverted).not.toContain(`${PACKAGE_NAME}@3.4.0`)
})
test("reverts to unpinned entry", () => {
//#given
const config = JSON.stringify({
plugin: ["oh-my-openagent@3.4.0"],
plugin: [`${PACKAGE_NAME}@3.4.0`],
})
fs.writeFileSync(configPath, config)
//#when
const result = revertPinnedVersion(configPath, "3.4.0", "oh-my-openagent")
const result = revertPinnedVersion(configPath, "3.4.0", PACKAGE_NAME)
//#then
expect(result).toBe(true)
const reverted = fs.readFileSync(configPath, "utf-8")
expect(reverted).toContain('"oh-my-openagent"')
expect(reverted).not.toContain("oh-my-openagent@3.4.0")
expect(reverted).toContain(`"${PACKAGE_NAME}"`)
expect(reverted).not.toContain(`${PACKAGE_NAME}@3.4.0`)
})
test("returns false when failed version not found", () => {
//#given
const config = JSON.stringify({
plugin: ["oh-my-openagent@3.1.8"],
plugin: [`${PACKAGE_NAME}@3.1.8`],
})
fs.writeFileSync(configPath, config)
//#when
const result = revertPinnedVersion(configPath, "3.4.0", "oh-my-openagent@3.1.8")
const result = revertPinnedVersion(configPath, "3.4.0", `${PACKAGE_NAME}@3.1.8`)
//#then
expect(result).toBe(false)
@@ -116,18 +117,18 @@ describe("pinned-version-updater", () => {
test("config returns to original state after update + revert", () => {
//#given
const originalConfig = JSON.stringify({
plugin: ["oh-my-openagent@3.1.8"],
plugin: [`${PACKAGE_NAME}@3.1.8`],
})
fs.writeFileSync(configPath, originalConfig)
//#when
updatePinnedVersion(configPath, "oh-my-openagent@3.1.8", "3.4.0")
revertPinnedVersion(configPath, "3.4.0", "oh-my-openagent@3.1.8")
updatePinnedVersion(configPath, `${PACKAGE_NAME}@3.1.8`, "3.4.0")
revertPinnedVersion(configPath, "3.4.0", `${PACKAGE_NAME}@3.1.8`)
//#then
const finalConfig = fs.readFileSync(configPath, "utf-8")
expect(finalConfig).toContain("oh-my-openagent@3.1.8")
expect(finalConfig).not.toContain("oh-my-openagent@3.4.0")
expect(finalConfig).toContain(`${PACKAGE_NAME}@3.1.8`)
expect(finalConfig).not.toContain(`${PACKAGE_NAME}@3.4.0`)
})
})
})
})
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"
import * as fs from "node:fs"
import * as os from "node:os"
import * as path from "node:path"
import { PACKAGE_NAME } from "../constants"
import { findPluginEntry } from "./plugin-entry"
describe("findPluginEntry", () => {
@@ -21,7 +22,7 @@ describe("findPluginEntry", () => {
test("returns unpinned for bare package name", () => {
// #given plugin is configured without a tag
fs.writeFileSync(configPath, JSON.stringify({ plugin: ["oh-my-openagent"] }))
fs.writeFileSync(configPath, JSON.stringify({ plugin: [PACKAGE_NAME] }))
// #when plugin entry is detected
const pluginInfo = findPluginEntry(temporaryDirectory)
@@ -34,7 +35,7 @@ describe("findPluginEntry", () => {
test("returns unpinned for latest dist-tag", () => {
// #given plugin is configured with latest dist-tag
fs.writeFileSync(configPath, JSON.stringify({ plugin: ["oh-my-openagent@latest"] }))
fs.writeFileSync(configPath, JSON.stringify({ plugin: [`${PACKAGE_NAME}@latest`] }))
// #when plugin entry is detected
const pluginInfo = findPluginEntry(temporaryDirectory)
@@ -47,7 +48,7 @@ describe("findPluginEntry", () => {
test("returns unpinned for beta dist-tag", () => {
// #given plugin is configured with beta dist-tag
fs.writeFileSync(configPath, JSON.stringify({ plugin: ["oh-my-openagent@beta"] }))
fs.writeFileSync(configPath, JSON.stringify({ plugin: [`${PACKAGE_NAME}@beta`] }))
// #when plugin entry is detected
const pluginInfo = findPluginEntry(temporaryDirectory)
@@ -60,7 +61,7 @@ describe("findPluginEntry", () => {
test("returns pinned for explicit semver", () => {
// #given plugin is configured with explicit version
fs.writeFileSync(configPath, JSON.stringify({ plugin: ["oh-my-openagent@3.5.2"] }))
fs.writeFileSync(configPath, JSON.stringify({ plugin: [`${PACKAGE_NAME}@3.5.2`] }))
// #when plugin entry is detected
const pluginInfo = findPluginEntry(temporaryDirectory)
@@ -11,7 +11,7 @@ interface CachePackageJson {
export interface SyncResult {
synced: boolean
error: "file_not_found" | "plugin_not_in_deps" | "parse_error" | "write_error" | null
error: "parse_error" | "write_error" | null
message?: string
}
@@ -32,12 +32,33 @@ function getIntentVersion(pluginInfo: PluginEntryInfo): string {
return pluginInfo.pinnedVersion
}
function writeCachePackageJson(
cachePackageJsonPath: string,
pkgJson: CachePackageJson,
): SyncResult {
const tmpPath = `${cachePackageJsonPath}.${crypto.randomUUID()}`
try {
fs.mkdirSync(path.dirname(cachePackageJsonPath), { recursive: true })
fs.writeFileSync(tmpPath, JSON.stringify(pkgJson, null, 2))
fs.renameSync(tmpPath, cachePackageJsonPath)
return { synced: true, error: null }
} catch (err) {
log("[auto-update-checker] Failed to write cache package.json:", err)
safeUnlink(tmpPath)
return { synced: false, error: "write_error", message: "Failed to write cache package.json" }
}
}
export function syncCachePackageJsonToIntent(pluginInfo: PluginEntryInfo): SyncResult {
const cachePackageJsonPath = path.join(CACHE_DIR, "package.json")
const intentVersion = getIntentVersion(pluginInfo)
if (!fs.existsSync(cachePackageJsonPath)) {
log("[auto-update-checker] Cache package.json not found, nothing to sync")
return { synced: false, error: "file_not_found", message: "Cache package.json not found" }
log("[auto-update-checker] Cache package.json missing, creating workspace package.json", { intentVersion })
return {
...writeCachePackageJson(cachePackageJsonPath, { dependencies: { [PACKAGE_NAME]: intentVersion } }),
message: `Created cache package.json with: ${intentVersion}`,
}
}
let content: string
@@ -58,12 +79,21 @@ export function syncCachePackageJsonToIntent(pluginInfo: PluginEntryInfo): SyncR
}
if (!pkgJson || !pkgJson.dependencies?.[PACKAGE_NAME]) {
log("[auto-update-checker] Plugin not in cache package.json dependencies, nothing to sync")
return { synced: false, error: "plugin_not_in_deps", message: "Plugin not in cache package.json dependencies" }
log("[auto-update-checker] Plugin missing from cache package.json dependencies, adding dependency", { intentVersion })
const nextPkgJson = {
...(pkgJson ?? {}),
dependencies: {
...(pkgJson?.dependencies ?? {}),
[PACKAGE_NAME]: intentVersion,
},
}
return {
...writeCachePackageJson(cachePackageJsonPath, nextPkgJson),
message: `Added ${PACKAGE_NAME}: ${intentVersion}`,
}
}
const currentVersion = pkgJson.dependencies[PACKAGE_NAME]
const intentVersion = getIntentVersion(pluginInfo)
if (currentVersion === intentVersion) {
log("[auto-update-checker] Cache package.json already matches intent:", intentVersion)
@@ -84,15 +114,8 @@ export function syncCachePackageJsonToIntent(pluginInfo: PluginEntryInfo): SyncR
}
pkgJson.dependencies[PACKAGE_NAME] = intentVersion
const tmpPath = `${cachePackageJsonPath}.${crypto.randomUUID()}`
try {
fs.writeFileSync(tmpPath, JSON.stringify(pkgJson, null, 2))
fs.renameSync(tmpPath, cachePackageJsonPath)
return { synced: true, error: null, message: `Updated: "${currentVersion}" → "${intentVersion}"` }
} catch (err) {
log("[auto-update-checker] Failed to write cache package.json:", err)
safeUnlink(tmpPath)
return { synced: false, error: "write_error", message: "Failed to write cache package.json" }
return {
...writeCachePackageJson(cachePackageJsonPath, pkgJson),
message: `Updated: "${currentVersion}" → "${intentVersion}"`,
}
}
@@ -6,9 +6,9 @@ describe("auto-update-checker constants", () => {
it("uses the OpenCode cache directory for installed package metadata", async () => {
const { CACHE_DIR, INSTALLED_PACKAGE_JSON, PACKAGE_NAME } = await import(`./constants?test=${Date.now()}`)
expect(CACHE_DIR).toBe(getOpenCodeCacheDir())
expect(CACHE_DIR).toBe(join(getOpenCodeCacheDir(), "packages"))
expect(INSTALLED_PACKAGE_JSON).toBe(
join(getOpenCodeCacheDir(), "node_modules", PACKAGE_NAME, "package.json")
join(getOpenCodeCacheDir(), "packages", "node_modules", PACKAGE_NAME, "package.json")
)
})
})
+3 -2
View File
@@ -7,8 +7,9 @@ export const PACKAGE_NAME = "oh-my-openagent"
export const NPM_REGISTRY_URL = `https://registry.npmjs.org/-/package/${PACKAGE_NAME}/dist-tags`
export const NPM_FETCH_TIMEOUT = 5000
export const CACHE_DIR = getOpenCodeCacheDir()
export const VERSION_FILE = path.join(CACHE_DIR, "version")
export const CACHE_ROOT_DIR = getOpenCodeCacheDir()
export const CACHE_DIR = path.join(CACHE_ROOT_DIR, "packages")
export const VERSION_FILE = path.join(CACHE_ROOT_DIR, "version")
export function getWindowsAppdataDir(): string | null {
if (process.platform !== "win32") return null
@@ -33,6 +33,10 @@ type BackgroundUpdateCheckRunner = (
getToastMessage: (isUpdate: boolean, latestVersion?: string) => string,
) => Promise<void>
function getCacheWorkspaceDir(deps: BackgroundUpdateCheckDeps): string {
return deps.join(deps.getOpenCodeCacheDir(), "packages")
}
const defaultDeps: BackgroundUpdateCheckDeps = {
existsSync,
join,
@@ -60,7 +64,7 @@ function getPinnedVersionToastMessage(latestVersion: string): string {
*/
function resolveActiveInstallWorkspace(deps: BackgroundUpdateCheckDeps): string {
const configPaths = deps.getOpenCodeConfigPaths({ binary: "opencode" })
const cacheDir = deps.getOpenCodeCacheDir()
const cacheDir = getCacheWorkspaceDir(deps)
const configInstallPath = deps.join(configPaths.configDir, "node_modules", PACKAGE_NAME, "package.json")
const cacheInstallPath = deps.join(cacheDir, "node_modules", PACKAGE_NAME, "package.json")
@@ -76,6 +80,12 @@ function resolveActiveInstallWorkspace(deps: BackgroundUpdateCheckDeps): string
return cacheDir
}
const cachePackageJsonPath = deps.join(cacheDir, "package.json")
if (deps.existsSync(cachePackageJsonPath)) {
deps.log(`[auto-update-checker] Active workspace: cache-dir (${cacheDir}, package.json present)`)
return cacheDir
}
// Default to config-dir if neither exists (matches doctor behavior)
deps.log(`[auto-update-checker] Active workspace: config-dir (default, no install detected)`)
return configPaths.configDir
@@ -95,6 +105,19 @@ async function runBunInstallSafe(workspaceDir: string, deps: BackgroundUpdateChe
}
}
async function primeCacheWorkspace(
activeWorkspace: string,
deps: BackgroundUpdateCheckDeps,
): Promise<boolean> {
const cacheWorkspace = getCacheWorkspaceDir(deps)
if (activeWorkspace === cacheWorkspace) {
return true
}
deps.log(`[auto-update-checker] Priming cache workspace after install: ${cacheWorkspace}`)
return runBunInstallSafe(cacheWorkspace, deps)
}
export function createBackgroundUpdateCheckRunner(
overrides: Partial<BackgroundUpdateCheckDeps> = {},
): BackgroundUpdateCheckRunner {
@@ -156,6 +179,13 @@ export function createBackgroundUpdateCheckRunner(
const installSuccess = await runBunInstallSafe(activeWorkspace, deps)
if (installSuccess) {
const cachePrimed = await primeCacheWorkspace(activeWorkspace, deps)
if (!cachePrimed) {
await deps.showUpdateAvailableToast(ctx, latestVersion, getToastMessage)
deps.log("[auto-update-checker] cache workspace priming failed after install")
return
}
await deps.showAutoUpdatedToast(ctx, currentVersion, latestVersion)
deps.log(`[auto-update-checker] Update installed: ${currentVersion}${latestVersion}`)
return