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
4 changes: 2 additions & 2 deletions packages/omo-senpi/plugin/extensions/omo.js

Large diffs are not rendered by default.

49 changes: 49 additions & 0 deletions packages/omo-senpi/src/components/config-watch/paths.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,55 @@ describe("resolveOmoConfigWatchTargets", () => {
const configTarget = targets.find((target) => target.path === omoDirectory)
expect(configTarget?.filterGlobs).toEqual(["/omo.jsonc", "/omo.json"])
})

it("#given a Plan 9 project filesystem #when resolving targets #then skips project and ancestor watches while retaining native user config", () => {
const fixture = createFixture()
const userConfigDirectory = join(fixture.homeDir, ".omo")
mkdirSync(userConfigDirectory, { recursive: true })
writeProjectConfig(fixture.projectDir)

const targets = resolveOmoConfigWatchTargets({
cwd: fixture.cwd,
env: fixtureEnv(fixture),
platform: "linux",
resolveFileSystemType: (path) => path === userConfigDirectory ? 0x01021994 : 0x01021997,
})

expect(targetFor(targets, userConfigDirectory, "/omo.jsonc")).toBe(true)
expect(targets.some((target) => target.path === fixture.projectDir && target.filterGlobs.includes("/.omo"))).toBe(false)
expect(targets.some((target) => target.filterGlobs.includes("/.omo"))).toBe(false)
})

it("#given a native project filesystem #when resolving targets #then preserves project and ancestor watches", () => {
const fixture = createFixture()
writeProjectConfig(fixture.projectDir)

const targets = resolveOmoConfigWatchTargets({
cwd: fixture.cwd,
env: fixtureEnv(fixture),
platform: "linux",
resolveFileSystemType: () => 0x01021994,
})

expect(targetFor(targets, join(fixture.projectDir, ".omo"), "/omo.jsonc")).toBe(true)
expect(targets.some((target) => target.filterGlobs.includes("/.omo"))).toBe(true)
})

it("#given a Plan 9 user config filesystem #when resolving targets #then skips the user config watch", () => {
const fixture = createFixture()
const userConfigDirectory = join(fixture.homeDir, ".omo")
mkdirSync(userConfigDirectory, { recursive: true })

const targets = resolveOmoConfigWatchTargets({
cwd: fixture.cwd,
env: fixtureEnv(fixture),
platform: "linux",
resolveFileSystemType: () => 0x01021997,
})

expect(targetFor(targets, userConfigDirectory, "/omo.jsonc")).toBe(false)
expect(targets.some((target) => target.filterGlobs.includes("/.omo"))).toBe(false)
})
})

process.on("beforeExit", () => {
Expand Down
47 changes: 38 additions & 9 deletions packages/omo-senpi/src/components/config-watch/paths.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { lstatSync, statSync } from "node:fs"
import * as nodeFs from "node:fs"
import { resolveAgentHome } from "../agent-home/resolve-agent-home"
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path"

Expand All @@ -12,7 +13,9 @@ import {
} from "@oh-my-opencode/omo-config-core"

const MAX_ANCESTOR_WATCH_TARGETS = 128
const SENPI_AGENT_DIR_ENV = "SENPI_CODING_AGENT_DIR"
// WSL exposes Windows drives as Plan 9/v9fs mounts. Keep this exact value
// distinct from nearby filesystem magic values such as tmpfs (0x01021994).
export const PLAN9_FILE_SYSTEM_TYPE = 0x01021997

// Root-anchored: senpi maps a `dir` target to a RECURSIVE watch, so unanchored
// globs make it hash the entire subtree. A `.omo` directory also holds runtime
Expand All @@ -35,10 +38,28 @@ export interface OmoConfigWatchTarget {
readonly filterGlobs: string[]
}

export type FileSystemTypeResolver = (path: string) => number | null

type NodeStatFsSync = (path: string) => { readonly type: number }

function createDefaultFileSystemTypeResolver(platform: NodeJS.Platform): FileSystemTypeResolver {
if (platform !== "linux") return () => null
const statFsSync = (nodeFs as typeof nodeFs & { statfsSync?: NodeStatFsSync }).statfsSync
if (typeof statFsSync !== "function") return () => null
return (path) => {
try {
return statFsSync(path).type
} catch {
return null
}
}
}

export interface ResolveOmoConfigWatchTargetsOptions {
readonly cwd: string
readonly env?: OmoConfigEnv
readonly platform?: NodeJS.Platform
readonly resolveFileSystemType?: FileSystemTypeResolver
}

export interface OmoConfigWatchTargetResolution {
Expand Down Expand Up @@ -148,6 +169,8 @@ export function resolveOmoConfigWatchTargetResolution(
): OmoConfigWatchTargetResolution {
const env = options.env ?? process.env
const platform = options.platform ?? process.platform
const resolveFileSystemType = options.resolveFileSystemType ?? createDefaultFileSystemTypeResolver(platform)
const isOnPlan9FileSystem = (path: string): boolean => resolveFileSystemType(path) === PLAN9_FILE_SYSTEM_TYPE
const userConfigDirectory = resolveUserOmoConfigDirectory(env)
const ancestorDirectories = findAncestorDirectories(options.cwd, resolveHomeDir(env))
const resolvedConfigPaths = resolveOmoConfigPaths({ cwd: options.cwd, env, platform })
Expand All @@ -164,20 +187,26 @@ export function resolveOmoConfigWatchTargetResolution(
const targets: OmoConfigWatchTarget[] = []

if (isExistingDirectory(userConfigDirectory)) {
targets.push(configTarget(userConfigDirectory))
if (!isOnPlan9FileSystem(userConfigDirectory)) targets.push(configTarget(userConfigDirectory))
} else {
const userConfigParent = dirname(userConfigDirectory)
if (isExistingDirectory(userConfigParent)) targets.push(userConfigCreationTarget(userConfigParent, userConfigDirectory))
if (isExistingDirectory(userConfigParent) && !isOnPlan9FileSystem(userConfigParent)) {
targets.push(userConfigCreationTarget(userConfigParent, userConfigDirectory))
}
}

for (const ancestorDirectory of ancestorDirectories) {
const omoDirectory = join(ancestorDirectory, ".omo")
if (configuredProjectDirectories.has(omoDirectory) || isExistingNonSymlinkDirectory(omoDirectory)) {
targets.push(configTarget(omoDirectory))
// The ancestor walk remains on the cwd's mount, so one probe covers every
// project config and creation target without probing potentially slow paths.
if (!isOnPlan9FileSystem(resolve(options.cwd))) {
for (const ancestorDirectory of ancestorDirectories) {
const omoDirectory = join(ancestorDirectory, ".omo")
if (configuredProjectDirectories.has(omoDirectory) || isExistingNonSymlinkDirectory(omoDirectory)) {
targets.push(configTarget(omoDirectory))
}
}
}

for (const ancestorDirectory of ancestorDirectories) targets.push(creationTarget(ancestorDirectory))
for (const ancestorDirectory of ancestorDirectories) targets.push(creationTarget(ancestorDirectory))
}

const senpiProtectedPaths = resolveSenpiProtectedPaths(env)
const permittedTargets = targets.filter((target) => !isSenpiRestrictedTarget(target, senpiProtectedPaths))
Expand Down
Loading