omo-ai 5.1.2 → 5.1.4
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/doctor.js +7 -44
- package/bin/lib/engine-prepare.js +15 -0
- package/bin/lib/launch-spec-mode.js +78 -0
- package/bin/lib/launcher.js +14 -17
- package/bin/lib/npm-dist-tags.js +42 -0
- package/bin/lib/package-paths.js +21 -1
- package/bin/lib/provider-map.json +1 -1
- package/bin/lib/rpc-stream-errors.js +2 -2
- package/bin/lib/self-update.js +46 -14
- package/bin/lib/update-args.js +49 -0
- package/bin/senpi-patch.mjs +2 -1
- package/package.json +2 -2
- package/plugin/CHANGELOG.md +58 -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 +2 -2
- package/plugin/extensions/omo-member.js +2 -2
- 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/index.js +3 -3
- package/plugin/runtime/lsp-daemon/dist/.omo-runtime-manifest.json +4 -4
- package/plugin/runtime/lsp-daemon/dist/cli.js +2 -1
- package/plugin/runtime/lsp-daemon/dist/client.js +2 -1
- package/plugin/runtime/lsp-daemon/dist/index.js +2 -1
- package/plugin/runtime/rollback-migrate.js +2 -2
- package/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
package/bin/lib/doctor.js
CHANGED
|
@@ -3,60 +3,20 @@ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"
|
|
|
3
3
|
import { homedir } from "node:os"
|
|
4
4
|
import { join, resolve } from "node:path"
|
|
5
5
|
import { canonicalAgentDir } from "./agent-dir.js"
|
|
6
|
-
import {
|
|
6
|
+
import { fetchNpmDistTagsSync } from "./npm-dist-tags.js"
|
|
7
|
+
import { channelDistTagVersion, packageManifest, packageRoot, readJson, releaseChannel, resolveSenpi, updateTarget } from "./package-paths.js"
|
|
7
8
|
import { daemonReportLines } from "./daemon.js"
|
|
8
9
|
import { migrationReport } from "./doctor-migration.js"
|
|
10
|
+
import { launchSpecDoctorLines } from "./launch-spec-mode.js"
|
|
9
11
|
import { piConfigReport } from "./doctor-pi-config.js"
|
|
10
12
|
import { needsSetupSuggestion } from "./setup-detect.js"
|
|
11
13
|
|
|
12
|
-
const NPM_DIST_TAGS_URL = "https://registry.npmjs.org/-/package/omo-ai/dist-tags"
|
|
13
|
-
const NPM_FETCH_TIMEOUT_MS = 5000
|
|
14
|
-
|
|
15
|
-
// Doctor is called without await from the launcher and the compiled entry, so the
|
|
16
|
-
// registry lookup has to finish before we print. A bounded child fetch keeps that
|
|
17
|
-
// synchronous and turns any network failure into "could not check".
|
|
18
|
-
|
|
19
14
|
export function installedDistTag(version) {
|
|
20
15
|
return releaseChannel(version)
|
|
21
16
|
}
|
|
22
17
|
|
|
23
18
|
export function latestFromDistTags(distTags, version) {
|
|
24
|
-
|
|
25
|
-
const value = distTags[installedDistTag(version)]
|
|
26
|
-
return typeof value === "string" && value.length > 0 ? value : "could not check"
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function distTagsFetchScript(url, timeoutMs) {
|
|
30
|
-
return `
|
|
31
|
-
const url = ${JSON.stringify(url)};
|
|
32
|
-
const timeout = ${Number(timeoutMs)};
|
|
33
|
-
const ac = new AbortController();
|
|
34
|
-
const timer = setTimeout(() => ac.abort(), timeout);
|
|
35
|
-
fetch(url, { signal: ac.signal, headers: { accept: "application/json" } })
|
|
36
|
-
.then((res) => { if (!res.ok) throw new Error(String(res.status)); return res.text(); })
|
|
37
|
-
.then((text) => { JSON.parse(text); process.stdout.write(text); })
|
|
38
|
-
.catch(() => { process.exitCode = 1; })
|
|
39
|
-
.finally(() => { clearTimeout(timer); });
|
|
40
|
-
`
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function fetchNpmDistTagsSync(options = {}) {
|
|
44
|
-
const spawn = options.spawn ?? spawnSync
|
|
45
|
-
const url = options.url ?? NPM_DIST_TAGS_URL
|
|
46
|
-
const timeoutMs = options.timeoutMs ?? NPM_FETCH_TIMEOUT_MS
|
|
47
|
-
try {
|
|
48
|
-
const result = spawn(process.execPath, ["-e", distTagsFetchScript(url, timeoutMs)], {
|
|
49
|
-
encoding: "utf8",
|
|
50
|
-
timeout: timeoutMs + 500,
|
|
51
|
-
windowsHide: true,
|
|
52
|
-
env: process.env,
|
|
53
|
-
})
|
|
54
|
-
if (result.error || result.status !== 0 || typeof result.stdout !== "string" || result.stdout.trim() === "") return null
|
|
55
|
-
const parsed = JSON.parse(result.stdout)
|
|
56
|
-
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null
|
|
57
|
-
} catch {
|
|
58
|
-
return null
|
|
59
|
-
}
|
|
19
|
+
return channelDistTagVersion(distTags, version) ?? "could not check"
|
|
60
20
|
}
|
|
61
21
|
|
|
62
22
|
function readDistTags(options) {
|
|
@@ -435,6 +395,9 @@ export function runDoctor(inventory, args = [], options = {}) {
|
|
|
435
395
|
lines.push(...staleEngineReport(options))
|
|
436
396
|
lines.push(...retiredPayloadReport(options))
|
|
437
397
|
lines.push(...transientMemoryReport(options))
|
|
398
|
+
const launchSpec = launchSpecDoctorLines(options.pluginRoot ?? join(packageRoot, "plugin"), options.launchSpecIo)
|
|
399
|
+
if (launchSpec.some((line) => line.startsWith("FAIL "))) failed = true
|
|
400
|
+
lines.push(...launchSpec)
|
|
438
401
|
lines.push(...daemonReport(options), ...(options.computerUse ?? []), ...(options.categoryCoverage ?? []))
|
|
439
402
|
if ((options.computerUse ?? []).some((line) => line.startsWith("FAIL "))) failed = true
|
|
440
403
|
if (needsSetupSuggestion(inventory)) {
|
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs"
|
|
|
2
2
|
import { join } from "node:path"
|
|
3
3
|
import { floorClaudeCodeVersion } from "./claude-code-floor.js"
|
|
4
4
|
import { prepareCompileSafeEngine } from "./compile-safe-engine.js"
|
|
5
|
+
import { LAUNCH_SPEC_FILENAME, normalizeLaunchSpecMode } from "./launch-spec-mode.js"
|
|
5
6
|
import { prepareRpcStreamErrors } from "./rpc-stream-errors.js"
|
|
6
7
|
|
|
7
8
|
// Written inside the engine tree, so reinstalling or upgrading the engine drops it with the tree.
|
|
@@ -28,6 +29,20 @@ function isPreparedFor(senpiRoot, omoVersion) {
|
|
|
28
29
|
* A failure is reported with the reinstall command and never blocks the launch: an unprepared
|
|
29
30
|
* engine still runs, only without the guards.
|
|
30
31
|
*/
|
|
32
|
+
/**
|
|
33
|
+
* The plugin's launch spec is re-checked on every launch, not behind the engine stamp: a reinstall
|
|
34
|
+
* of omo-ai under a loose umask rewrites the plugin while the engine tree keeps its stamp (#9208).
|
|
35
|
+
* Fail-open like the engine preparation: a failure warns with the manual fix and never blocks.
|
|
36
|
+
*/
|
|
37
|
+
export function preparePluginLaunchSpec({ pluginRoot, report = (line) => { process.stderr.write(line) }, io = {} }) {
|
|
38
|
+
try {
|
|
39
|
+
normalizeLaunchSpecMode(pluginRoot, io)
|
|
40
|
+
} catch (error) {
|
|
41
|
+
const path = join(pluginRoot, LAUNCH_SPEC_FILENAME)
|
|
42
|
+
report(`omo: could not make ${path} private (${error.message}); task children and teams need it, run: chmod 644 ${path}\n`)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
31
46
|
export function ensureEnginePrepared({ senpiRoot, omoVersion, reinstallCommand, report = (line) => { process.stderr.write(line) } }) {
|
|
32
47
|
if (isPreparedFor(senpiRoot, omoVersion)) return
|
|
33
48
|
try {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { chmodSync, lstatSync, statSync } from "node:fs"
|
|
2
|
+
import { userInfo } from "node:os"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
|
|
5
|
+
export const LAUNCH_SPEC_FILENAME = "daemon-launch-spec.json"
|
|
6
|
+
|
|
7
|
+
// The task host refuses a spec another user could rewrite (senpi-task `rejectInsecureMode`):
|
|
8
|
+
// any group/world write bit, or an owner other than the current user.
|
|
9
|
+
const WRITABLE_BY_OTHERS = 0o022
|
|
10
|
+
|
|
11
|
+
function currentUid() {
|
|
12
|
+
return process.getuid?.()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isMissing(error) {
|
|
16
|
+
return error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* npm and bun extract package files with the installing user's umask, so a `umask 002` install
|
|
21
|
+
* leaves the spec 0664 and every task host start refuses it (#9208). Only the group/world write
|
|
22
|
+
* bits are removed, only on a regular file this user owns; a symlink is never followed, so a link
|
|
23
|
+
* (or a file owned by someone else) is left exactly as it is and the host still rejects it.
|
|
24
|
+
*/
|
|
25
|
+
export function normalizeLaunchSpecMode(pluginRoot, io = {}) {
|
|
26
|
+
const path = join(pluginRoot, LAUNCH_SPEC_FILENAME)
|
|
27
|
+
if ((io.platform ?? process.platform) === "win32") return { action: "skipped", path, reason: "win32" }
|
|
28
|
+
let stat
|
|
29
|
+
try {
|
|
30
|
+
stat = (io.lstat ?? lstatSync)(path)
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (isMissing(error)) return { action: "skipped", path, reason: "missing" }
|
|
33
|
+
throw error
|
|
34
|
+
}
|
|
35
|
+
if (!stat.isFile()) return { action: "left", path, reason: "not_regular_file" }
|
|
36
|
+
const uid = io.getuid === undefined ? currentUid() : io.getuid()
|
|
37
|
+
if (uid !== undefined && stat.uid !== uid) return { action: "left", path, reason: "foreign_owner" }
|
|
38
|
+
const from = stat.mode & 0o7777
|
|
39
|
+
if ((from & WRITABLE_BY_OTHERS) === 0) return { action: "unchanged", path }
|
|
40
|
+
const to = from & ~WRITABLE_BY_OTHERS
|
|
41
|
+
;(io.chmod ?? chmodSync)(path, to)
|
|
42
|
+
return { action: "normalized", path, from, to }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function ownerName() {
|
|
46
|
+
try {
|
|
47
|
+
return userInfo().username
|
|
48
|
+
} catch {
|
|
49
|
+
return "$USER"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The doctor view applies the task host's own rule (the reader follows symlinks), so it reports
|
|
55
|
+
* exactly the specs the host would refuse: the ones a launch could not normalize.
|
|
56
|
+
*/
|
|
57
|
+
export function launchSpecDoctorLines(pluginRoot, io = {}) {
|
|
58
|
+
if ((io.platform ?? process.platform) === "win32") return []
|
|
59
|
+
const path = join(pluginRoot, LAUNCH_SPEC_FILENAME)
|
|
60
|
+
let stat
|
|
61
|
+
try {
|
|
62
|
+
stat = (io.stat ?? statSync)(path)
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (isMissing(error)) return []
|
|
65
|
+
throw error
|
|
66
|
+
}
|
|
67
|
+
if (!stat.isFile()) return []
|
|
68
|
+
const uid = io.getuid === undefined ? currentUid() : io.getuid()
|
|
69
|
+
const impact = "the task host refuses it, so process-mode task children and every team_create fail"
|
|
70
|
+
if (uid !== undefined && stat.uid !== uid) {
|
|
71
|
+
return [`FAIL launch spec: launch_spec_insecure: ${path} is not owned by you (uid ${stat.uid}); ${impact}. Fix: reinstall omo as this user, or run: sudo chown ${ownerName()} ${path}`]
|
|
72
|
+
}
|
|
73
|
+
if ((stat.mode & WRITABLE_BY_OTHERS) !== 0) {
|
|
74
|
+
const mode = (stat.mode & 0o777).toString(8)
|
|
75
|
+
return [`FAIL launch spec: launch_spec_insecure: ${path} is group/world-writable (${mode}); ${impact}. Fix: chmod 644 ${path}`]
|
|
76
|
+
}
|
|
77
|
+
return []
|
|
78
|
+
}
|
package/bin/lib/launcher.js
CHANGED
|
@@ -6,29 +6,17 @@ import { doctorCoverageLines } from "./category-coverage.js"
|
|
|
6
6
|
import { doctorComputerUseLines } from "./computer-use-doctor.js"
|
|
7
7
|
import { runDaemonCommand } from "./daemon.js"
|
|
8
8
|
import { runDoctor } from "./doctor.js"
|
|
9
|
-
import { ensureEnginePrepared } from "./engine-prepare.js"
|
|
9
|
+
import { ensureEnginePrepared, preparePluginLaunchSpec } from "./engine-prepare.js"
|
|
10
10
|
import { migrateLegacyBunGlobalManifest } from "./legacy-bun-global-migration.js"
|
|
11
11
|
import { adoptLegacyFlatState, canonicalAgentDir } from "./agent-dir.js"
|
|
12
12
|
import { nearestNodeBin, packageManifest, packageRoot, readJson, releaseBanner, releaseChannel, resolveSenpi, updateTarget } from "./package-paths.js"
|
|
13
13
|
import { runSelfUpdate } from "./self-update.js"
|
|
14
|
+
import { isSelfUpdate } from "./update-args.js"
|
|
14
15
|
import { detectHarnesses } from "./setup-detect.js"
|
|
15
16
|
import { readSetupSuggestionCache, spawnSetupSuggestionRefresh } from "./setup-detect-cache.js"
|
|
16
17
|
import { printSetupReport } from "./setup-report.js"
|
|
17
18
|
|
|
18
19
|
const earlyCommands = new Set(["install", "remove", "list", "config", "auth", "app-server", "host"])
|
|
19
|
-
const selfUpdateTargets = new Set(["self", "senpi", "omo"])
|
|
20
|
-
// Updating extensions or model catalogs is the engine's job; everything else under `update`
|
|
21
|
-
// would try to replace the pinned engine, so the launcher answers it instead.
|
|
22
|
-
const engineUpdateTargets = new Set(["--extensions", "--models"])
|
|
23
|
-
|
|
24
|
-
function isSelfUpdate(args) {
|
|
25
|
-
if (args[0] !== "update") return false
|
|
26
|
-
const rest = args.slice(1)
|
|
27
|
-
if (rest.length === 0) return true
|
|
28
|
-
if (rest.some((arg) => engineUpdateTargets.has(arg))) return false
|
|
29
|
-
return rest.every((arg) => arg.startsWith("-") || selfUpdateTargets.has(arg))
|
|
30
|
-
}
|
|
31
|
-
|
|
32
20
|
// Identity the engine adopts for this install: what the user sees, where state lives, which
|
|
33
21
|
// environment prefix is read first, what goes on the wire, and which channel to check for
|
|
34
22
|
// updates. The engine consumes this once and scrubs it, so nested engine processes are
|
|
@@ -136,6 +124,7 @@ function senpiEnvironment(senpiRoot) {
|
|
|
136
124
|
}
|
|
137
125
|
|
|
138
126
|
function preparedSenpi() {
|
|
127
|
+
preparePluginLaunchSpec({ pluginRoot: join(packageRoot, "plugin") })
|
|
139
128
|
const senpi = resolveSenpi()
|
|
140
129
|
ensureEnginePrepared({
|
|
141
130
|
senpiRoot: senpi.packageRoot,
|
|
@@ -162,6 +151,12 @@ async function spawnSenpi(args, withExtension) {
|
|
|
162
151
|
await spawnNode(senpi.cliPath, finalArgs, { env })
|
|
163
152
|
}
|
|
164
153
|
|
|
154
|
+
// Routine launch notices go straight to stderr: Bun renders every console.error line red on a
|
|
155
|
+
// color terminal, which made a healthy startup look like a failure (#8442).
|
|
156
|
+
function notice(line) {
|
|
157
|
+
process.stderr.write(`${line}\n`)
|
|
158
|
+
}
|
|
159
|
+
|
|
165
160
|
function isInteractiveDefault(args) {
|
|
166
161
|
return process.stderr.isTTY === true && !args.includes("-p") && !args.includes("--print")
|
|
167
162
|
}
|
|
@@ -180,7 +175,7 @@ function reportLegacyFlatAdoption() {
|
|
|
180
175
|
}
|
|
181
176
|
if (!result.adopted) return
|
|
182
177
|
const moved = [...result.copied, ...result.backfilled].join(", ")
|
|
183
|
-
|
|
178
|
+
notice(`omo: carried forward settings from the legacy ~/.omo layout (${moved})`)
|
|
184
179
|
}
|
|
185
180
|
|
|
186
181
|
/**
|
|
@@ -261,6 +256,8 @@ export async function runLauncher(args = process.argv.slice(2)) {
|
|
|
261
256
|
return
|
|
262
257
|
}
|
|
263
258
|
if (command === "doctor") {
|
|
259
|
+
// Doctor is a launch too: it reports only what the launch-time preparation could not fix.
|
|
260
|
+
preparePluginLaunchSpec({ pluginRoot: join(packageRoot, "plugin") })
|
|
264
261
|
const [categoryCoverage, computerUse] = args[1] === "--reap"
|
|
265
262
|
? [[], []]
|
|
266
263
|
: await Promise.all([
|
|
@@ -301,9 +298,9 @@ export async function runLauncher(args = process.argv.slice(2)) {
|
|
|
301
298
|
return
|
|
302
299
|
}
|
|
303
300
|
if (isInteractiveDefault(args)) {
|
|
304
|
-
|
|
301
|
+
notice(releaseBanner())
|
|
305
302
|
if (process.stdout.isTTY === true && setupSuggestionForLaunch()) {
|
|
306
|
-
|
|
303
|
+
notice("omo: sibling credentials detected; run `omo setup` to review them")
|
|
307
304
|
}
|
|
308
305
|
}
|
|
309
306
|
await spawnSenpi(args, true)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process"
|
|
2
|
+
|
|
3
|
+
const NPM_DIST_TAGS_URL = "https://registry.npmjs.org/-/package/omo-ai/dist-tags"
|
|
4
|
+
const NPM_FETCH_TIMEOUT_MS = 5000
|
|
5
|
+
|
|
6
|
+
// The dist-tags of omo-ai, read by `omo doctor` for "Latest" and by `omo update` for the version it
|
|
7
|
+
// installs. Doctor is called without await from the launcher and the compiled entry, so the
|
|
8
|
+
// registry lookup has to finish before we print. A bounded child fetch keeps that synchronous
|
|
9
|
+
// and turns any network failure into null.
|
|
10
|
+
|
|
11
|
+
function distTagsFetchScript(url, timeoutMs) {
|
|
12
|
+
return `
|
|
13
|
+
const url = ${JSON.stringify(url)};
|
|
14
|
+
const timeout = ${Number(timeoutMs)};
|
|
15
|
+
const ac = new AbortController();
|
|
16
|
+
const timer = setTimeout(() => ac.abort(), timeout);
|
|
17
|
+
fetch(url, { signal: ac.signal, headers: { accept: "application/json" } })
|
|
18
|
+
.then((res) => { if (!res.ok) throw new Error(String(res.status)); return res.text(); })
|
|
19
|
+
.then((text) => { JSON.parse(text); process.stdout.write(text); })
|
|
20
|
+
.catch(() => { process.exitCode = 1; })
|
|
21
|
+
.finally(() => { clearTimeout(timer); });
|
|
22
|
+
`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function fetchNpmDistTagsSync(options = {}) {
|
|
26
|
+
const spawn = options.spawn ?? spawnSync
|
|
27
|
+
const url = options.url ?? NPM_DIST_TAGS_URL
|
|
28
|
+
const timeoutMs = options.timeoutMs ?? NPM_FETCH_TIMEOUT_MS
|
|
29
|
+
try {
|
|
30
|
+
const result = spawn(process.execPath, ["-e", distTagsFetchScript(url, timeoutMs)], {
|
|
31
|
+
encoding: "utf8",
|
|
32
|
+
timeout: timeoutMs + 500,
|
|
33
|
+
windowsHide: true,
|
|
34
|
+
env: process.env,
|
|
35
|
+
})
|
|
36
|
+
if (result.error || result.status !== 0 || typeof result.stdout !== "string" || result.stdout.trim() === "") return null
|
|
37
|
+
const parsed = JSON.parse(result.stdout)
|
|
38
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null
|
|
39
|
+
} catch {
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
}
|
package/bin/lib/package-paths.js
CHANGED
|
@@ -20,6 +20,13 @@ export function releaseChannel(version = packageManifest().version) {
|
|
|
20
20
|
return typeof version === "string" && version.includes("-") ? "beta" : "latest"
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** The version the npm dist-tag of `version`'s channel names, or undefined when the tags lack it. */
|
|
24
|
+
export function channelDistTagVersion(distTags, version = packageManifest().version) {
|
|
25
|
+
if (distTags === null || distTags === undefined || typeof distTags !== "object") return undefined
|
|
26
|
+
const value = distTags[releaseChannel(version)]
|
|
27
|
+
return typeof value === "string" && value.length > 0 ? value : undefined
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
/** The startup banner line: a prerelease names its beta channel, a stable release does not. */
|
|
24
31
|
export function releaseBanner(version = packageManifest().version) {
|
|
25
32
|
return releaseChannel(version) === "beta" ? `omo (omo-ai beta ${version})` : `omo (omo-ai ${version})`
|
|
@@ -38,14 +45,27 @@ function installedVersion(root) {
|
|
|
38
45
|
}
|
|
39
46
|
}
|
|
40
47
|
|
|
48
|
+
/**
|
|
49
|
+
* The package-manager command that updates the install at `root`. `targetVersion` pins the exact
|
|
50
|
+
* `omo-ai@<version>` spec; without it the channel spec is installed.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} [root]
|
|
53
|
+
* @param {NodeJS.Platform} [platform]
|
|
54
|
+
* @param {string} [version]
|
|
55
|
+
* @param {string} [homeDir]
|
|
56
|
+
* @param {(path: string) => boolean} [exists]
|
|
57
|
+
* @param {string} [targetVersion]
|
|
58
|
+
*/
|
|
41
59
|
export function updateTarget(
|
|
42
60
|
root = packageRoot,
|
|
43
61
|
platform = process.platform,
|
|
44
62
|
version = installedVersion(root),
|
|
45
63
|
homeDir = process.env.HOME || process.env.USERPROFILE || homedir(),
|
|
46
64
|
exists = existsSync,
|
|
65
|
+
targetVersion = undefined,
|
|
47
66
|
) {
|
|
48
|
-
|
|
67
|
+
// A resolved target installs that exact version; without one the channel spec is the only answer.
|
|
68
|
+
const spec = targetVersion === undefined ? channelPackageSpec(version) : `omo-ai@${targetVersion}`
|
|
49
69
|
const updateCwd = dirname(join(root, "package.json"))
|
|
50
70
|
const normalizedRoot = updateCwd.replaceAll("\\", "/")
|
|
51
71
|
const normalizedHome = homeDir.replaceAll("\\", "/").replace(/\/+$/, "")
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_comment": "Derived from @code-yeongyu/senpi 2026.9.29-
|
|
2
|
+
"_comment": "Derived from @code-yeongyu/senpi 2026.9.29-5 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",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs"
|
|
2
2
|
import { join } from "node:path"
|
|
3
3
|
|
|
4
|
-
const serialization = 'return JSON.stringify(value.type === "message_update" ? toJsonEvent(value) : value);'
|
|
5
|
-
const guardedSerialization = `try {
|
|
4
|
+
export const serialization = 'return JSON.stringify(value.type === "message_update" ? toJsonEvent(value) : value);'
|
|
5
|
+
export const guardedSerialization = `try {
|
|
6
6
|
${serialization}
|
|
7
7
|
} catch (error) {
|
|
8
8
|
if (!(error instanceof Error)) throw error;
|
package/bin/lib/self-update.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { join } from "node:path"
|
|
2
2
|
import { runChild } from "./child-process.js"
|
|
3
|
-
import {
|
|
3
|
+
import { fetchNpmDistTagsSync } from "./npm-dist-tags.js"
|
|
4
|
+
import { channelDistTagVersion, channelPackageSpec, packageManifest, readJson, releaseChannel, resolveSenpi, updateTarget } from "./package-paths.js"
|
|
5
|
+
import { isPrintOnlyUpdate, updateUsageAnswer } from "./update-args.js"
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export function isPrintOnlyUpdate(args) {
|
|
8
|
-
return args.slice(1).some((arg) => PRINT_ONLY_FLAGS.has(arg))
|
|
9
|
-
}
|
|
7
|
+
export { isPrintOnlyUpdate }
|
|
10
8
|
|
|
11
9
|
export function formatUpdateCommand(update) {
|
|
12
10
|
return `omo is updated via ${update.manager}: ${update.command}`
|
|
@@ -27,16 +25,22 @@ export function formatVersionChange(before, after) {
|
|
|
27
25
|
}
|
|
28
26
|
|
|
29
27
|
/**
|
|
30
|
-
*
|
|
31
|
-
* and
|
|
32
|
-
*
|
|
28
|
+
* Resolves the version the running build's channel dist-tag names (the lookup `omo doctor` uses for
|
|
29
|
+
* "Latest"), then prints and optionally runs the package-manager command that installs exactly that
|
|
30
|
+
* version. `--help`/`-h` print the usage and an unknown flag exits 2, both before the registry lookup,
|
|
31
|
+
* so neither can install anything (#9207). `--dry-run` and `--print` print the resolved command only.
|
|
32
|
+
* Already on the target, it says so and installs nothing. After a manager exit 0 it re-reads the installed version: one that did not
|
|
33
|
+
* reach the target exits non-zero with the retry command, because the manager's own success does not
|
|
34
|
+
* prove the install moved (#9198). An unreachable registry falls back to the unpinned channel spec.
|
|
33
35
|
* `run` defaults to `runChild` so tests inject a spawn without touching the child-process helper.
|
|
34
36
|
*
|
|
35
37
|
* @typedef {{ omo: string, engine: string }} InstalledVersion
|
|
36
38
|
* @typedef {{ status: number | null, signal: string | null }} ChildResult
|
|
37
39
|
* @typedef {{ stdio?: "inherit", windowsHide?: boolean, env?: NodeJS.ProcessEnv }} RunOptions
|
|
40
|
+
* @typedef {{ manager: string, command: string, argv: string[], env?: Record<string, string> }} UpdateTarget
|
|
38
41
|
* @typedef {{
|
|
39
|
-
*
|
|
42
|
+
* resolveUpdate?: (targetVersion?: string) => UpdateTarget,
|
|
43
|
+
* fetchDistTags?: () => Record<string, unknown> | null,
|
|
40
44
|
* log?: (line: string) => void,
|
|
41
45
|
* error?: (line: string) => void,
|
|
42
46
|
* run?: (command: string, args: string[], options?: RunOptions) => Promise<ChildResult>,
|
|
@@ -48,17 +52,39 @@ export function formatVersionChange(before, after) {
|
|
|
48
52
|
* @returns {Promise<number>}
|
|
49
53
|
*/
|
|
50
54
|
export async function runSelfUpdate(args, options = {}) {
|
|
51
|
-
const
|
|
55
|
+
const resolveUpdate = options.resolveUpdate
|
|
56
|
+
?? ((targetVersion) => updateTarget(undefined, undefined, undefined, undefined, undefined, targetVersion))
|
|
57
|
+
const fetchDistTags = options.fetchDistTags ?? fetchNpmDistTagsSync
|
|
52
58
|
const log = options.log ?? ((line) => console.log(line))
|
|
53
59
|
const error = options.error ?? ((line) => console.error(line))
|
|
54
60
|
const run = options.run ?? runChild
|
|
55
61
|
const readInstalled = options.readInstalled ?? readInstalledVersion
|
|
56
62
|
const env = options.env ?? process.env
|
|
57
63
|
|
|
58
|
-
|
|
59
|
-
if (
|
|
64
|
+
const usage = updateUsageAnswer(args)
|
|
65
|
+
if (usage !== undefined) {
|
|
66
|
+
;(usage.stream === "stderr" ? error : log)(usage.text)
|
|
67
|
+
return usage.exitCode
|
|
68
|
+
}
|
|
60
69
|
|
|
61
70
|
const before = readInstalled()
|
|
71
|
+
const channel = releaseChannel(before.omo)
|
|
72
|
+
const target = channelDistTagVersion(fetchDistTags(), before.omo)
|
|
73
|
+
const update = resolveUpdate(target)
|
|
74
|
+
|
|
75
|
+
if (target === undefined) {
|
|
76
|
+
log(`omo: could not confirm the ${channel} omo-ai version from the npm registry; installing the unpinned ${channelPackageSpec(before.omo)}`)
|
|
77
|
+
}
|
|
78
|
+
if (isPrintOnlyUpdate(args)) {
|
|
79
|
+
log(formatUpdateCommand(update))
|
|
80
|
+
return 0
|
|
81
|
+
}
|
|
82
|
+
if (target !== undefined && target === before.omo) {
|
|
83
|
+
log(`omo ${before.omo} is up to date (omo-ai@${channel} is ${target})`)
|
|
84
|
+
return 0
|
|
85
|
+
}
|
|
86
|
+
log(formatUpdateCommand(update))
|
|
87
|
+
|
|
62
88
|
const [command, ...argv] = update.argv
|
|
63
89
|
let result
|
|
64
90
|
try {
|
|
@@ -77,6 +103,12 @@ export async function runSelfUpdate(args, options = {}) {
|
|
|
77
103
|
return result.status ?? 1
|
|
78
104
|
}
|
|
79
105
|
|
|
80
|
-
|
|
106
|
+
const after = readInstalled()
|
|
107
|
+
if (target !== undefined && after.omo !== target) {
|
|
108
|
+
error(`omo is still ${after.omo}; ${target} is published`)
|
|
109
|
+
error(`omo: update failed; retry with: ${update.command}`)
|
|
110
|
+
return 1
|
|
111
|
+
}
|
|
112
|
+
log(formatVersionChange(before, after))
|
|
81
113
|
return 0
|
|
82
114
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const selfUpdateTargets = new Set(["self", "senpi", "omo"])
|
|
2
|
+
// Updating extensions or model catalogs is the engine's job; everything else under `update`
|
|
3
|
+
// would try to replace the pinned engine, so the launcher answers it instead.
|
|
4
|
+
const engineUpdateTargets = new Set(["--extensions", "--models"])
|
|
5
|
+
const PRINT_ONLY_FLAGS = new Set(["--dry-run", "--print"])
|
|
6
|
+
const HELP_FLAGS = new Set(["--help", "-h"])
|
|
7
|
+
// `--self` is the documented spelling of the bare command (docs/reference/omo-ai-publishing.md).
|
|
8
|
+
const KNOWN_FLAGS = new Set([...PRINT_ONLY_FLAGS, ...HELP_FLAGS, "--self"])
|
|
9
|
+
|
|
10
|
+
export const UPDATE_USAGE = `Usage: omo update [--dry-run | --print]
|
|
11
|
+
|
|
12
|
+
Update omo to the newest release on its channel. The engine is pinned by omo and moves with it.
|
|
13
|
+
|
|
14
|
+
Options:
|
|
15
|
+
--dry-run, --print print the update command without running it
|
|
16
|
+
-h, --help show this help`
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Whether `args` is a product self-update, which the launcher answers itself rather than letting
|
|
20
|
+
* the engine move its pin: bare `update`, the self/senpi/omo targets, or flags only.
|
|
21
|
+
* @param {readonly string[]} args
|
|
22
|
+
*/
|
|
23
|
+
export function isSelfUpdate(args) {
|
|
24
|
+
if (args[0] !== "update") return false
|
|
25
|
+
const rest = args.slice(1)
|
|
26
|
+
if (rest.length === 0) return true
|
|
27
|
+
if (rest.some((arg) => engineUpdateTargets.has(arg))) return false
|
|
28
|
+
return rest.every((arg) => arg.startsWith("-") || selfUpdateTargets.has(arg))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** @param {readonly string[]} args */
|
|
32
|
+
export function isPrintOnlyUpdate(args) {
|
|
33
|
+
return args.slice(1).some((arg) => PRINT_ONLY_FLAGS.has(arg))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Help and usage errors of a self-update, answered before any registry lookup or install:
|
|
38
|
+
* `--help`/`-h` print the usage (exit 0), and a flag `update` does not know is a usage error that
|
|
39
|
+
* names it (exit 2). Undefined means the arguments are a real update request (#9207).
|
|
40
|
+
* @param {readonly string[]} args
|
|
41
|
+
* @returns {{ exitCode: 0 | 2, stream: "stdout" | "stderr", text: string } | undefined}
|
|
42
|
+
*/
|
|
43
|
+
export function updateUsageAnswer(args) {
|
|
44
|
+
const rest = args.slice(1)
|
|
45
|
+
if (rest.some((arg) => HELP_FLAGS.has(arg))) return { exitCode: 0, stream: "stdout", text: UPDATE_USAGE }
|
|
46
|
+
const unknown = rest.find((arg) => arg.startsWith("-") && !KNOWN_FLAGS.has(arg))
|
|
47
|
+
if (unknown === undefined) return undefined
|
|
48
|
+
return { exitCode: 2, stream: "stderr", text: `omo update: unknown option ${unknown}\nRun omo update --help for usage.` }
|
|
49
|
+
}
|
package/bin/senpi-patch.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from "node:fs"
|
|
|
2
2
|
import { dirname, join } from "node:path"
|
|
3
3
|
import { createRequire } from "node:module"
|
|
4
4
|
import { fileURLToPath } from "node:url"
|
|
5
|
-
import { prepareInstalledEngine, writeEnginePreparedStamp } from "./lib/engine-prepare.js"
|
|
5
|
+
import { prepareInstalledEngine, preparePluginLaunchSpec, writeEnginePreparedStamp } from "./lib/engine-prepare.js"
|
|
6
6
|
|
|
7
7
|
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
8
8
|
const require = createRequire(join(packageRoot, "package.json"))
|
|
@@ -23,5 +23,6 @@ try {
|
|
|
23
23
|
throw new Error("omo-ai: unable to resolve the installed @code-yeongyu/senpi package", { cause: error })
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
preparePluginLaunchSpec({ pluginRoot: join(packageRoot, "plugin") })
|
|
26
27
|
prepareInstalledEngine(senpiRoot)
|
|
27
28
|
writeEnginePreparedStamp(senpiRoot, JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8")).version)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omo-ai",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.4",
|
|
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,7 +17,7 @@
|
|
|
17
17
|
"postinstall": "node bin/senpi-patch.mjs"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@code-yeongyu/senpi": "2026.9.29-
|
|
20
|
+
"@code-yeongyu/senpi": "2026.9.29-5"
|
|
21
21
|
},
|
|
22
22
|
"repository": {
|
|
23
23
|
"type": "git",
|
package/plugin/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,64 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [5.1.4] - 2026-09-29
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
OmO Native moves from senpi 2026.9.29-4 to senpi 2026.9.29-5, and the engine changes below come with it.
|
|
15
|
+
|
|
16
|
+
**GPT-6.1 Sol arrives, and `deep-low` and Geeky · Normal lead with it at medium.** ([#9214](https://github.com/code-yeongyu/oh-my-openagent/issues/9214), [senpi#2390](https://github.com/code-yeongyu/senpi/issues/2390)) The engine adds `gpt-6.1-sol` and its Fast tier `gpt-6.1-sol-fast` on the OpenAI API, ChatGPT subscription, Azure, GitHub Copilot, OpenCode Zen, OpenRouter, Venice and Vercel lanes with the documented `low`..`max` efforts, and makes it the implicit default for the OpenAI and ChatGPT-subscription providers where GPT-6 Sol was. It runs on the GPT-6 system prompt, which gains the two writing rules from OpenAI's own GPT-6.1 Sol template. The default deep delegation lane now starts on `gpt-6.1-sol` (medium) from your ChatGPT subscription or the OpenAI API, then `gpt-6.1-sol-fast`, and keeps GPT-5.6 Sol (medium) and GPT-5.6 Sol Fast behind them, so GitHub Copilot, OpenCode Zen and an engine that does not list GPT-6.1 Sol yet still run the lane on GPT-5.6 Sol as before. The `geeky-normal` model profile likewise applies GPT-6.1 Sol (medium) first and GPT-5.6 Sol (medium) when 6.1 Sol is not served. The effort stays medium on every rung. The other categories and model profiles are unchanged.
|
|
17
|
+
|
|
18
|
+
**A host can give each session the chat-style app prompt.** ([senpi#2377](https://github.com/code-yeongyu/senpi/issues/2377)) With `SENPI_PROMPT_SURFACE=app`, or `open_session.promptSurface: "app"` on a multi-session host, replies drop the terminal routing line and keep tool and hook feedback with the agent unless it changes what you get. Terminal sessions keep today's prompts.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
**Editing a file outside a project no longer asks you to install a language server.** ([#9223](https://github.com/code-yeongyu/oh-my-openagent/issues/9223)) After an edit to OmO's own config, a scratch file in a temp folder, or any file with no project around it, the LSP check told the agent to install the missing server "in THIS repository" and to ask you first, which would have created a `package.json` in your home or temp folder, and it came back on every edit. Those files now get no install prompt, and inside a project the prompt for a missing server appears once per session instead of after every edit. Thanks to @haamsuk-collab for the report.
|
|
23
|
+
|
|
24
|
+
**Kibitzer recall and quick delegation work with a single Z.ai or Xiaomi login.** ([#9202](https://github.com/code-yeongyu/oh-my-openagent/issues/9202)) The `quick` category had no Z.ai or Xiaomi model, so on a machine logged in to only one of them recall stayed off and quick tasks were unavailable. It now falls back to `glm-5.3-flash` (Z.ai) and `mimo-v2.6-flash` (Xiaomi) at low effort, after every model it already used, so machines that had a quick model keep the same one.
|
|
25
|
+
|
|
26
|
+
**Engine fixes from senpi 2026.9.29-5.** Remote compaction works again on the ChatGPT subscription lane instead of silently falling back to a local summary ([senpi#2378](https://github.com/code-yeongyu/senpi/issues/2378)). A session no longer leaves an empty `.omo/` folder in the project ([senpi#2386](https://github.com/code-yeongyu/senpi/issues/2386)). RPC `get_auth_providers` reports each login method's status separately ([senpi#2384](https://github.com/code-yeongyu/senpi/issues/2384)). The embedded tree-sitter grammars load on Node again ([senpi#2032](https://github.com/code-yeongyu/senpi/issues/2032)).
|
|
27
|
+
|
|
28
|
+
## [5.1.3] - 2026-09-29
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- OmO Native moves from senpi 2026.9.29-3 to senpi 2026.9.29-4; the engine changes below come with it.
|
|
33
|
+
|
|
34
|
+
**Breaking: an untrusted project's legacy `.pi/` resources no longer load.** ([senpi#2375](https://github.com/code-yeongyu/senpi/pull/2375)) Extensions, skills, prompt templates, themes and hooks under a project's `.pi/` now follow project trust like the rest of the project config. A project whose only project resources are in `.pi/` now asks whether you trust it instead of opening as trusted. Trusted projects load them as before.
|
|
35
|
+
|
|
36
|
+
**Breaking: interactive sessions no longer join a shared engine host.** ([senpi#2328](https://github.com/code-yeongyu/senpi/issues/2328)) Every interactive launch runs on its own local runtime. Extensions lose `pi.sharedHostEnabled`, and the `experimental.sharedHost` setting and the `SENPI_ENABLE_SHARED_HOST` / `SENPI_DISABLE_SHARED_HOST` variables are ignored if set. On first load `experimental.sharedHost` is removed from the global settings file.
|
|
37
|
+
|
|
38
|
+
**Local sessions can pass messages and whole sessions to each other.** ([senpi#2328](https://github.com/code-yeongyu/senpi/issues/2328)) An extension can open a terminal session to other local sessions with `pi.session.registerControlEndpoint`, and each message delivered that way is admitted once and waits while you are typing. A multi-session host hands a quiet session to your terminal with `release_session`, and with `interrupt: true` it stops the running work first and returns what it took out of the queue. Host status shows each endpoint's kind, whether it answers and who owns it, and a suspended terminal no longer holds the listing for 10 seconds.
|
|
39
|
+
|
|
40
|
+
**Claude answers in your language.** ([senpi#2366](https://github.com/code-yeongyu/senpi/issues/2366)) With a "reply in Korean" rule, or when you write in Korean, the routing line, the handoff block and todo labels come back in that language. The handoff labels (`Ask`, `For you`, `Now`, `Next`) stay as they are. Thanks to @floweredao for the report.
|
|
41
|
+
|
|
42
|
+
**The engine installs smaller and faster.** ([senpi#2360](https://github.com/code-yeongyu/senpi/issues/2360), [senpi#2362](https://github.com/code-yeongyu/senpi/issues/2362)) senpi now declares its real dependencies instead of shipping its whole dependency tree inside the tarball, and it no longer ships sourcemaps.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
**Memory recall works on a machine whose quick-category pin names only providers you are not logged into.** ([#9216](https://github.com/code-yeongyu/oh-my-openagent/issues/9216)) With `categories.quick.models` pinned to, say, OpenAI while only another provider is connected, Kibitzer started its recall judge on the pinned model, which failed at once with "No API key found", so recalled-memory nudges never arrived. Kibitzer now starts on the first connected model, your pins first and then the category's built-in chain, and when nothing is connected it shows the "Kibitzer unavailable" notice naming the providers to connect. The `task` tool still uses your pin as written.
|
|
47
|
+
|
|
48
|
+
**Teams and process-mode subagents work again on installs made under `umask 002`.** ([#9208](https://github.com/code-yeongyu/oh-my-openagent/issues/9208)) With the Ubuntu default umask for private-group users, npm installed the task-host launch spec group-writable, the task host refused it, and every `team_create` failed with only "The task host is unavailable." OmO now makes that file private (0644) when it launches, never following a symlink and never touching a file another user owns. When the task host still refuses the spec, the task, the `team_create` error and `omo doctor` name the file and the fix (`chmod 644 <path>`). Reported and diagnosed by @devswha.
|
|
49
|
+
|
|
50
|
+
**An npm-installed comment checker is found again, in the old and the new package layout.** ([#9180](https://github.com/code-yeongyu/oh-my-openagent/issues/9180)) The OpenCode edition's comment-checker hook looked for `@code-yeongyu/comment-checker` only in the `bin/` folder its install script used to fill, so an npm install of 0.7.1 or later (binaries under `vendor/`) was never used and the hook fell back to downloading its own copy. The hook, `omo doctor` and the LazyCodex fallback now find the binary under `vendor/` and in the per-platform packages the checker is moving to. Those packages cut its install from about 255 MiB to 51 MiB.
|
|
51
|
+
|
|
52
|
+
**Agent mailbox state no longer lands in your repository.** ([#9201](https://github.com/code-yeongyu/oh-my-openagent/issues/9201)) OmO Native's thread tools kept their mailbox and receipts in `<project>/.omo/thread-tools`. They now live in the same per-project state folder as task state, outside the project. A project that already has the folder keeps using it.
|
|
53
|
+
|
|
54
|
+
**A Claude `Request not allowed` error no longer strands a session on a fallback model.** ([senpi#2376](https://github.com/code-yeongyu/senpi/issues/2376)) A transient 403 from a Claude subscription is retried on the same model before the session falls back. A fallback target that answers with a billing error no longer pins the session: that provider is skipped for its cooldown, and the next turn returns to your original model with a notice saying why.
|
|
55
|
+
|
|
56
|
+
**A project you trusted loads its `.agents/skills` again.** ([senpi#2371](https://github.com/code-yeongyu/senpi/issues/2371)) They appear under `/skill:` without launching with `--approve`. An untrusted project no longer picks up resources another session loaded from the same folder as trusted. Thanks to @sorenjuul for the report and the root-cause analysis.
|
|
57
|
+
|
|
58
|
+
**Session files survive a failed write.** ([senpi#2328](https://github.com/code-yeongyu/senpi/issues/2328)) Opening a session saved by an older version rewrites it through a complete copy, so a full disk or an I/O error keeps the original intact. A write refused for permissions or space no longer leaves the refused entry in the session, and a prompt whose messages could not be saved reports that error. A refused delivery, a half-written first save or the release of a never-saved session no longer leaves a session stuck or ends an engine host.
|
|
59
|
+
|
|
60
|
+
**Every `extension_ui_response` gets a reply.** ([senpi#2372](https://github.com/code-yeongyu/senpi/issues/2372)) A host or a terminal endpoint that settles one answers with the frame's own id, so a client can tell a delivered answer from a lost one.
|
|
61
|
+
|
|
62
|
+
**The startup banner is no longer red.** ([#8442](https://github.com/code-yeongyu/oh-my-openagent/issues/8442)) Under Bun, `omo` printed its version banner, the `omo setup` credentials hint and the legacy-settings notice in the error color, so a healthy start looked like a failure. They now print without color; the text and the stream are unchanged. (#8870 by @cynkai)
|
|
63
|
+
|
|
64
|
+
**`omo update` no longer reports success while leaving the old version installed.** ([#9198](https://github.com/code-yeongyu/oh-my-openagent/issues/9198)) It now looks up the version published on your channel and installs exactly that, for example `bun add -g omo-ai@5.1.2`. If you are already on it, nothing is installed. If the package manager finishes but OmO is still on the old version, `omo update` says `omo is still <old>; <new> is published`, prints the command to retry, and exits with an error. `omo update --dry-run` shows the exact command. When the npm registry can't be reached, it installs the unpinned `omo-ai` as before and tells you it could not confirm the version.
|
|
65
|
+
|
|
66
|
+
**`omo update --help` shows help instead of updating.** ([#9207](https://github.com/code-yeongyu/oh-my-openagent/issues/9207), reported by @devswha) `omo update --help` and `omo update -h` used to run the install, and so did a mistyped flag. They now print what `omo update` does and its flags. A flag it doesn't know, such as `--forse`, stops with `omo update: unknown option --forse` and installs nothing. The compiled `omo` binary answers the same way.
|
|
67
|
+
|
|
10
68
|
## [5.1.2] - 2026-09-29
|
|
11
69
|
|
|
12
70
|
### Added
|