Revert "fix(memory): bound OOM-prone accumulators (#10179)" (#10255)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Neil
2026-07-23 18:35:31 -07:00
committed by GitHub
co-authored by Orca
parent 6eb70d8370
commit aab112933e
1577 changed files with 15166 additions and 91654 deletions
@@ -1,71 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
CONFIG_OVERLAY_MAX_RETAINED_NAME_BYTES,
CONFIG_OVERLAY_MAX_SOURCE_ENTRIES,
ConfigOverlayCapacityError
} from '../pty/config-overlay-mirroring'
import { _configOverlayManifestInternals } from './config-overlay-manifest'
const { parseOverlayManifest } = _configOverlayManifestInternals
describe('OpenCode overlay manifest bounds', () => {
it('accepts the exact entry limit and rejects one more retained entry', () => {
const exact = parseOverlayManifest(
JSON.stringify({
topLevelEntries: Array.from({ length: CONFIG_OVERLAY_MAX_SOURCE_ENTRIES }, () => 'same'),
pluginEntries: []
})
)
expect(exact.topLevelEntries).toHaveLength(CONFIG_OVERLAY_MAX_SOURCE_ENTRIES)
expect(() =>
parseOverlayManifest(
JSON.stringify({
topLevelEntries: Array.from(
{ length: CONFIG_OVERLAY_MAX_SOURCE_ENTRIES + 1 },
() => 'same'
),
pluginEntries: []
})
)
).toThrowError(
new ConfigOverlayCapacityError(
'entries',
CONFIG_OVERLAY_MAX_SOURCE_ENTRIES + 1,
CONFIG_OVERLAY_MAX_SOURCE_ENTRIES
)
)
})
it('accepts the exact encoded-name budget and rejects the next name', () => {
const name = 'a'.repeat(4_094)
const names = Array.from({ length: CONFIG_OVERLAY_MAX_RETAINED_NAME_BYTES / 4_096 }, () => name)
expect(
parseOverlayManifest(JSON.stringify({ topLevelEntries: names })).topLevelEntries
).toHaveLength(names.length)
expect(() =>
parseOverlayManifest(JSON.stringify({ topLevelEntries: [...names, 'a'] }))
).toThrowError(
new ConfigOverlayCapacityError(
'retained-name-bytes',
CONFIG_OVERLAY_MAX_RETAINED_NAME_BYTES + 3,
CONFIG_OVERLAY_MAX_RETAINED_NAME_BYTES
)
)
})
it('never authorizes cleanup of reserved overlay-owned paths', () => {
expect(
parseOverlayManifest(
JSON.stringify({
topLevelEntries: ['plugins', '.orca-opencode-overlay-manifest.json', 'auth.json'],
pluginEntries: ['orca-opencode-status.js', 'user-plugin.js']
})
)
).toEqual({
topLevelEntries: ['auth.json'],
pluginEntries: ['user-plugin.js']
})
})
})
@@ -1,164 +0,0 @@
import { unlinkSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { stringifyJsonWithinByteLimit } from '../../shared/node-bounded-json-stringify'
import {
NodeFileReadTooLargeError,
readNodeFileSyncWithinLimit
} from '../../shared/node-bounded-file-reader'
import {
CONFIG_OVERLAY_MAX_SOURCE_ENTRIES,
ConfigOverlayCapacityError,
ConfigOverlayEntryBudget,
applyConfigOverlayPlan,
createConfigOverlayPlan,
type AppliedConfigOverlayEntries
} from '../pty/config-overlay-mirroring'
import { safeRemoveTree } from '../pty/overlay-mirror'
export const ORCA_OPENCODE_PLUGIN_FILE = 'orca-opencode-status.js'
export const OPENCODE_OVERLAY_MANIFEST_FILE = '.orca-opencode-overlay-manifest.json'
export const OPENCODE_OVERLAY_MANIFEST_MAX_BYTES = 2 * 1_024 * 1_024
type OpenCodeOverlayManifest = {
topLevelEntries: string[]
pluginEntries: string[]
}
const RESERVED_TOP_LEVEL_ENTRIES = new Set([OPENCODE_OVERLAY_MANIFEST_FILE])
function emptyManifest(): OpenCodeOverlayManifest {
return { topLevelEntries: [], pluginEntries: [] }
}
function parseManifestEntryNames(
value: unknown,
kind: 'top-level' | 'plugin',
budget: ConfigOverlayEntryBudget
): string[] {
if (!Array.isArray(value)) {
return []
}
const names: string[] = []
for (const candidate of value) {
if (typeof candidate !== 'string') {
continue
}
budget.reserve(candidate)
if (
(kind === 'top-level' &&
(candidate === 'plugins' || candidate === OPENCODE_OVERLAY_MANIFEST_FILE)) ||
(kind === 'plugin' && candidate === ORCA_OPENCODE_PLUGIN_FILE)
) {
continue
}
names.push(candidate)
}
return names
}
function parseOverlayManifest(contents: string): OpenCodeOverlayManifest {
const parsed = JSON.parse(contents) as Partial<OpenCodeOverlayManifest>
const topLevelCount = Array.isArray(parsed.topLevelEntries) ? parsed.topLevelEntries.length : 0
const pluginCount = Array.isArray(parsed.pluginEntries) ? parsed.pluginEntries.length : 0
if (topLevelCount + pluginCount > CONFIG_OVERLAY_MAX_SOURCE_ENTRIES) {
throw new ConfigOverlayCapacityError(
'entries',
topLevelCount + pluginCount,
CONFIG_OVERLAY_MAX_SOURCE_ENTRIES
)
}
const budget = new ConfigOverlayEntryBudget()
return {
topLevelEntries: parseManifestEntryNames(parsed.topLevelEntries, 'top-level', budget),
pluginEntries: parseManifestEntryNames(parsed.pluginEntries, 'plugin', budget)
}
}
function readOverlayManifest(overlayDir: string): OpenCodeOverlayManifest {
try {
const contents = readNodeFileSyncWithinLimit(
join(overlayDir, OPENCODE_OVERLAY_MANIFEST_FILE),
OPENCODE_OVERLAY_MANIFEST_MAX_BYTES
).buffer.toString('utf8')
return parseOverlayManifest(contents)
} catch (error) {
if (error instanceof ConfigOverlayCapacityError || error instanceof NodeFileReadTooLargeError) {
throw error
}
return emptyManifest()
}
}
function removePathBeforeWrite(path: string): void {
try {
unlinkSync(path)
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
throw error
}
}
}
function writeOverlayManifest(overlayDir: string, manifest: OpenCodeOverlayManifest): void {
const manifestPath = join(overlayDir, OPENCODE_OVERLAY_MANIFEST_FILE)
const { serialized } = stringifyJsonWithinByteLimit(
manifest,
OPENCODE_OVERLAY_MANIFEST_MAX_BYTES - 1
)
removePathBeforeWrite(manifestPath)
writeFileSync(manifestPath, `${serialized}\n`)
}
function clearManifestEntries(overlayDir: string, manifest: OpenCodeOverlayManifest): void {
for (const entryName of manifest.topLevelEntries) {
if (!safeRemoveTree(join(overlayDir, entryName))) {
throw new Error('Unable to clear an OpenCode config overlay entry')
}
}
const overlayPluginsDir = join(overlayDir, 'plugins')
for (const entryName of manifest.pluginEntries) {
if (!safeRemoveTree(join(overlayPluginsDir, entryName))) {
throw new Error('Unable to clear an OpenCode plugin overlay entry')
}
}
}
function toManifest(applied: AppliedConfigOverlayEntries): OpenCodeOverlayManifest {
return {
topLevelEntries: applied.topLevelEntryNames,
pluginEntries: applied.pluginEntryNames
}
}
export function mirrorOpenCodeConfigWithManifest(sourceDir: string, overlayDir: string): void {
const plan = createConfigOverlayPlan(sourceDir, {
reservedPluginFile: ORCA_OPENCODE_PLUGIN_FILE,
reservedTopLevelEntryNames: RESERVED_TOP_LEVEL_ENTRIES
})
const previousManifest = readOverlayManifest(overlayDir)
clearManifestEntries(overlayDir, previousManifest)
const applied: AppliedConfigOverlayEntries = {
topLevelEntryNames: [],
pluginEntryNames: []
}
try {
applyConfigOverlayPlan(plan, overlayDir, applied)
writeOverlayManifest(overlayDir, toManifest(applied))
} catch (error) {
try {
clearManifestEntries(overlayDir, toManifest(applied))
} catch {
// Preserve the original mirror/write failure; a later spawn can retry cleanup.
}
throw error
}
}
export const _configOverlayManifestInternals = {
parseOverlayManifest,
writeOverlayManifest
}
-86
View File
@@ -8,12 +8,10 @@ import {
readdirSync,
rmSync,
symlinkSync,
truncateSync,
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { GENERATED_NODE_MANAGED_FILE_MAX_BYTES } from '../generated-node-bounded-file-reader'
const { getPathMock } = vi.hoisted(() => ({
getPathMock: vi.fn<(name: string) => string>()
@@ -26,10 +24,6 @@ vi.mock('electron', () => ({
}))
import { OpenCodeHookService, _internals } from './hook-service'
import {
OPENCODE_OVERLAY_MANIFEST_FILE,
OPENCODE_OVERLAY_MANIFEST_MAX_BYTES
} from './config-overlay-manifest'
const { isUsableId, toSafeDirName } = _internals
@@ -71,11 +65,6 @@ describe('OpenCode hook plugin source', () => {
expect(source).toContain('const coords = resolveHookCoords();')
expect(source).toContain('`http://127.0.0.1:${coords.port}/hook/opencode`')
expect(source).toContain('"X-Orca-Agent-Hook-Token": coords.token')
expect(source).toContain(
`function readOrcaManagedFileWithinLimit(fs, path, maxBytes = ${GENERATED_NODE_MANAGED_FILE_MAX_BYTES})`
)
expect(source).toContain('readOrcaManagedFileWithinLimit(fs, path)')
expect(source).not.toContain('fs.readFileSync')
})
it('caches the parsed endpoint file on mtime+size+inode to skip re-reads per post', () => {
@@ -366,81 +355,6 @@ describe('OpenCodeHookService overlay mode (user OPENCODE_CONFIG_DIR set)', () =
expectUserConfigIntact()
})
it('does not mirror or overwrite a user file named like the internal manifest', () => {
const userManifest = join(userConfigDir, OPENCODE_OVERLAY_MANIFEST_FILE)
writeFileSync(userManifest, 'USER MANIFEST SENTINEL')
const env = new OpenCodeHookService().buildPtyEnv(ptyId, userConfigDir)
const overlayManifest = join(env.OPENCODE_CONFIG_DIR!, OPENCODE_OVERLAY_MANIFEST_FILE)
expect(readFileSync(userManifest, 'utf8')).toBe('USER MANIFEST SENTINEL')
expect(lstatSync(overlayManifest).isSymbolicLink()).toBe(false)
expect(JSON.parse(readFileSync(overlayManifest, 'utf8'))).toMatchObject({
topLevelEntries: expect.arrayContaining(['auth.json', 'opencode.json'])
})
})
it('falls back before mutation when a retained manifest exceeds its byte limit', () => {
const service = new OpenCodeHookService()
const first = service.buildPtyEnv(ptyId, userConfigDir)
const overlayDir = first.OPENCODE_CONFIG_DIR!
const overlayManifest = join(overlayDir, OPENCODE_OVERLAY_MANIFEST_FILE)
truncateSync(overlayManifest, OPENCODE_OVERLAY_MANIFEST_MAX_BYTES + 1)
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
expect(service.buildPtyEnv(ptyId, userConfigDir)).toEqual({
OPENCODE_CONFIG_DIR: userConfigDir
})
expect(readFileSync(join(overlayDir, 'auth.json'), 'utf8')).toBe('user-auth-token')
expect(warnSpy).toHaveBeenCalledWith(
'[opencode-hooks] config overlay exceeded its memory limit; using the original OPENCODE_CONFIG_DIR without Orca status integration'
)
expectUserConfigIntact()
})
it('ignores forged manifest traversal entries during cleanup', () => {
const overlayDir = join(
userDataDir,
'opencode-config-overlays',
toSafeDirName(`source:${userConfigDir}`)
)
mkdirSync(overlayDir, { recursive: true })
const outsideMarker = join(userDataDir, 'outside-manifest-marker')
writeFileSync(outsideMarker, 'keep')
writeFileSync(
join(overlayDir, OPENCODE_OVERLAY_MANIFEST_FILE),
JSON.stringify({ topLevelEntries: ['../outside-manifest-marker'] })
)
const env = new OpenCodeHookService().buildPtyEnv(ptyId, userConfigDir)
expect(env.OPENCODE_CONFIG_DIR).toBe(overlayDir)
expect(readFileSync(outsideMarker, 'utf8')).toBe('keep')
})
it.skipIf(process.platform === 'win32')(
'replaces a stale manifest symlink without writing through to the user file',
() => {
const userManifest = join(userConfigDir, OPENCODE_OVERLAY_MANIFEST_FILE)
writeFileSync(userManifest, 'USER MANIFEST SENTINEL')
const overlayDir = join(
userDataDir,
'opencode-config-overlays',
toSafeDirName(`source:${userConfigDir}`)
)
mkdirSync(overlayDir, { recursive: true })
symlinkSync(userManifest, join(overlayDir, OPENCODE_OVERLAY_MANIFEST_FILE), 'file')
const env = new OpenCodeHookService().buildPtyEnv(ptyId, userConfigDir)
expect(env.OPENCODE_CONFIG_DIR).toBe(overlayDir)
expect(readFileSync(userManifest, 'utf8')).toBe('USER MANIFEST SENTINEL')
expect(lstatSync(join(overlayDir, OPENCODE_OVERLAY_MANIFEST_FILE)).isSymbolicLink()).toBe(
false
)
}
)
it.skipIf(process.platform === 'win32')(
'does not write through a symlinked plugins/ directory into the user filesystem',
() => {
+108 -30
View File
@@ -1,19 +1,29 @@
/* eslint-disable max-lines -- Why: holds an inline JS plugin source emitted as one file; splitting across TS modules would scatter tightly coupled string-template logic. */
import { app } from 'electron'
import { join } from 'node:path'
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs'
import { createHash } from 'node:crypto'
import { NodeFileReadTooLargeError } from '../../shared/node-bounded-file-reader'
import { getGeneratedNodeBoundedFileReaderSourceLines } from '../generated-node-bounded-file-reader'
import { ConfigOverlayCapacityError } from '../pty/config-overlay-mirroring'
import {
ORCA_OPENCODE_PLUGIN_FILE,
mirrorOpenCodeConfigWithManifest
} from './config-overlay-manifest'
existsSync,
mkdirSync,
readFileSync,
readdirSync,
realpathSync,
statSync,
unlinkSync,
writeFileSync
} from 'node:fs'
import { createHash } from 'node:crypto'
import { mirrorEntry, safeRemoveTree } from '../pty/overlay-mirror'
const ORCA_OPENCODE_PLUGIN_FILE = 'orca-opencode-status.js'
const OPENCODE_LEGACY_HOOKS_DIR = 'opencode-hooks'
const OPENCODE_OVERLAY_DIR = 'opencode-config-overlays'
const OPENCODE_SHARED_CONFIG_DIR = 'shared'
const OPENCODE_OVERLAY_MANIFEST_FILE = '.orca-opencode-overlay-manifest.json'
type OpenCodeOverlayManifest = {
topLevelEntries: string[]
pluginEntries: string[]
}
// Why: bounds-check only — the id is a daemon sessionId with path separators, hashed downstream to a filesystem-safe name (an old regex rejecting "/"/":" broke every such id, #1148); 1024 just caps pathological hash input.
function isUsableId(id: string): boolean {
@@ -38,13 +48,12 @@ export function getOpenCodeFamilyPluginSource(hookPathname: string): string {
"// OpenCode's Node process (not Orca's) and has no access to server.ts's",
'// equivalent warnedVersions / warnedEnvs Sets.',
'let warnedBadEndpoint = false;',
...getGeneratedNodeBoundedFileReaderSourceLines(),
'',
'// Why: message.part.updated can fire many times per second during a',
'// streaming assistant reply, and each post() calls resolveHookCoords()',
'// which reads the endpoint file. The file only changes on Orca restart',
'// (rare), so a stat+mtime check is substantially cheaper than a full',
'// bounded read+parse on every streamed part. On stat error we fall',
'// readFileSync+parse on every streamed part. On stat error we fall',
'// through to parse so the fail-open behavior is preserved.',
'let cachedEndpointKey = "";',
'let cachedEndpointValues = null;',
@@ -65,7 +74,7 @@ export function getOpenCodeFamilyPluginSource(hookPathname: string): string {
' if (cacheKey === cachedEndpointKey && cachedEndpointValues) {',
' return cachedEndpointValues;',
' }',
' const contents = readOrcaManagedFileWithinLimit(fs, path);',
' const contents = fs.readFileSync(path, "utf8");',
' const out = {};',
' for (const line of contents.split(/\\r?\\n/)) {',
' // Why: Windows endpoint.cmd uses `set KEY=VALUE`; Unix endpoint.env',
@@ -82,7 +91,7 @@ export function getOpenCodeFamilyPluginSource(hookPathname: string): string {
' return out;',
' } catch (ioErr) {',
' // Why: any stat or read failure (file yanked mid-read, permission',
' // race, unlink between stat and bounded read) must invalidate the',
' // race, unlink between stat and readFileSync) must invalidate the',
' // cache so a transient failure does not lock in a stale parse for',
' // the remaining process lifetime; rethrow to the outer catch.',
' cachedEndpointKey = "";',
@@ -368,8 +377,6 @@ export function getOpenCodeFamilyPluginSource(hookPathname: string): string {
// Why: installs the plugin into OPENCODE_CONFIG_DIR so it POSTs to the shared agent-hooks server, unifying OpenCode status with Claude/Codex/Gemini (the old loopback-IPC path never reached agentStatusByPaneKey).
export class OpenCodeHookService {
private warnedOverlayCapacity = false
clearPty(_ptyId: string): void {
// Why: no-op — config dirs are app/source-scoped now, and recursive delete on the main-process hot path could freeze on Windows.
}
@@ -400,16 +407,7 @@ export class OpenCodeHookService {
mkdirSync(overlayDir, { recursive: true })
this.mirrorUserConfig(existingConfigDir, overlayDir)
this.writePluginIntoOverlay(overlayDir)
} catch (error) {
if (
!this.warnedOverlayCapacity &&
(error instanceof ConfigOverlayCapacityError || error instanceof NodeFileReadTooLargeError)
) {
this.warnedOverlayCapacity = true
console.warn(
'[opencode-hooks] config overlay exceeded its memory limit; using the original OPENCODE_CONFIG_DIR without Orca status integration'
)
}
} catch {
// Why: best-effort — symlink creation needs Windows developer mode (else EPERM) and userData may be read-only; preserve the user's config over dropping their auth/models/keymap.
return { OPENCODE_CONFIG_DIR: existingConfigDir }
}
@@ -429,8 +427,90 @@ export class OpenCodeHookService {
return join(app.getPath('userData'), OPENCODE_LEGACY_HOOKS_DIR, OPENCODE_SHARED_CONFIG_DIR)
}
private readOverlayManifest(overlayDir: string): OpenCodeOverlayManifest {
try {
const parsed = JSON.parse(
readFileSync(join(overlayDir, OPENCODE_OVERLAY_MANIFEST_FILE), 'utf8')
) as Partial<OpenCodeOverlayManifest>
return {
topLevelEntries: Array.isArray(parsed.topLevelEntries) ? parsed.topLevelEntries : [],
pluginEntries: Array.isArray(parsed.pluginEntries) ? parsed.pluginEntries : []
}
} catch {
return { topLevelEntries: [], pluginEntries: [] }
}
}
private writeOverlayManifest(overlayDir: string, manifest: OpenCodeOverlayManifest): void {
writeFileSync(
join(overlayDir, OPENCODE_OVERLAY_MANIFEST_FILE),
`${JSON.stringify(manifest, null, 2)}\n`
)
}
private clearManifestEntries(overlayDir: string, manifest: OpenCodeOverlayManifest): void {
for (const entryName of manifest.topLevelEntries) {
safeRemoveTree(join(overlayDir, entryName))
}
const overlayPluginsDir = join(overlayDir, 'plugins')
for (const entryName of manifest.pluginEntries) {
if (entryName === ORCA_OPENCODE_PLUGIN_FILE) {
continue
}
safeRemoveTree(join(overlayPluginsDir, entryName))
}
}
// Why: mirror user config entries as symlinks so edits propagate live; only plugins/ becomes a real overlay dir so Orca can drop a sibling plugin file.
private mirrorUserConfig(sourceDir: string, overlayDir: string): void {
mirrorOpenCodeConfigWithManifest(sourceDir, overlayDir)
const previousManifest = this.readOverlayManifest(overlayDir)
// Why: overlays persist across terminals; remove only Orca-mirrored paths so stale user config clears but OpenCode runtime dirs (node_modules) survive.
this.clearManifestEntries(overlayDir, previousManifest)
const nextManifest: OpenCodeOverlayManifest = { topLevelEntries: [], pluginEntries: [] }
for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
const sourcePath = join(sourceDir, entry.name)
if (entry.name === 'plugins') {
// Why: check isSymbolicLink before isDirectory — a Windows junction reports both, and the symlink branch must win.
const isSymlink = entry.isSymbolicLink()
let isLinkPointingToDir = false
if (isSymlink) {
try {
isLinkPointingToDir = statSync(sourcePath).isDirectory()
} catch {
// Why: broken/inaccessible symlink — mirror the dangling link verbatim instead of resolving through it.
isLinkPointingToDir = false
}
}
if ((!isSymlink && entry.isDirectory()) || isLinkPointingToDir) {
// Why: resolve a symlinked plugins/ to its real target so <overlay>/plugins stays a real dir and writePluginIntoOverlay can't write through the user's link.
const resolvedSource = isLinkPointingToDir ? realpathSync(sourcePath) : sourcePath
const overlayPluginsDir = join(overlayDir, 'plugins')
mkdirSync(overlayPluginsDir, { recursive: true })
for (const pluginEntry of readdirSync(resolvedSource, { withFileTypes: true })) {
// Why: skip a user plugin sharing Orca's filename; mirroring it would let writePluginIntoOverlay clobber the user's file.
if (pluginEntry.name === ORCA_OPENCODE_PLUGIN_FILE) {
continue
}
mirrorEntry(
join(resolvedSource, pluginEntry.name),
join(overlayPluginsDir, pluginEntry.name)
)
nextManifest.pluginEntries.push(pluginEntry.name)
}
continue
}
}
mirrorEntry(sourcePath, join(overlayDir, entry.name))
nextManifest.topLevelEntries.push(entry.name)
}
this.writeOverlayManifest(overlayDir, nextManifest)
}
// Why: pre-write unlink guards against POSIX writeFileSync writing through a mirrored symlink and clobbering a same-named user plugin.
@@ -440,10 +520,8 @@ export class OpenCodeHookService {
const pluginPath = join(pluginsDir, ORCA_OPENCODE_PLUGIN_FILE)
try {
unlinkSync(pluginPath)
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
throw error
}
} catch {
// File may not exist on a fresh overlay; a real failure surfaces on writeFileSync below.
}
writeFileSync(pluginPath, getOpenCodePluginSource())
}
@@ -453,7 +531,7 @@ export class OpenCodeHookService {
const pluginsDir = join(configDir, 'plugins')
try {
mkdirSync(pluginsDir, { recursive: true })
this.writePluginIntoOverlay(configDir)
writeFileSync(join(pluginsDir, ORCA_OPENCODE_PLUGIN_FILE), getOpenCodePluginSource())
} catch {
// Why: userData can be locked on Windows (EPERM/EBUSY); plugin is non-critical, so spawn without it.
return null
@@ -1,46 +0,0 @@
import { describe, expect, it } from 'vitest'
import { collectOpenCodeDatabaseFiles } from './opencode-database-files'
function entry(name: string, file = true): { name: string; isFile(): boolean } {
return { name, isFile: () => file }
}
describe('OpenCode database file discovery', () => {
it('preserves and sorts every matching file within the limit', async () => {
const result = await collectOpenCodeDatabaseFiles(
'/data',
(async function* () {
yield entry('opencode-z.db')
yield entry('notes.txt')
yield entry('opencode.db')
yield entry('opencode-directory.db', false)
})(),
2
)
expect(result).toEqual({
paths: ['/data/opencode-z.db', '/data/opencode.db'].sort(),
truncated: false
})
})
it('stops retaining names immediately after the database count limit', async () => {
let enumerated = 0
const result = await collectOpenCodeDatabaseFiles(
'/data',
(async function* () {
for (const name of ['opencode-a.db', 'opencode-b.db', 'opencode-c.db', 'opencode-d.db']) {
enumerated += 1
yield entry(name)
}
})(),
2
)
expect(result).toEqual({
paths: ['/data/opencode-a.db', '/data/opencode-b.db'],
truncated: true
})
expect(enumerated).toBe(3)
})
})
@@ -1,40 +0,0 @@
import { opendir } from 'node:fs/promises'
import { join } from 'node:path'
export const OPENCODE_DATABASE_FILE_LIMIT = 256
type OpenCodeDatabaseDirectoryEntry = {
name: string
isFile(): boolean
}
export type OpenCodeDatabaseFiles = {
paths: string[]
truncated: boolean
}
export async function listOpenCodeDatabaseFiles(dataDir: string): Promise<OpenCodeDatabaseFiles> {
try {
return collectOpenCodeDatabaseFiles(dataDir, await opendir(dataDir))
} catch {
return { paths: [], truncated: false }
}
}
export async function collectOpenCodeDatabaseFiles(
dataDir: string,
directory: AsyncIterable<OpenCodeDatabaseDirectoryEntry>,
maxFiles = OPENCODE_DATABASE_FILE_LIMIT
): Promise<OpenCodeDatabaseFiles> {
const paths: string[] = []
for await (const entry of directory) {
if (!entry.isFile() || !/^opencode(?:-[A-Za-z0-9_.-]+)?\.db$/.test(entry.name)) {
continue
}
if (paths.length >= maxFiles) {
return { paths: paths.sort(), truncated: true }
}
paths.push(join(dataDir, entry.name))
}
return { paths: paths.sort(), truncated: false }
}