omo-ai 5.1.1 → 5.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/lib/claude-code-floor.js +8 -4
- package/bin/lib/doctor-migration.js +22 -7
- package/bin/lib/doctor-pi-config.js +70 -0
- package/bin/lib/doctor.js +2 -0
- package/bin/lib/engine-dependency.js +23 -0
- package/bin/lib/engine-models.js +5 -2
- package/bin/lib/provider-map.json +1 -1
- package/bin/lib/setup-opencode-models.d.ts +53 -0
- package/bin/lib/setup-opencode-models.js +121 -34
- package/package.json +2 -3
- package/plugin/CHANGELOG.md +60 -0
- package/plugin/extensions/memory-run-supervisor.mjs +1 -1
- package/plugin/extensions/omo-computer-use.js +1 -1
- package/plugin/extensions/omo-init-deep-advisor.js +1 -1
- package/plugin/extensions/omo-member.js +1 -1
- package/plugin/extensions/omo-task.js +2 -2
- package/plugin/extensions/omo.js +2 -2
- package/plugin/package.json +1 -1
- package/plugin/runtime/agent-toolkit-sdk/sdk.js +2 -2
- package/plugin/runtime/ast-grep-mcp/manifest.json +1 -1
- package/plugin/runtime/category-coverage/assets.generated.json +7 -0
- package/plugin/runtime/rollback-migrate.js +1 -1
- package/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
- package/plugin/skills/hyperplan/SKILL.md +1 -0
- package/plugin/skills/init-deep/SKILL.md +1 -0
- package/plugin/skills/mass-ulw/SKILL.md +1 -0
- package/plugin/skills/refactor/SKILL.md +1 -0
- package/plugin/skills/remove-ai-slops/SKILL.md +1 -0
- package/plugin/skills/ulw-execute/SKILL.md +1 -0
- package/plugin/skills/ulw-loop/SKILL.md +1 -0
- package/plugin/skills/ulw-plan/SKILL.md +1 -0
- package/plugin/skills/ulw-research/SKILL.md +1 -0
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
2
2
|
import { join } from "node:path"
|
|
3
|
+
import { engineDependencyDir } from "./engine-dependency.js"
|
|
3
4
|
|
|
4
|
-
const
|
|
5
|
+
const piAiPackage = "@earendil-works/pi-ai"
|
|
6
|
+
const claudeCodeVersionRelative = "dist/api/anthropic-messages.js"
|
|
7
|
+
const claudeCodeVersionTarget = `${piAiPackage}/${claudeCodeVersionRelative}`
|
|
5
8
|
const claudeCodeVersionPattern = /const claudeCodeVersion = "(\d+)\.(\d+)\.(\d+)";/
|
|
6
9
|
// Claude Opus 5.5 rejects OAuth requests advertising Claude Code below 2.1.280 (claude_code_version_too_old).
|
|
7
10
|
export const claudeCodeVersionFloor = "2.1.280"
|
|
@@ -13,11 +16,12 @@ function isBelowFloor([major, minor, patch]) {
|
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
function floorPiAi(senpiRoot) {
|
|
16
|
-
const
|
|
17
|
-
|
|
19
|
+
const piAiRoot = engineDependencyDir(senpiRoot, piAiPackage)
|
|
20
|
+
const path = piAiRoot === undefined ? undefined : join(piAiRoot, claudeCodeVersionRelative)
|
|
21
|
+
if (path === undefined || !existsSync(path)) throw new Error(`omo-ai: installed Senpi target is missing: ${claudeCodeVersionTarget}`)
|
|
18
22
|
const source = readFileSync(path, "utf8")
|
|
19
23
|
const match = claudeCodeVersionPattern.exec(source)
|
|
20
|
-
if (match === null) throw new Error(`omo-ai: unsupported Senpi ${
|
|
24
|
+
if (match === null) throw new Error(`omo-ai: unsupported Senpi ${claudeCodeVersionTarget}`)
|
|
21
25
|
if (!isBelowFloor(match.slice(1).map(Number))) return
|
|
22
26
|
writeFileSync(path, source.replace(claudeCodeVersionPattern, `const claudeCodeVersion = "${claudeCodeVersionFloor}";`))
|
|
23
27
|
}
|
|
@@ -4,6 +4,7 @@ import { delimiter, dirname, isAbsolute, join, win32 } from "node:path"
|
|
|
4
4
|
import { parseJsonc } from "./jsonc.js"
|
|
5
5
|
import { releaseChannel } from "./package-paths.js"
|
|
6
6
|
import { opencodeConfigSources } from "./setup-opencode-assets.js"
|
|
7
|
+
import { openCodeRoutingGap, openCodeRoutingNotice, readEditionRouting } from "./setup-opencode-models.js"
|
|
7
8
|
import { standaloneBinaryVersion } from "./standalone-binary.js"
|
|
8
9
|
|
|
9
10
|
// Migration leftovers from the OpenCode edition, reported and never touched: another `omo` ahead of
|
|
@@ -321,16 +322,30 @@ export function formatMigrationLines({ shadowing, nativeDirectory, legacyPackage
|
|
|
321
322
|
return lines
|
|
322
323
|
}
|
|
323
324
|
|
|
325
|
+
// The OpenCode edition's model settings Native does not use: the same line Native's first start
|
|
326
|
+
// prints (omo-senpi config-startup), for as long as the gap exists.
|
|
327
|
+
export function openCodeRoutingReport({ env, homeDir }) {
|
|
328
|
+
const path = ["omo.jsonc", "omo.json"].map((name) => join(homeDir, ".omo", name)).find((candidate) => readWholeFile(candidate) !== undefined)
|
|
329
|
+
let document = {}
|
|
330
|
+
if (path !== undefined) {
|
|
331
|
+
try {
|
|
332
|
+
document = parseJsonc(readWholeFile(path))
|
|
333
|
+
} catch {
|
|
334
|
+
return [] // The config loader reports an unreadable omo.jsonc itself.
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const gap = openCodeRoutingGap(document, readEditionRouting({ home: homeDir, env }))
|
|
338
|
+
return gap.length === 0 ? [] : [`INFO ${openCodeRoutingNotice(gap)}`]
|
|
339
|
+
}
|
|
340
|
+
|
|
324
341
|
/** The doctor's migration section. `options.env` / `homeDir` / `platform` keep tests off the real machine. */
|
|
325
342
|
export function migrationReport(options, restoreCommand) {
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
homeDir: options.homeDir ?? homedir(),
|
|
330
|
-
})
|
|
343
|
+
const env = options.env ?? process.env
|
|
344
|
+
const homeDir = options.homeDir ?? homedir()
|
|
345
|
+
const environment = resolveMigrationEnvironment({ env, platform: options.platform ?? process.platform, homeDir })
|
|
331
346
|
const bins = scanOmoBins(environment)
|
|
332
347
|
const omoInstalls = omoInstallsOnPath(bins)
|
|
333
|
-
return formatMigrationLines({
|
|
348
|
+
return [...formatMigrationLines({
|
|
334
349
|
shadowing: shadowingOmoBins(bins),
|
|
335
350
|
nativeDirectory: omoInstalls[0]?.directory ?? null,
|
|
336
351
|
legacyPackages: findLegacyPackages(environment),
|
|
@@ -339,5 +354,5 @@ export function migrationReport(options, restoreCommand) {
|
|
|
339
354
|
omoInstalls,
|
|
340
355
|
bunRoot: environment.bunRoot,
|
|
341
356
|
standalone: options.standalone === true,
|
|
342
|
-
})
|
|
357
|
+
}), ...openCodeRoutingReport({ env, homeDir })]
|
|
343
358
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { canonicalAgentDir, runtimeHome } from "./agent-dir.js"
|
|
4
|
+
|
|
5
|
+
// Mirrors the engine's legacy-pi-edits check (senpi `src/legacy-pi-edits.ts`), which this package
|
|
6
|
+
// cannot import synchronously: the same four files, the same `legacyPiAgentDir.copiedAt` record in
|
|
7
|
+
// migrations-state.json, and the same rule: newer than the recorded copy (or, before the time was
|
|
8
|
+
// recorded, than the agent copy's preserved mtime) and different in content. `~/.pi/agent` is only
|
|
9
|
+
// read (#9173).
|
|
10
|
+
export const PI_CONFIG_FILES = ["auth.json", "keybindings.json", "models.json", "settings.json"]
|
|
11
|
+
|
|
12
|
+
function fileStat(path) {
|
|
13
|
+
try {
|
|
14
|
+
const stat = statSync(path)
|
|
15
|
+
return stat.isFile() ? stat : undefined
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (error?.code === "ENOENT" || error?.code === "ENOTDIR") return undefined
|
|
18
|
+
throw error
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function recordedCopyTime(agentDir) {
|
|
23
|
+
let state
|
|
24
|
+
try {
|
|
25
|
+
state = JSON.parse(readFileSync(join(agentDir, "migrations-state.json"), "utf8"))
|
|
26
|
+
} catch {
|
|
27
|
+
// A missing or hand-broken state file means no recorded copy time; the rule falls back.
|
|
28
|
+
return undefined
|
|
29
|
+
}
|
|
30
|
+
const copiedAt = state?.schemaVersion === 1 ? state.legacyPiAgentDir?.copiedAt : undefined
|
|
31
|
+
return typeof copiedAt === "number" && Number.isFinite(copiedAt) ? copiedAt : undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function findPiConfigEdits({ agentDir, homeDir }) {
|
|
35
|
+
const piAgentDir = join(homeDir, ".pi", "agent")
|
|
36
|
+
if (!existsSync(piAgentDir)) return []
|
|
37
|
+
if (existsSync(agentDir) && realpathSync(agentDir) === realpathSync(piAgentDir)) return []
|
|
38
|
+
const copiedAt = recordedCopyTime(agentDir)
|
|
39
|
+
const edits = []
|
|
40
|
+
for (const file of PI_CONFIG_FILES) {
|
|
41
|
+
const piPath = join(piAgentDir, file)
|
|
42
|
+
const agentPath = join(agentDir, file)
|
|
43
|
+
const pi = fileStat(piPath)
|
|
44
|
+
if (pi === undefined) continue
|
|
45
|
+
const agent = fileStat(agentPath)
|
|
46
|
+
if (pi.mtimeMs <= (copiedAt ?? agent?.mtimeMs ?? 0)) continue
|
|
47
|
+
if (agent !== undefined && readFileSync(piPath).equals(readFileSync(agentPath))) continue
|
|
48
|
+
edits.push({ file, piPath, agentPath })
|
|
49
|
+
}
|
|
50
|
+
return edits
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function formatPiConfigLines(agentDir, edits) {
|
|
54
|
+
return [
|
|
55
|
+
`INFO config dir: ${agentDir}`,
|
|
56
|
+
...edits.map((edit) =>
|
|
57
|
+
`WARN You edited ${edit.piPath} after omo moved to ${agentDir}; omo reads ${edit.agentPath}. Copy your change there (or run: omo config import-pi ${edit.file}).`),
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function piConfigReport(options = {}) {
|
|
62
|
+
const env = options.env ?? process.env
|
|
63
|
+
const agentDir = options.agentDir ?? canonicalAgentDir(env)
|
|
64
|
+
const homeDir = options.homeDir ?? runtimeHome(env)
|
|
65
|
+
try {
|
|
66
|
+
return formatPiConfigLines(agentDir, findPiConfigEdits({ agentDir, homeDir }))
|
|
67
|
+
} catch (error) {
|
|
68
|
+
return [`INFO config dir: ${agentDir}`, `WARN could not check ${join(homeDir, ".pi", "agent")} for config edits: ${error.message}`]
|
|
69
|
+
}
|
|
70
|
+
}
|
package/bin/lib/doctor.js
CHANGED
|
@@ -6,6 +6,7 @@ import { canonicalAgentDir } from "./agent-dir.js"
|
|
|
6
6
|
import { packageManifest, packageRoot, readJson, releaseChannel, resolveSenpi, updateTarget } from "./package-paths.js"
|
|
7
7
|
import { daemonReportLines } from "./daemon.js"
|
|
8
8
|
import { migrationReport } from "./doctor-migration.js"
|
|
9
|
+
import { piConfigReport } from "./doctor-pi-config.js"
|
|
9
10
|
import { needsSetupSuggestion } from "./setup-detect.js"
|
|
10
11
|
|
|
11
12
|
const NPM_DIST_TAGS_URL = "https://registry.npmjs.org/-/package/omo-ai/dist-tags"
|
|
@@ -430,6 +431,7 @@ export function runDoctor(inventory, args = [], options = {}) {
|
|
|
430
431
|
lines.push(`INFO Update: ${updateTarget().command}`)
|
|
431
432
|
lines.push(...migrationReport(options, updateTarget().command))
|
|
432
433
|
lines.push(...warningsForSettings())
|
|
434
|
+
lines.push(...piConfigReport({ env: options.env, homeDir: options.homeDir }))
|
|
433
435
|
lines.push(...staleEngineReport(options))
|
|
434
436
|
lines.push(...retiredPayloadReport(options))
|
|
435
437
|
lines.push(...transientMemoryReport(options))
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { realpathSync, statSync } from "node:fs"
|
|
2
|
+
import { basename, dirname, join } from "node:path"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The directory of `name` as the engine installed at `senpiRoot` resolves it: Node's
|
|
6
|
+
* node_modules walk from senpi's real path. That one walk covers every install shape - a
|
|
7
|
+
* bundled senpi keeps the dependency in its own `node_modules`, npm hoists it beside senpi,
|
|
8
|
+
* and bun's isolated store links it beside senpi's real path in `.bun/<key>/node_modules` -
|
|
9
|
+
* so no caller may assume where the dependency lives. Returns undefined when nothing on the
|
|
10
|
+
* walk provides `name`.
|
|
11
|
+
*/
|
|
12
|
+
export function engineDependencyDir(senpiRoot, name) {
|
|
13
|
+
let current = realpathSync(senpiRoot)
|
|
14
|
+
while (true) {
|
|
15
|
+
if (basename(current) !== "node_modules") {
|
|
16
|
+
const candidate = join(current, "node_modules", name)
|
|
17
|
+
if (statSync(candidate, { throwIfNoEntry: false })?.isDirectory()) return candidate
|
|
18
|
+
}
|
|
19
|
+
const parent = dirname(current)
|
|
20
|
+
if (parent === current) return undefined
|
|
21
|
+
current = parent
|
|
22
|
+
}
|
|
23
|
+
}
|
package/bin/lib/engine-models.js
CHANGED
|
@@ -11,13 +11,16 @@ import { existsSync, readFileSync } from "node:fs"
|
|
|
11
11
|
import { join } from "node:path"
|
|
12
12
|
import { pathToFileURL } from "node:url"
|
|
13
13
|
import { parseJsonc } from "./jsonc.js"
|
|
14
|
+
import { engineDependencyDir } from "./engine-dependency.js"
|
|
14
15
|
import { resolveSenpi } from "./package-paths.js"
|
|
15
16
|
|
|
16
17
|
// The same module the engine's model-runtime.js composes its builtins from
|
|
17
|
-
// (`builtinProviderCatalog.builtinProviders()`),
|
|
18
|
+
// (`builtinProviderCatalog.builtinProviders()`), in the pi-ai the engine itself resolves.
|
|
18
19
|
async function builtinCatalog() {
|
|
19
20
|
const { packageRoot } = resolveSenpi()
|
|
20
|
-
const
|
|
21
|
+
const piAiRoot = engineDependencyDir(packageRoot, "@earendil-works/pi-ai")
|
|
22
|
+
if (piAiRoot === undefined) throw new Error(`the engine model catalog is missing: @earendil-works/pi-ai is not resolvable from ${packageRoot}`)
|
|
23
|
+
const path = join(piAiRoot, "dist", "providers", "all.js")
|
|
21
24
|
if (!existsSync(path)) throw new Error(`the engine model catalog is missing at ${path}`)
|
|
22
25
|
return (await import(pathToFileURL(path).href)).builtinProviders()
|
|
23
26
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_comment": "Derived from @code-yeongyu/senpi 2026.9.29 builtinProviders(). builtinProviderIds and oauthProviderIds are the engine registry verbatim; providers maps a source harness id onto the omo provider that serves the same endpoint; oauthLogins maps a source OAuth id onto the omo provider to run /login for. Re-derive all four whenever the senpi pin changes.",
|
|
2
|
+
"_comment": "Derived from @code-yeongyu/senpi 2026.9.29-3 builtinProviders(). builtinProviderIds and oauthProviderIds are the engine registry verbatim; providers maps a source harness id onto the omo provider that serves the same endpoint; oauthLogins maps a source OAuth id onto the omo provider to run /login for. Re-derive all four whenever the senpi pin changes.",
|
|
3
3
|
"builtinProviderIds": [
|
|
4
4
|
"alibaba-token-plan",
|
|
5
5
|
"amazon-bedrock",
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Types for setup-opencode-models.js, which omo-senpi's config-startup imports across the package
|
|
2
|
+
// boundary: omo-ai ships this module as plain JS, so it is the single source of the mapping.
|
|
3
|
+
|
|
4
|
+
export type ModelRegistry = {
|
|
5
|
+
readonly models: ReadonlyMap<string, ReadonlySet<string>>
|
|
6
|
+
readonly dynamic: ReadonlySet<string>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type EditionRouting = {
|
|
10
|
+
readonly agents: Readonly<Record<string, unknown>>
|
|
11
|
+
readonly categories: Readonly<Record<string, unknown>>
|
|
12
|
+
readonly notices: readonly string[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type ConvertedChoice = {
|
|
16
|
+
readonly name: string
|
|
17
|
+
readonly entry: Readonly<Record<string, unknown>>
|
|
18
|
+
readonly from: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const NATIVE_AGENT_NAMES: readonly string[]
|
|
22
|
+
|
|
23
|
+
export function readEditionRouting(options: { home: string; env: Readonly<Record<string, string | undefined>> }): EditionRouting
|
|
24
|
+
|
|
25
|
+
export function readModelChoices(options: {
|
|
26
|
+
home: string
|
|
27
|
+
env: Readonly<Record<string, string | undefined>>
|
|
28
|
+
opencodeBlock: unknown
|
|
29
|
+
}): Record<string, unknown>
|
|
30
|
+
|
|
31
|
+
export function hasModelChoices(raw: unknown): boolean
|
|
32
|
+
|
|
33
|
+
export function translateModelRef(
|
|
34
|
+
raw: unknown,
|
|
35
|
+
context: { registry?: ModelRegistry; oauthProviders: ReadonlySet<string> },
|
|
36
|
+
): { ref?: string; provider?: string; modelId?: string; reason?: string }
|
|
37
|
+
|
|
38
|
+
export function convertModelChoices(
|
|
39
|
+
raw: unknown,
|
|
40
|
+
registry: ModelRegistry,
|
|
41
|
+
oauthProviders?: ReadonlySet<string>,
|
|
42
|
+
): {
|
|
43
|
+
defaultModel?: { source: string; provider: string; modelId: string; ref: string }
|
|
44
|
+
categories: readonly ConvertedChoice[]
|
|
45
|
+
agents: readonly ConvertedChoice[]
|
|
46
|
+
dropped: readonly string[]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function nativeRoutingLine(section: string, name: string, value: Readonly<Record<string, unknown>>): string
|
|
50
|
+
|
|
51
|
+
export function openCodeRoutingGap(document: unknown, edition: EditionRouting): readonly string[]
|
|
52
|
+
|
|
53
|
+
export function openCodeRoutingNotice(lines: readonly string[]): string
|
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
* converts each into what the native harness reads. Provider ids go through provider-map.json, the
|
|
5
5
|
* table the credential stage uses, and every provider/model is checked against the engine's model
|
|
6
6
|
* list; what does not resolve is returned with its reason, never converted. Read-only.
|
|
7
|
+
*
|
|
8
|
+
* Native's startup migration (omo-senpi config-startup) runs the same conversion without the
|
|
9
|
+
* engine's model list through `openCodeRoutingGap`, so both paths map names and providers alike.
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
|
-
import { existsSync, readdirSync
|
|
12
|
+
import { existsSync, readdirSync } from "node:fs"
|
|
10
13
|
import { isAbsolute, join, relative, resolve } from "node:path"
|
|
14
|
+
import providerMap from "./provider-map.json" with { type: "json" }
|
|
11
15
|
import { opencodeConfigSources, readOpencodeConfig } from "./setup-opencode-assets.js"
|
|
12
16
|
|
|
13
17
|
// The OpenCode edition's legacy plugin files, highest precedence first, as its config migration
|
|
@@ -29,6 +33,9 @@ export const NATIVE_AGENT_NAMES = [
|
|
|
29
33
|
// OpenCode's own primary agents: the native main session has no per-mode agent, it runs the default model.
|
|
30
34
|
const OPENCODE_PRIMARY_AGENTS = new Set(["build", "plan"])
|
|
31
35
|
|
|
36
|
+
// The OpenCode edition's plan agents, keyed to the native agent that does the same job.
|
|
37
|
+
const AGENT_ALIASES = { metis: "plan-consultant", momus: "plan-reviewer" }
|
|
38
|
+
|
|
32
39
|
// omo-config-core schema/legacy-category-names.ts: the loader renames these and reports it.
|
|
33
40
|
const CATEGORY_ALIASES = { deep: "deep-low" }
|
|
34
41
|
|
|
@@ -44,10 +51,6 @@ function record(value) {
|
|
|
44
51
|
return isPlainObject(value) ? value : {}
|
|
45
52
|
}
|
|
46
53
|
|
|
47
|
-
function readProviderMap() {
|
|
48
|
-
return JSON.parse(readFileSync(new URL("./provider-map.json", import.meta.url), "utf8"))
|
|
49
|
-
}
|
|
50
|
-
|
|
51
54
|
// The first start of any current harness runs the config unification migration, which keeps none
|
|
52
55
|
// of a legacy file's categories or agents and moves the file to
|
|
53
56
|
// `~/.omo/migration-backup-<timestamp>-opencode-config/<path relative to home>`
|
|
@@ -69,12 +72,11 @@ function editionFile(path, home, notices) {
|
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
/**
|
|
72
|
-
* The
|
|
73
|
-
*
|
|
75
|
+
* The OpenCode edition's own routing (`agents`, `categories`) from its legacy plugin files, or the
|
|
76
|
+
* migration backups the unification migration moved them into.
|
|
74
77
|
*/
|
|
75
|
-
export function
|
|
78
|
+
export function readEditionRouting({ home, env }) {
|
|
76
79
|
const notices = []
|
|
77
|
-
const opencode = readOpencodeConfig(opencodeConfigSources(home, env).files, "model choices", notices)
|
|
78
80
|
const globalDir = join(env.XDG_CONFIG_HOME || join(home, ".config"), "opencode")
|
|
79
81
|
const explicitDir = env.OPENCODE_CONFIG_DIR?.trim()
|
|
80
82
|
const directories = explicitDir ? [resolve(explicitDir), globalDir] : [globalDir]
|
|
@@ -83,14 +85,31 @@ export function readModelChoices({ home, env, opencodeBlock }) {
|
|
|
83
85
|
.flatMap((directory) => EDITION_FILES.map((name) => editionFile(join(directory, name), home, notices)))
|
|
84
86
|
.reverse()
|
|
85
87
|
const edition = readOpencodeConfig(legacyFiles, "model choices", notices)
|
|
86
|
-
|
|
88
|
+
return { agents: record(edition.agents), categories: record(edition.categories), notices }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The raw choices, before any translation. `opencodeBlock` is the `[opencode]` block of the user's
|
|
93
|
+
* omo.json[c]: the unification migration's target, so an entry there wins over the legacy files.
|
|
94
|
+
*/
|
|
95
|
+
export function readModelChoices({ home, env, opencodeBlock }) {
|
|
96
|
+
const notices = []
|
|
97
|
+
const opencode = readOpencodeConfig(opencodeConfigSources(home, env).files, "model choices", notices)
|
|
98
|
+
const edition = readEditionRouting({ home, env })
|
|
87
99
|
return {
|
|
88
100
|
model: typeof opencode.model === "string" ? opencode.model : undefined,
|
|
89
101
|
smallModel: typeof opencode.small_model === "string" ? opencode.small_model : undefined,
|
|
90
102
|
opencodeAgents: record(opencode.agent),
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
103
|
+
...editionChoices(edition, opencodeBlock),
|
|
104
|
+
notices: [...notices, ...edition.notices],
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function editionChoices(edition, opencodeBlock) {
|
|
109
|
+
const block = record(opencodeBlock)
|
|
110
|
+
return {
|
|
111
|
+
categories: { ...edition.categories, ...record(block.categories) },
|
|
112
|
+
agents: { ...edition.agents, ...record(block.agents) },
|
|
94
113
|
}
|
|
95
114
|
}
|
|
96
115
|
|
|
@@ -104,12 +123,12 @@ export function hasModelChoices(raw) {
|
|
|
104
123
|
}
|
|
105
124
|
|
|
106
125
|
function engineProvider(provider, modelId, context) {
|
|
107
|
-
const {
|
|
126
|
+
const { oauthProviders, registry } = context
|
|
108
127
|
// A provider opencode signed in to with OAuth (`openai` through a ChatGPT plan) is served here by
|
|
109
128
|
// the provider the credential stage says to `/login` to (provider-map.json `oauthLogins`), not by
|
|
110
129
|
// the same id, which would need an API key the user never had.
|
|
111
130
|
const login = oauthProviders.has(provider) && Object.hasOwn(providerMap.oauthLogins, provider) ? providerMap.oauthLogins[provider] : undefined
|
|
112
|
-
if (login !== undefined && registry
|
|
131
|
+
if (login !== undefined && registry?.models.get(login)?.has(modelId)) return login
|
|
113
132
|
if (providerMap.builtinProviderIds.includes(provider)) return provider
|
|
114
133
|
return providerMap.providers[provider] ?? provider
|
|
115
134
|
}
|
|
@@ -125,20 +144,30 @@ function splitSuffix(text) {
|
|
|
125
144
|
return { selector: text.slice(0, colon), suffix: `:${token}` }
|
|
126
145
|
}
|
|
127
146
|
|
|
128
|
-
/**
|
|
147
|
+
/**
|
|
148
|
+
* `{ ref, provider, modelId }` for a model string the engine serves, else `{ reason }`. Without a
|
|
149
|
+
* `context.registry` (Native startup, which has no engine model list) only the provider is checked,
|
|
150
|
+
* against the engine's builtin providers.
|
|
151
|
+
*/
|
|
129
152
|
export function translateModelRef(raw, context) {
|
|
130
153
|
if (typeof raw !== "string" || raw.trim() === "") return { reason: "not a model string" }
|
|
131
154
|
const { selector, suffix } = splitSuffix(raw.trim())
|
|
132
155
|
const slash = selector.indexOf("/")
|
|
156
|
+
const { registry } = context
|
|
133
157
|
if (slash <= 0) {
|
|
134
158
|
// A bare id is matched against every provider by the native resolver, so any provider serving it will do.
|
|
135
|
-
const served = [...
|
|
159
|
+
const served = registry === undefined || [...registry.models.values()].some((ids) => ids.has(selector))
|
|
136
160
|
return served ? { ref: `${selector}${suffix}`, modelId: selector } : { reason: `no omo provider serves a model named ${selector}` }
|
|
137
161
|
}
|
|
138
162
|
const source = selector.slice(0, slash)
|
|
139
163
|
const modelId = selector.slice(slash + 1)
|
|
140
164
|
const provider = engineProvider(source, modelId, context)
|
|
141
|
-
|
|
165
|
+
if (registry === undefined) {
|
|
166
|
+
return providerMap.builtinProviderIds.includes(provider)
|
|
167
|
+
? { ref: `${provider}/${modelId}${suffix}`, provider, modelId }
|
|
168
|
+
: { reason: `provider ${source} is not one omo serves or was carried over` }
|
|
169
|
+
}
|
|
170
|
+
const ids = registry.models.get(provider)
|
|
142
171
|
if (ids === undefined) return { reason: `provider ${source} is not one omo serves or was carried over` }
|
|
143
172
|
if (!ids.has(modelId)) {
|
|
144
173
|
return {
|
|
@@ -180,10 +209,10 @@ function chainItem(item, take) {
|
|
|
180
209
|
* OpenCode edition tried them in. Undefined when the entry named models and none of them resolve:
|
|
181
210
|
* its tuning alone would retune the builtin chain the user had replaced.
|
|
182
211
|
*/
|
|
183
|
-
function convertEntry(label, entry, context, dropped) {
|
|
212
|
+
function convertEntry(label, entry, context, dropped, target) {
|
|
184
213
|
const take = (raw) => {
|
|
185
214
|
const result = translateModelRef(raw, context)
|
|
186
|
-
if (result.reason !== undefined) dropped.push(`${label} model ${String(raw)}: ${result.reason}
|
|
215
|
+
if (result.reason !== undefined) dropped.push({ text: `${label} model ${String(raw)}: ${result.reason}`, target })
|
|
187
216
|
return result.ref
|
|
188
217
|
}
|
|
189
218
|
const folded = !Array.isArray(entry.models) && entry.fallback_models !== undefined
|
|
@@ -208,17 +237,31 @@ function convertAgents(raw, context, dropped) {
|
|
|
208
237
|
for (const [origin, entries] of [["opencode agent", raw.opencodeAgents], ["agent", raw.agents]]) {
|
|
209
238
|
for (const [name, entry] of Object.entries(entries)) {
|
|
210
239
|
if (!hasModelChoice(entry)) continue
|
|
211
|
-
|
|
240
|
+
const target = Object.hasOwn(AGENT_ALIASES, name) ? AGENT_ALIASES[name] : name
|
|
241
|
+
// A native name beside its alias in the same source wins, as categories resolve the pair.
|
|
242
|
+
if (target !== name && Object.hasOwn(entries, target) && hasModelChoice(entries[target])) continue
|
|
243
|
+
if (!native.has(target)) {
|
|
212
244
|
dropped.push(OPENCODE_PRIMARY_AGENTS.has(name) && origin === "opencode agent"
|
|
213
|
-
? `${origin} ${name}: the OpenCode primary agent; omo's main session runs the default model instead`
|
|
214
|
-
:
|
|
245
|
+
? { text: `${origin} ${name}: the OpenCode primary agent; omo's main session runs the default model instead` }
|
|
246
|
+
: {
|
|
247
|
+
text: `${origin} ${name}: omo has no agent of that name (its agents: ${NATIVE_AGENT_NAMES.join(", ")}); route that work through a category instead`,
|
|
248
|
+
target: { section: "categories", name: "<category>", model: firstModel(entry, context) },
|
|
249
|
+
})
|
|
215
250
|
continue
|
|
216
251
|
}
|
|
217
|
-
const converted = convertEntry(`${origin} ${name}`, entry, context, dropped)
|
|
218
|
-
if (converted !== undefined) agents.set(
|
|
252
|
+
const converted = convertEntry(`${origin} ${name}`, entry, context, dropped, { section: "agents", name: target })
|
|
253
|
+
if (converted !== undefined) agents.set(target, { entry: converted, from: name })
|
|
219
254
|
}
|
|
220
255
|
}
|
|
221
|
-
return [...agents].map(([name, entry]) => ({ name, entry })).sort((left, right) => left.name.localeCompare(right.name))
|
|
256
|
+
return [...agents].map(([name, { entry, from }]) => ({ name, entry, from })).sort((left, right) => left.name.localeCompare(right.name))
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// The model an unmappable agent named, as the native harness would spell it when it can.
|
|
260
|
+
function firstModel(entry, context) {
|
|
261
|
+
const raw = entry.model ?? [entry.models, entry.fallback_models].flat().find((item) => item !== undefined)
|
|
262
|
+
const text = isPlainObject(raw) ? raw.model : raw
|
|
263
|
+
if (typeof text !== "string") return undefined
|
|
264
|
+
return translateModelRef(text, context).ref ?? text
|
|
222
265
|
}
|
|
223
266
|
|
|
224
267
|
function convertCategories(raw, context, dropped) {
|
|
@@ -228,21 +271,21 @@ function convertCategories(raw, context, dropped) {
|
|
|
228
271
|
const target = Object.hasOwn(CATEGORY_ALIASES, name) ? CATEGORY_ALIASES[name] : name
|
|
229
272
|
// A canonical key beside its retired alias wins, as the loader resolves the pair.
|
|
230
273
|
if (target !== name && Object.hasOwn(raw.categories, target)) continue
|
|
231
|
-
const converted = convertEntry(`category ${name}`, entry, context, dropped)
|
|
232
|
-
if (converted !== undefined) categories.set(target, converted)
|
|
274
|
+
const converted = convertEntry(`category ${name}`, entry, context, dropped, { section: "categories", name: target })
|
|
275
|
+
if (converted !== undefined) categories.set(target, { entry: converted, from: name })
|
|
233
276
|
}
|
|
234
|
-
return [...categories].map(([name, entry]) => ({ name, entry })).sort((left, right) => left.name.localeCompare(right.name))
|
|
277
|
+
return [...categories].map(([name, { entry, from }]) => ({ name, entry, from })).sort((left, right) => left.name.localeCompare(right.name))
|
|
235
278
|
}
|
|
236
279
|
|
|
237
280
|
function convertDefault(model, context, dropped) {
|
|
238
281
|
if (model === undefined) return undefined
|
|
239
282
|
const result = translateModelRef(model, context)
|
|
240
283
|
if (result.reason !== undefined) {
|
|
241
|
-
dropped.push(`default model ${model}: ${result.reason}`)
|
|
284
|
+
dropped.push({ text: `default model ${model}: ${result.reason}` })
|
|
242
285
|
return undefined
|
|
243
286
|
}
|
|
244
287
|
if (result.provider === undefined) {
|
|
245
|
-
dropped.push(`default model ${model}: names no provider, and omo's default model needs one`)
|
|
288
|
+
dropped.push({ text: `default model ${model}: names no provider, and omo's default model needs one` })
|
|
246
289
|
return undefined
|
|
247
290
|
}
|
|
248
291
|
return { source: model, provider: result.provider, modelId: result.modelId, ref: `${result.provider}/${result.modelId}` }
|
|
@@ -253,11 +296,55 @@ function convertDefault(model, context, dropped) {
|
|
|
253
296
|
* `oauthProviders` are the opencode provider ids whose opencode credential is an OAuth login.
|
|
254
297
|
*/
|
|
255
298
|
export function convertModelChoices(raw, registry, oauthProviders = new Set()) {
|
|
256
|
-
const context = { registry,
|
|
299
|
+
const context = { registry, oauthProviders }
|
|
257
300
|
const dropped = []
|
|
258
301
|
const defaultModel = convertDefault(raw.model, context, dropped)
|
|
259
|
-
if (raw.smallModel !== undefined) dropped.push(`small_model ${raw.smallModel}: omo has no small-model setting`)
|
|
302
|
+
if (raw.smallModel !== undefined) dropped.push({ text: `small_model ${raw.smallModel}: omo has no small-model setting` })
|
|
260
303
|
const categories = convertCategories(raw, context, dropped)
|
|
261
304
|
const agents = convertAgents(raw, context, dropped)
|
|
262
|
-
return { defaultModel, categories, agents, dropped }
|
|
305
|
+
return { defaultModel, categories, agents, dropped: dropped.map((item) => item.text) }
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// omo-config-core loader/resolution.ts folds a legacy `[senpi]` block under `[native]`.
|
|
309
|
+
const NATIVE_BLOCKS = ["[senpi]", "[native]"]
|
|
310
|
+
|
|
311
|
+
/** The omo.jsonc member that sets `value` for `section.name` in the native harness. */
|
|
312
|
+
export function nativeRoutingLine(section, name, value) {
|
|
313
|
+
const members = Object.entries(value).map(([key, member]) => `"${key}": ${JSON.stringify(member)}`).join(", ")
|
|
314
|
+
return `"[native]": { "${section}": { "${name}": { ${members} } } }`
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* The OpenCode edition's model settings (`edition`: readEditionRouting, overlaid by `document`'s
|
|
319
|
+
* `[opencode]` block) that the native harness of `document` (a user omo.json[c]) does not use,
|
|
320
|
+
* one line each with the omo.jsonc member that would set it natively. The legacy migration never
|
|
321
|
+
* copies agents or categories (#7270), and `omo setup` carries them only with the user's consent, so
|
|
322
|
+
* this only reports: Native's startup notice and `omo doctor` print these lines. Names and providers
|
|
323
|
+
* convert as setup converts them, without the engine's model list; a key the native view (shared
|
|
324
|
+
* base, `[senpi]`, `[native]`) already sets is the user's and is not reported.
|
|
325
|
+
*/
|
|
326
|
+
export function openCodeRoutingGap(document, edition) {
|
|
327
|
+
const raw = { opencodeAgents: {}, ...editionChoices(edition, record(document)["[opencode]"]) }
|
|
328
|
+
const context = { registry: undefined, oauthProviders: new Set() }
|
|
329
|
+
const dropped = []
|
|
330
|
+
const converted = { categories: convertCategories(raw, context, dropped), agents: convertAgents(raw, context, dropped) }
|
|
331
|
+
const carriable = ["agents", "categories"].flatMap((section) => converted[section]
|
|
332
|
+
.filter(({ name }) => !isConfigured(document, section, name))
|
|
333
|
+
.map(({ name, entry, from }) => `${section === "agents" ? "agent" : "category"} ${from}: add ${nativeRoutingLine(section, name, entry)}`))
|
|
334
|
+
const unmapped = dropped
|
|
335
|
+
.filter(({ target }) => target === undefined || !isConfigured(document, target.section, target.name))
|
|
336
|
+
.map(({ text, target }) => target === undefined
|
|
337
|
+
? text
|
|
338
|
+
: `${text}: add ${nativeRoutingLine(target.section, target.name, { model: target.model ?? "<provider>/<model>" })}`)
|
|
339
|
+
return [...carriable, ...unmapped]
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** The one-line lead of the report, for `lines` from openCodeRoutingGap. */
|
|
343
|
+
export function openCodeRoutingNotice(lines) {
|
|
344
|
+
return `OpenCode edition model settings Native does not use; run omo setup to carry them over, or set them in omo.jsonc: ${lines.join("; ")}`
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function isConfigured(document, section, name) {
|
|
348
|
+
return [record(document)[section], ...NATIVE_BLOCKS.map((key) => record(record(document)[key])[section])]
|
|
349
|
+
.some((entries) => Object.hasOwn(record(entries), name))
|
|
263
350
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omo-ai",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OmO Native - the standalone omo command with the OMO extension built in: bun add -g omo-ai",
|
|
6
6
|
"bin": {
|
|
@@ -17,8 +17,7 @@
|
|
|
17
17
|
"postinstall": "node bin/senpi-patch.mjs"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@
|
|
21
|
-
"@code-yeongyu/senpi": "2026.9.29"
|
|
20
|
+
"@code-yeongyu/senpi": "2026.9.29-3"
|
|
22
21
|
},
|
|
23
22
|
"repository": {
|
|
24
23
|
"type": "git",
|
package/plugin/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,66 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [5.1.2] - 2026-09-29
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
**A one-line installer at get.omo.dev.** ([#9166](https://github.com/code-yeongyu/oh-my-openagent/issues/9166)) `curl -fsSL https://get.omo.dev/install.sh | bash` on macOS and Linux, or `irm https://get.omo.dev/install.ps1 | iex` in Windows PowerShell, installs the native `omo` binary for your OS and CPU into `~/.local/bin`, after checking it against the release `SHA256SUMS`. When get.omo.dev is unreachable it falls back to the npm registry and GitHub Releases. `bun add -g omo-ai` keeps working. omo.dev now shows the command for your OS and has an install page at `/docs/install` ([#9177](https://github.com/code-yeongyu/oh-my-openagent/issues/9177)).
|
|
15
|
+
|
|
16
|
+
**`web_search` can use your own SearXNG.** ([senpi#2339](https://github.com/code-yeongyu/senpi/issues/2339)) Add `{ "provider": "searxng", "baseUrl": "http://localhost:8888" }` to `websearch.json`.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
**An unknown slash command no longer reaches the model by accident.** ([senpi#2348](https://github.com/code-yeongyu/senpi/issues/2348)) A typo like `/ulw-exec` goes back into the editor with a `Did you mean /skill:ulw-execute?` hint instead of being sent as a prompt. A second Enter on the unchanged text sends it as a normal message, and Esc keeps editing.
|
|
21
|
+
|
|
22
|
+
**Moving from the OpenCode edition, OmO Native tells you which model choices it ignores.** ([#9147](https://github.com/code-yeongyu/oh-my-openagent/issues/9147), [#6794](https://github.com/code-yeongyu/oh-my-openagent/issues/6794)) On its first start, Native lists each agent and category model from your OpenCode edition settings that it ignores, with the `omo setup` step and the `omo.jsonc` key that carries it over. `omo setup` itself now carries the edition's `metis` and `momus` models over to `plan-consultant` and `plan-reviewer`.
|
|
23
|
+
|
|
24
|
+
**Hosted web search runs on the provider's cheaper search model.** ([senpi#2340](https://github.com/code-yeongyu/senpi/issues/2340)) On the same login, for example `claude-haiku-4-5` on Claude routes, when your model list shows it at a lower price than the session model. If it fails or finds nothing, the search retries on the session model. `"nativeModel": "session"` in `websearch.json` restores the old behavior.
|
|
25
|
+
|
|
26
|
+
**Without a `websearch.json`, `web_search` tries more than DuckDuckGo.** ([senpi#2339](https://github.com/code-yeongyu/senpi/issues/2339)) It now tries DuckDuckGo, Exa, Startpage, Mojeek, Ecosia and Google in turn and pauses an engine that blocks it, so your queries may reach these services. List only the providers you want in `websearch.json` to keep them away from the rest.
|
|
27
|
+
|
|
28
|
+
**Remote MCP servers declared by a skill no longer receive your `bearerTokenEnv` token.** ([senpi#2345](https://github.com/code-yeongyu/senpi/issues/2345)) The skill picks the server's URL, so declare that server in your own `mcp.json` to keep its auth. Stdio servers from skills you installed now expand `${VAR}` the way your own `mcp.json` does.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
**A running session survives `omo update`.** ([senpi#2358](https://github.com/code-yeongyu/senpi/issues/2358)) `omo update` and `bun install -g` replace the installed package, and a session started earlier used to fail every later request with `Cannot find module './<chunk>-<hash>.js'` until you restarted it. Each launch now runs from its own copy of the engine.
|
|
33
|
+
|
|
34
|
+
**Config you edit in `~/.pi/agent` is no longer silently ignored.** ([#9173](https://github.com/code-yeongyu/oh-my-openagent/issues/9173)) After OmO copied `~/.pi/agent` to `~/.omo/agent`, edits to the old copy had no effect. The next start now warns once per edited file and names the file OmO reads, `omo doctor` lists the same files, and `omo config import-pi <file>` copies the edit over.
|
|
35
|
+
|
|
36
|
+
**`omo doctor` no longer fails its computer-use check on every OS.** ([#9193](https://github.com/code-yeongyu/oh-my-openagent/issues/9193)) 5.1.1 shipped without an asset the check reads, so it always reported `ENOENT` for `assets.generated.json`.
|
|
37
|
+
|
|
38
|
+
**A finished task child no longer stays "running" after its parent's host dies.** ([#9183](https://github.com/code-yeongyu/oh-my-openagent/issues/9183)) The child's record kept its dead owner, so Desktop showed a working agent that no longer existed and task status and DAG waits kept waiting on it.
|
|
39
|
+
|
|
40
|
+
**Scrolling on X11 delivers every wheel click.** ([#9136](https://github.com/code-yeongyu/oh-my-openagent/issues/9136)) A foreground scroll from computer use sometimes dropped clicks and still reported success.
|
|
41
|
+
|
|
42
|
+
**Bedrock accepts tool schemas with a root `anyOf`, `oneOf` or `allOf`.** ([senpi#1947](https://github.com/code-yeongyu/senpi/issues/1947)) It also accepts schemas without an object type, which it used to reject.
|
|
43
|
+
|
|
44
|
+
**Memory reflection works when your only provider comes from an extension.** ([#9175](https://github.com/code-yeongyu/oh-my-openagent/issues/9175)) If that provider disappears you get one notice instead of the same failure on every run.
|
|
45
|
+
|
|
46
|
+
**Cursor runs each tool call once.** ([senpi#2334](https://github.com/code-yeongyu/senpi/issues/2334)) It used to run every call a second time, and the replayed write could undo a file fix.
|
|
47
|
+
|
|
48
|
+
**A resumed Claude conversation keeps its resume point.** ([senpi#1972](https://github.com/code-yeongyu/senpi/issues/1972)) When a monitor or goal starts the first turn on restore, the conversation no longer resends its whole history.
|
|
49
|
+
|
|
50
|
+
**In headless runs, a late Kibitzer verdict no longer replaces the final answer.** ([#9158](https://github.com/code-yeongyu/oh-my-openagent/pull/9158)) It used to add a turn after the answer that ended in `NO_REPLY`.
|
|
51
|
+
|
|
52
|
+
**An MCP server that changes its tool list mid-session gets the new list registered.** ([senpi#2188](https://github.com/code-yeongyu/senpi/issues/2188)) Added tools now show up and removed ones stay gone.
|
|
53
|
+
|
|
54
|
+
**Native OpenAI requests with hosted web search no longer fail every turn with `Tool choice 'web_search' not found in 'tools' parameter`.** ([senpi#2234](https://github.com/code-yeongyu/senpi/issues/2234))
|
|
55
|
+
|
|
56
|
+
**Web search through OpenAI and xAI lists only pages the search returned.** ([senpi#2337](https://github.com/code-yeongyu/senpi/issues/2337)) Links the model wrote on its own are no longer counted as sources.
|
|
57
|
+
|
|
58
|
+
**A rejected OpenAI Responses WebSocket request shows the provider's message.** ([senpi#2235](https://github.com/code-yeongyu/senpi/issues/2235)) It used to show `Error Code undefined: undefined`.
|
|
59
|
+
|
|
60
|
+
**Claude subscription sessions keep the field descriptions in custom tool schemas.** ([senpi#2145](https://github.com/code-yeongyu/senpi/issues/2145)) Claude no longer wastes a call on a missing required field.
|
|
61
|
+
|
|
62
|
+
**The OpenAI-compatible adapter merges back-to-back user messages for non-OpenAI hosts.** ([senpi#2120](https://github.com/code-yeongyu/senpi/issues/2120)) Direct OpenAI requests are unchanged.
|
|
63
|
+
|
|
64
|
+
**Extensions that depend on sloppy-mode CommonJS packages load again under Bun.** ([senpi#1841](https://github.com/code-yeongyu/senpi/issues/1841))
|
|
65
|
+
|
|
66
|
+
**Picking a skill that needs input from the slash menu now waits for you to type it.** ([#9168](https://github.com/code-yeongyu/oh-my-openagent/issues/9168)) On OmO Native, choosing `/ulw-execute`, `/ulw-plan`, `/ulw-loop`, `/ulw-research`, `/mass-ulw`, `/hyperplan`, `/init-deep`, `/refactor` or `/remove-ai-slops` from the slash menu with Enter sent it right away with nothing after it, so the skill started without the plan name, request or goal it works on. These skills now declare what they take, and Enter leaves `/ulw-execute ` (or `/skill:ulw-execute `) in the input with the hint shown in the menu; type the arguments and press Enter again to send. Skills that take no input still run on one Enter. The waiting half needs the engine release that reads the hint (code-yeongyu/senpi#2258); until OmO adopts it, the menu behaves as before.
|
|
67
|
+
|
|
68
|
+
**Your own `computer-use` skill no longer opens every session with a "Skill conflicts" warning.** ([#9160](https://github.com/code-yeongyu/oh-my-openagent/issues/9160)) With computer use on, OmO Native added its built-in `computer-use` skill on every start, even when you already had a skill of that name (Orca CLI ships one), so each session opened with a collision box. The built-in skill now steps aside for a same-name user, project or package skill, which is the one that loads, and `/computer status` says so in one line. The same applies to the `x-search` skill. `disabled_skills` now hides both the way it hides the bundled skills; the `computer` and `x_search` tools stay available either way.
|
|
69
|
+
|
|
10
70
|
## [5.1.1] - 2026-09-29
|
|
11
71
|
|
|
12
72
|
### Changed
|