StopOnTextCriteria decoded the last STOP_DECODE_WINDOW_TOKENS of the whole
sequence, so prompt tokens were eligible for matching. A prompt that itself
contains the stop string stops generation at the first generated token and
yields an empty title.
Anchor the window to the generation boundary by recording the first
generated index per batch entry. Existing local title models are
unaffected: with the assistant-prefill prompt shape, the example `</title>`
tags sit outside the 32-token window for normal messages, so no shipping
model changes behavior. The bug becomes reachable with any chat-level
few-shot prompt that places the stop string near the generation boundary.
A Codex request to a model the signed-in ChatGPT account is not entitled
to fails with "The '<model>' model is not supported when using Codex with
a ChatGPT account." That was classified as a plain provider error, so the
request failed outright even when a sibling account was signed in and
entitled to the model.
Classify that exact denial as an account-policy error, the same category
`cyber_policy` already uses, so the existing credential-rotation path can
reach an entitled account.
The match is deliberately narrow: it fires only for provider
`openai-codex`, only when the denied model in the message is the model that
was requested, and only for a bounded, non-null model identity. A denial
naming some other model does not trigger rotation, so an unrelated mention
cannot burn sibling credentials.
The memory-extraction prompt concatenated its instructions, few-shot
examples, and the user message into a single user turn, so a small local
model could not distinguish instructions from input and frequently echoed
the Globex/weather examples instead of extracting facts.
Send the instructions as a real system turn and the raw text as the user
turn. The tiny worker protocol gains a systemPrompt field, and Mnemopi
completion input carries task metadata so the backend selects the right
prompt per call.
Drop the code-built MEMORY_EXTRACTION_TEMPLATE rather than porting it:
prompt text belongs in .md files, and resolveMemoryCompletionInput already
overrides that template for every extraction call, so Mnemopi rendered it
only for the result to be discarded.
Measured on ONNX q4 CPU, LFM2.5-1.2B memory extraction improved from 1/8
to 5/8 once the roles were separated.
The claude-plugins provider read installed_plugins.json but never the
`enabledPlugins` map Claude Code keeps in ~/.claude/settings.json and
<project>/.claude/settings(.local).json. Two consequences: a plugin the
user switched off for a project still loaded there, and a local-scope
install enabled for a project never loaded unless the project directory
matched the install's recorded projectPath exactly.
Merge enabledPlugins across the same layers Claude Code consults (user
settings, then the active project root's and cwd's .claude/settings.json
and settings.local.json; later wins) and apply it in
listClaudePluginRoots: `false` hides the plugin, `true` opts a
local-scope install in regardless of projectPath. Untouched ids keep the
existing behavior. Contributing settings files join the cache key.
Solves: Claude marketplace plugins loading in the wrong projects
Tests: claude-plugins.test.ts — per-project off switch (local wins over
settings.json, other projects unaffected) and enabledPlugins:true
opt-in for a local install recorded under a parent directory
Independent review noted that a file at or below the snapshot cap with a
text-like extension but binary content was fully decoded into three string
views and a line array before the sniff rejected it — roughly three times
the file size in transient allocations for output that is thrown away.
Split the loader: read the bytes, sniff those bytes, and derive the views
only for what survives. A refused 4MiB binary now costs one read and no
decode.
No observable change: the 966-case differential against the base commit is
byte-identical to the run before this split, still differing only in the four
intended BOM snapshot tags.
Also name in BlockContextSource.text the one shape that would violate its
same-content contract — a source object reused across two different line
arrays — since the contract is documented rather than enforced.
Alibaba Token Plan advertises both dated DeepSeek V4 snapshots, but only
deepseek-v4-flash-0731 had an entry in ALIBABA_TOKEN_PLAN_DISCOVERED_MODEL_LIMITS.
deepseek-v4-pro-0813 is not in ALIBABA_TOKEN_PLAN_STATIC_MODELS either (only the
undated deepseek-v4-pro is), so it fell through to `contextWindow: null` /
`maxTokens: null` — the #7486 symptom, still live for this one id.
Reasoning already worked: the `normalizedId.startsWith("deepseek-v4")` branch
gives it reasoning: true and the high/max effort ladder. Only the limits were
missing, so this is a one-entry fix at 1M context / 384K output, matching both
deepseek-v4-pro and deepseek-v4-flash-0731.
Extends the existing discovery test to advertise the id and assert its limits
and thinking config. Verified the test fails without the source change
(contextWindow/maxTokens come back null) and passes with it.
Refs #8847
The local text read path opened the same file for every consumer. A ranged
read of a file within the snapshot cap cost four opens and three decodes:
an 8KiB binary sniff, a streaming scan for the rendered window, a whole-file
read for bracket context, and another whole-file read to hash the snapshot.
Whole-file reads under the structural summarizer paid a fifth. Two of those
readers also ran normalizeToLF over the same bytes.
Read the bytes once at or below SNAPSHOT_MAX_BYTES and derive every view
from them: sniff the leading 8KiB of the buffer, slice the rendered window
out of it under the identical line and byte budgets, index bracket context
into its addressable lines, and hand the normalized text to the snapshot
store and the summarizer. Past the cap nothing wants the whole file, so the
streaming reader stays.
Line byte lengths are walked out of the buffer rather than measured on the
decoded strings, so reported byte counts and the truncation boundary stay
exact for content that is not valid UTF-8.
The buffered text is BOM-stripped for hashing, matching the decoder the
patcher's live read uses. A whole-file read of a BOM file previously hashed
its tag from BOM-bearing text, so the following edit only applied through
stale-hash recovery and told the model the file had changed externally when
it had not.
Also stop rejoining lines into a fresh whole-file string on the way to
tree-sitter when the caller still holds that text, and drop the unread
selectedBytesTotal accounting from the streaming reader.
Measured on 966 differential cases across CRLF, BOM, lone-CR, invalid-UTF-8,
oversized-line, empty, no-trailing-newline, multi-range and raw shapes: the
spurious recovery warning is the only behavioral difference. Raw reads, which
skip the tree-sitter parse that dominates everything else, get 30-45% faster
(2.7MB: 10.3ms -> 5.7ms); non-raw reads 1-3%.
Retry: fixed changelog bundle probe asserting latest release equals VERSION (fails on releases with no coding-agent changelog content); widened issue-4593 watchdog test budgets from 5ms to 50ms against CI runner scheduling noise.
collect_boundaries walked every node in the file even though the answer is
bounded by the visible window, which cost roughly twice the parse: on an
81KB source the walk was 8.95ms against a 4.32ms parse, and on 1MB it was
138.8ms against 87.6ms.
A node contributes a boundary only when one of its own endpoint lines is
visible, and both of those lines lie inside its raw row span. Every
descendant's span is contained in its ancestor's, so a span holding no
visible line rules out that node and everything beneath it. Skip such
subtrees with a binary search over the merged visible ranges.
The test is the raw span, not endpoint visibility: a node whose span merely
straddles the window has both endpoints outside it yet can contain a child
that opens exactly on a visible line. The raw span is also conservative
relative to node_content_end_line, so the prune needs no reasoning about
that newline adjustment.
Equivalence is proven differentially rather than argued: the pre-prune walk
is retained under cfg(test) and compared for exact Option<Vec<u32>> equality
across 4827 .ts/.py/.rs files and 38,616 comparisons over eight window
shapes, including whole-file-visible, past-EOF, disjoint ranges, empty range
lists and files that fail to parse. Zero mismatches. root.has_error() is
still evaluated on the whole tree before the walk, so pruning cannot change
a None verdict.
Measured on the built addon with a mid-file 40-line window, medians of 20,
against the parse cache alone: 81KB 13.4ms -> 4.45ms cold and 9.04ms ->
0.149ms warm; 1.06MB 188.1ms -> 55.8ms cold and 131.7ms -> 0.440ms warm.
`enclosing_block_boundaries`, `block_range_at` and `summarize_code` each
re-parsed the whole file on every call. The results are not cacheable —
boundaries depend on the caller's visible ranges, which differ per call —
but the `tree_sitter::Tree` is, so cache that instead and hand out
`ts_tree_copy` clones.
Keyed on (xxh64 of the source, source length, language). The hash is a
bucket selector only: a hit re-verifies the stored source against the
request byte-for-byte before returning the tree, so a collision costs a
re-parse and can never yield a tree built from other content. Language is
in the key because the same bytes parsed as TypeScript and as Python are
different trees.
Bounded at 12 slots and 4 MiB of retained source with LRU eviction;
sources above 4 MiB are parsed but never retained. `Tree` is `Send` but
not `Sync`, so entries sit behind a `Mutex` that is held only for a map
probe, a byte compare and a refcount bump, never across a parse or walk.
Error trees are cached like any other: `has_error()` is a property of the
tree, so the callers' own checks reach an identical verdict from a cached
tree, and repeated "does this parse" probes get the speedup too.
Measured (M4 Max, bazel-built .node, median of 20, 1-40 visible):
read.ts 81 KB 13.34 ms -> 8.86 ms on repeat; 1 MB synthetic 225.7 ms ->
138.3 ms. Parser::new + set_language measured at 0.30 us against a
3.91 ms parse, so no parser pooling.
The previous CI reds were a truncated rust-overlay fetch in Evaluate
flake and a Julia kernel shutdown in julia-prelude.test.ts. Neither
path is in this change.
`lsp regressions > detects pyright and pylsp in Windows virtualenv Scripts for
Python-only roots` fails on any machine that has ~/.omp/agent/lsp.json:
expect(config.servers[server]?.resolvedCommand).toBe(localBin)
Expected: ".../.venv/Scripts/pyright-langserver.exe"
Received: undefined
The test was not asserting against the packaged defaults at all. loadConfig
walks the user config dirs (~/.omp/agent, ~/.pi/agent, ~/.claude) via
getConfigDirPaths, which resolves from os.homedir(). Any user lsp.json with a
`servers` block sets hasOverrides, which takes loadConfig off its auto-detect
branch and onto the override branch, where the user's rootMarkers replace the
packaged ones.
On this machine that file overrides pyright with
rootMarkers: ["pyproject.toml", "uv.lock", "requirements.txt", "setup.py"]
which contains neither `pyrightconfig.json` nor `setup.cfg` — precisely the
two markers the test creates. loadConfig therefore returned zero servers.
`ruff` is absent from that file, keeps the packaged rootMarkers, and its
sibling tests pass, which is why only this one failed.
Confirmed by probing inside the test: hasRootMarkers() true and
resolveCommand() returning the .exe, while loadConfig().servers was {} — the
detection helpers were fine, the branch was not.
Point os.homedir() at an empty directory for every test in the file so they
see a pristine environment. Bun's os.homedir() reads the passwd entry rather
than $HOME, so the env var alone does not redirect the walk; both are set.
Verification: 76 pass / 0 fail (was 75 / 1) on a machine with a user lsp.json.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A selector like anthropic/claude-opus-5 is both the anthropic provider's
canonical selector and, verbatim, an OpenRouter model id. When the named
provider is out of the candidate set (disabled, missing creds at boot), the
exact provider-scoped reference misses and the raw-id flat match re-binds the
request onto OpenRouter's same-named model. Requests that should fail closed
instead bill the aggregator at per-token Claude prices.
Raw-id fallback is only legitimate when the named provider does not carry the
id (openai/gpt-4o:extended). Lock the reference when it does by checking the
bundled catalog, then fail rather than shadow. openrouter/anthropic/claude-opus-5
still selects OpenRouter explicitly.
Cursor grok-4.6-xhigh stalled after a short "I'll fetch the page"
preamble because interaction_query frames (including proto field 9)
were dropped and the server waited until the 300s idle watchdog fired.
isMultiplexerSession() treats TMUX, STY, ZELLIJ, HERDR_ENV=1, the CMUX_*
markers and a tmux/screen TERM as authoritative, and routes rendering down the
path that cannot rebuild scrollback. Nine tests across four files assert the
destructive full-paint behavior and fail for anyone running the suite inside
tmux, screen, Zellij or CMUX.
component-render.test.ts already cleared HERDR_ENV for exactly this reason but
covered only one of the nine signals. Replace it with a shared helper that
clears the whole family and restores it afterwards, and call it from the other
three files.
Three tests fail on a clean checkout depending on where the repo lives and
which terminal runs the suite:
- status-line-path builds its fake home inside the checkout, so a clone under
/tmp lands in a SCRATCH_ROOTS prefix and renders the scratch icon.
- status-line/component renders a fixed 120 columns, so a long checkout path
or branch name pushes the cost segment out of the assertion.
- bash-executor runs an interactive login zsh, which loads the system
/etc/zshrc; under Apple Terminal that appends session-save lines to the
captured output. HOME does not isolate a system-level file.
- Updated GPT-5.6 context window floor to 1,000,000 tokens across discovery, policies, and tests.
- Updated model configurations and pricing parameters in catalog models JSON.
A developer shell with ANTHROPIC_BASE_URL set reroutes the effective endpoint
away from official, switching off eager tool-input streaming, long cache
retention, the Cowork TLS profile, the Claude Code session header and priority
service tier. 14 tests across 6 files assert those behaviors and fail on a
clean checkout.
Add withOfficialAnthropicEndpoint(), a beforeEach/afterEach pair that removes
the variable and restores it, and call it from the six affected files.
The ask tool stores the user-attached note in result details (note on the
single-question shape, per-item note inside results[]), but the shared
tool renderer dropped it: AskAnswer carried no note field and the card
never rendered one. Extract and render it as a muted row so HTML exports
and the collab guest view keep the user's context.
omp update re-threw Bun's raw fetch() UnsupportedProxyProtocol error, telling CLI users to pass verbose:true to fetch() — an instruction unavailable through the CLI. The update fetch catches now detect this failure and report which proxy env var uses an unsupported scheme plus the http/https requirement.
Fixes#8784
Force process termination via `postmortem.quit(0)` in
`Completions.run()` after writing completion scripts to stdout. Loading
all command modules during completion generation leaves open event loop
handles (sockets, timers) that prevent natural process exit, causing
tools like chezmoi to hang.
Per review on #8717: the customSystemPrompt assignment ran after the hook,
so when both options.customSystemPrompt and an extension payload
replacement were set, the option silently won. Now the option is applied
before the hook (the extension can inspect or drop it in its replacement),
matching anthropic, where the hook runs right before serialization and is
the last word on the wire body.
Adds regression tests: replacement drops customSystemPrompt, replacement
overrides it, and the option still applies when the hook returns undefined.
OpenCode substitutes {env:VAR} and {file:path} in config text at load
time, but OMP's OpenCode discovery ran the generic ${VAR}-only
expandEnvVarsDeep, leaving those tokens literal. An MCP header like
`Bearer {env:MCP_KEY}` reached the server verbatim and returned 401.
The OpenCode loader now applies OpenCode's own substitution to raw
config text before parsing: {env:VAR} -> env value or empty string,
{file:path} -> trimmed, JSON-escaped file contents resolved relative to
the config dir / ~ / absolute, skipping tokens on // comment lines.
Fixes#8778
The Home Manager module symlinked ~/.omp/agent/config.yml to a read-only
/nix/store path. OMP acquires an advisory lock on and atomically rewrites
its config when persisting runtime changes; on macOS the lock backend
creates an flock sidecar next to the target, so the first startup save
failed with "Failed to acquire native file lock ... Permission denied
(os error 13)", breaking every launch once programs.omp.settings was set.
Copy the generated config into place as a writable regular file via
home.activation instead. OMP can now lock and rewrite it, and the next
home-manager switch reapplies the declared settings. The DAG entry is
written literally so the home-manager-free module evaluation in flake.nix
keeps working; that check now asserts the activation entry.
Fixes#8775
usageReservePct scoped limits through scopeClaudeLimitsForModelHardBlock,
which drops a Fable/Mythos weekly tier row until confirmed exhaustion
(>=100% or server exhausted). That guard is correct for credential-wide
hard blocks but wrong for the opt-in, non-destructive reserve fallback:
a tier row at 96% was removed before reserve health, so the model stayed
healthy and kept serving past the configured margin.
Added a scopeLimitsForReserve strategy hook (falls back to scopeLimits)
and pointed the Claude strategy at scopeClaudeLimitsForModel, so reserve
health honors the mapped tier row while credential hard blocks and all
other providers are unchanged.
Fixes#8773
Point xai and xai-oauth at grok-4.6, already in the bundled catalog.
Tests pin the default id in models.json and load picker fixtures from
the catalog so the next bump does not rot hardcoded name or cost.
Current Finder Cmd+C pasteboards advertise both a public.file-url and a
generated 1024x1024 file-icon bitmap. arboard::get_image() succeeded with
the icon, so handleImagePaste attached it before readMacFileUrlsFromClipboard
was reached, sending a generic PNG document icon to vision models instead of
the copied screenshot.
Probe the Darwin file URLs before the bitmap representation so an image-file
URL wins over the co-advertised icon. Pure bitmap pasteboards (screenshots,
browser copies) and non-image file URLs still fall through to the image/text
paths. readMacFileUrls is a no-op off Darwin.
Fixes#8769
Strict Anthropic-compatible endpoints (Z.AI GLM at api.z.ai/api/anthropic)
reject a whole request when a tool_result block carries content: [] ,
returning 400 code 1213 "The prompt parameter was not received normally".
The official API accepts both shapes, so the empty array only surfaced on
compatible endpoints once a tool returned empty output on a vision-capable
model (text-only models already encode the joined empty string).
Normalize the empty array to "" at encode time, alongside the existing
error-placeholder normalization from #2250.
A ThinkingLoop abort is the loop guard asking for a same-model resample
(it injects a thinking-loop-redirect notice that only makes sense on the
model that looped), not a provider failure. #handleRetryableError routed
it through the generic retryable-error branch, so on attempt 1 it called
noteRetryFallbackCooldown + #tryRetryModelFallback and could switch to
another family from retry.fallbackChains while parking the original
selector on a 5-minute cooldown. A healthy Grok 4.6 planning turn got
replaced by whatever the chain listed next.
Carve ThinkingLoop out of the model-fallback branch and out of the
Fireworks Fast->base degrade so the loop guard always re-samples the same
model; the retry budget still bounds a genuinely stuck stream.
Fixes#8760
Review noted that the option whitelist still let generic short flags
escape: `rm -rf -v /` and `rm -rf -i /` were not classified critical,
which is the same separator class the change set out to close.
Pin only the recursive/force flag and skip any other options on either
side of it, which also removes the need to enumerate long options. An
absolute target is still required, so `rm -rf -- ./build`,
`rm --recursive --force ./dist` and `rm -v /tmp/scratch` remain benign
and are asserted.
`CRITICAL_BASH_PATTERNS` required the target to follow one short flag
cluster directly, so anything in between escaped the check:
rm -rf / matched
rm -rf -- / missed
rm --recursive --force / missed
rm -rf --no-preserve-root / missed
rm --no-preserve-root -rf / missed
The last two matter most. GNU coreutils already refuses `rm -rf /` with
"it is dangerous to operate recursively on '/'" and names
`--no-preserve-root` as the override, so the pattern matched the form
that fails safe and missed the form that does not.
Repeat the option separator instead of assuming the path follows one
cluster, and treat `--no-preserve-root` as critical wherever it appears.
Absolute targets are still required for the first pattern, so
`rm -rf -- ./build` and `rm --recursive --force ./dist` stay benign;
both are asserted in the test.
PR #5270 reported the `--` and long-option forms in July and was closed
unmerged by the contributor-vouch bot rather than on merit. This keeps
its cases, credits them in the tests, and adds the `--no-preserve-root`
forms that patch did not cover.
Add grok-4.6 to the SuperGrok Responses effort allowlist so /model
can select low/medium/high/xhigh. Stale omitReasoningEffort cache
rows no longer hide the dial. max is omitted because api.x.ai 400s.
Biome 2.x fully supports CSS parsing, formatting, linting, and assist
actions via its CLI and LSP proxy. The built-in Biome client config
did not include .css in its fileTypes array, causing OMP to skip Biome
for CSS files even when the project's biome.json enables CSS rules.
This change adds .css to the default fileTypes so that projects with
CSS files automatically route through Biome without needing a per-project
override of the full fileTypes array.
Closes#8735
A plugin installed as a bun git dependency contains its full workspace
tree but no node_modules links: bun materializes a git dep's regular npm
dependencies into the host tree and skips workspace:*/file: edges. Bare
imports between workspace siblings inside such a plugin therefore failed
with 'Cannot find module' - the extension host's package walk only
looked at node_modules dirs.
Teach findNodePackageRoot to also recognize workspace roots on the
walk-up: when a directory's package.json declares workspaces (array or
yarn-style { packages }), scan member manifests for the requested name.
node_modules candidates at the same level still win, so explicitly
installed copies shadow workspace members. Exports conditions (bun ->
source) apply to the member manifest as usual, so unbuilt source-only
members resolve.
Contract tests: a fake installed monorepo tree resolves a bare member
import to the member's bun-condition source entry; an installed
node_modules copy shadows the member.