mirror of
https://github.com/stablyai/orca.git
synced 2026-10-03 00:02:19 +00:00
fix(opencode): keep OpenCode 2 panes Working across plugin reloads (#23700)
* fix(opencode): keep OpenCode 2 panes Working across plugin reloads OpenCode 2 disposes and re-sets-up every plugin whenever its plugins dir changes, while sessions keep running. The status plugin published a final Idle on dispose, so a pane read Done mid-turn. Orca also rewrote the plugin file on every PTY spawn, so opening any terminal triggered that reload. Dispose now releases the factory's bookkeeping without publishing a verdict; the next lifecycle event settles the pane, and Orca's ended-process reconciliation still retires panes whose agent exited. The plugin file is written only when its bytes differ. * fix(opencode): skip rewriting an unchanged plugin in the SSH relay install too The relay's canonical-config install still unlinked and rewrote the status plugin on every OpenCode launch over SSH, which restarts every plugin in a remote OpenCode 2 server. Share one install-currency check (lstat + the existing byte comparison) between the local and relay writers, and pin write-if-changed with mtime so the tests also fail on filesystems that reuse a freed inode. * fix(opencode): keep the final Idle when OpenCode 1 tears its instance down OpenCode 1 disposes a plugin only when it tears the instance down, and that teardown cancels every running session, so the Idle published on dispose is true there; the cancelled run's own idle may never reach the plugin. Only OpenCode 2 disposes on a hot reload while turns keep running. The generated module serves both hosts, so the OpenCode 2 setup() entry point now tells the shared factory that sessions outlive disposal; the server() path keeps the previous disposal behaviour, including the hand-off to a surviving factory. * fix(opencode): compare a symlinked plugin by its target before rewriting OpenCode 2 loads plugins through file-level symlinks and reads the revision from the target's mtime, so a user whose Orca plugin file is a symlink (per-file dotfile managers) failed the regular-file check and got a write through the link, and a reload, on every spawn. The config-dir and relay installs now skip the write when the resolved target already has Orca's bytes; when stale they behave as before. Only the per-source overlay keeps the regular-file check, since a link there mirrors a user entry. Installers also skip the write inside a guarded block rather than returning early, so later install steps still run. * test(opencode): skip the plugin symlink tests on Windows like their neighbours Creating a file symlink on Windows needs Developer Mode or admin rights. * test(opencode): stub fetch without a type assertion in the dispose host test
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import { mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
isInstalledOpenCodePluginCurrent,
|
||||
isOverlayOpenCodePluginCurrent
|
||||
} from './opencode-installed-plugin'
|
||||
|
||||
describe('installed OpenCode plugin currency', () => {
|
||||
let dir: string
|
||||
let pluginPath: string
|
||||
|
||||
beforeEach(() => {
|
||||
dir = mkdtempSync(join(tmpdir(), 'orca-opencode-installed-plugin-'))
|
||||
pluginPath = join(dir, 'orca-opencode-status.js')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('treats a missing or different file as stale', () => {
|
||||
expect(isInstalledOpenCodePluginCurrent(pluginPath, 'plugin')).toBe(false)
|
||||
expect(isOverlayOpenCodePluginCurrent(pluginPath, 'plugin')).toBe(false)
|
||||
writeFileSync(pluginPath, 'older plugin')
|
||||
expect(isInstalledOpenCodePluginCurrent(pluginPath, 'plugin')).toBe(false)
|
||||
expect(isOverlayOpenCodePluginCurrent(pluginPath, 'plugin')).toBe(false)
|
||||
})
|
||||
|
||||
it('treats a regular file with the same bytes as current', () => {
|
||||
writeFileSync(pluginPath, 'plugin')
|
||||
expect(isInstalledOpenCodePluginCurrent(pluginPath, 'plugin')).toBe(true)
|
||||
expect(isOverlayOpenCodePluginCurrent(pluginPath, 'plugin')).toBe(true)
|
||||
})
|
||||
|
||||
it.skipIf(process.platform === 'win32')(
|
||||
'follows a symlink except in an overlay, where a link is a mirrored user entry',
|
||||
() => {
|
||||
const targetPath = join(dir, 'target.js')
|
||||
writeFileSync(targetPath, 'plugin')
|
||||
symlinkSync(targetPath, pluginPath)
|
||||
expect(isInstalledOpenCodePluginCurrent(pluginPath, 'plugin')).toBe(true)
|
||||
expect(isOverlayOpenCodePluginCurrent(pluginPath, 'plugin')).toBe(false)
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
import { lstatSync } from 'node:fs'
|
||||
import { nodeFileContentsEqualSync } from './node-file-content-equality'
|
||||
|
||||
// Why: OpenCode 2 reloads a plugin (and every plugin loaded after it) when its file's mtime changes,
|
||||
// even with identical bytes, so installers must skip the write when Orca's plugin is already current.
|
||||
// Follows symlinks: OpenCode 2 loads through them, so a linked file with Orca's bytes is current.
|
||||
export function isInstalledOpenCodePluginCurrent(pluginPath: string, source: string): boolean {
|
||||
try {
|
||||
return nodeFileContentsEqualSync(pluginPath, source)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Overlay variant: a symlink there mirrors a user entry, never Orca's file, even when the bytes match.
|
||||
export function isOverlayOpenCodePluginCurrent(pluginPath: string, source: string): boolean {
|
||||
try {
|
||||
return lstatSync(pluginPath).isFile() && nodeFileContentsEqualSync(pluginPath, source)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user