- Rejected bare `A` anchors; single-line ranges must now be spelled `A A`.
- Added a descriptive error for single-number headers to guide model output.
- Updated grammar, tokenizer, prompt docs, and tests to reflect the change.
- Added a `vault.enabled` setting and `isVaultEnabled` guard, and vault resolve, write, and path resolution now threw a disabled error when the feature was off.
- Improved CLI handling by parsing active vault path output and treating `Error:` lines from stdout/stderr as command failures.
- Updated tests to validate the disabled gate, cached active-vault path resolution, and CLI error surfacing on successful exit codes.
- Handled "incomplete" stop reasons in session recovery and auto-compaction workflows.
- Dropped the prior assistant turn before attempting recovery on incomplete-length stops.
- Expanded auto-compaction reason types and triggers to include "incomplete".
- Updated internal URLs parsing internals, export order, tests, and Obsidian URI prompt docs.
- Added vault:// URL parsing, typed variants, and path resolution with vault-root validation.
- Added VaultProtocolHandler with fs and Obsidian CLI-backed resolve/read/write/list support plus caching.
- Added vault scheme integration in router, path utils, and plan-mode guard using resolveVaultUrlToPath.
- Documented vault:// read/edit and `?op`-scoped URI formats in system prompts when Obsidian is available.
- Secured vault:// operations by rejecting traversal, absolute, and symlink-escape path cases.
- Fixed response.incomplete recovery by dropping truncated turns and promoting context.
- Added internal tests for vault protocol parsing, caching, CLI behavior, and invalid-path defenses.
- Replaced the `keepaliveWhile` Promise wrapper with a new `EventLoopKeepalive` class that registers and disposes an interval timer through `Symbol.dispose`.
- Updated `Agent` to instantiate `EventLoopKeepalive` via `using` during prompt execution instead of manually managing an interval.
- Wrapped interactive mode's await path with the new helper and removed redundant `keepaliveWhile` usage from the CLI entrypoint.
Wafer (https://wafer.ai) exposes a single OpenAI-compatible endpoint
(`https://pass.wafer.ai/v1`) for two SKUs whose entitlement differs
server-side, so we model them as two parallel providers — mirroring the
firepass/fireworks split so a user with both subscriptions can switch
without re-pasting:
- `wafer-pass` — flat-rate. `/v1/models` is filtered to entries whose
`wafer.tier === "pass_included"`.
- `wafer-serverless` — pay-as-you-go superset of Pass.
Both issue `wfr_…` keys. `/login wafer-pass` and `/login wafer-serverless`
paste-and-validate via `/v1/models`. `WAFER_PASS_API_KEY` and
`WAFER_SERVERLESS_API_KEY` are wired through `getEnvApiKey`.
Bundled catalog:
- `wafer-pass`: GLM-5.1, Qwen3.5-397B-A17B.
- `wafer-serverless`: GLM-5.1, Qwen3.5-397B-A17B, Kimi-K2.6, Qwen3.6-35B-A3B.
Dynamic discovery via `/v1/models` overlays additional models at runtime
and folds the `wafer` envelope (tier, capabilities, cents/M pricing) into
the canonical `Model<"openai-completions">` shape. GLM-family entries
carry the zai-style thinking compat (`thinkingFormat: "zai"`,
`reasoningContentField: "reasoning_content"`) so reasoning tokens land in
the right field. Cents-per-million → dollars-per-million via /100.
Tests (`packages/ai/test/wafer.test.ts`, 5 cases): bundled catalog
contract for both providers and wire-id pass-through (case-sensitive,
no rewrite — `GLM-5.1` must round-trip verbatim or upstream 404s).
Optional `packages/ai/test/wafer.live.ts` exercises a real round-trip
against `pass.wafer.ai` when `WAFER_PASS_API_KEY` is set.
- Namespaced `AgentSession.executePython()` session IDs before invoking the Python executor.
- Added a regression test proving eval state is visible to the user shortcut path.
- Redesigned hashline patch syntax from anchor-based (`A-B:`) to hunk-header format (`@@ A..B @@`) with unified-diff compatibility.
- Removed `autoDropPureInsertDuplicates` option and simplified apply behavior to preserve duplicated boundary and context lines.
- Changed repeat operator from `^A-B` to `&A..B` and range separator from `-` to `..` for consistency with hunk-header syntax.
- Added image resizing and dimension notes to eval tool output; improved write tool hashline header sanitation for legacy formats.
- Removed 521 lines of boundary-duplicate absorption code and simplified parser to auto-convert bare body rows and unified-diff contamination.
- Classified usage-limit gateway responses as `429 rate_limit_error` in auth handling paths.
- Aligned auth-gateway and pi-native key retrieval with derived `sessionId` for `getApiKey` lookups.
- Handled usage-limit auth failures by rotating credentials with retry hints and returning undefined when none available.
- Replaced stream auth checks with retryable-upstream logic for 401 and usage-limit errors before content.
- Expanded `extractRetryHint` parsing for `~`, `sec`, `ms`, and minute/hour units.
- Added coverage for classifyGatewayError, retry-hint parsing variants, and stream-auth retry edge cases.
When the operator disables MCP client-side timeouts via timeout: 0 or OMP_MCP_TIMEOUT_MS=0, do not impose a 1s startup deadline on the optional HTTP GET SSE listener — let the listener wait as long as the server takes so server-to-client messages are not lost.
Refs #1460
Abort the optional Streamable HTTP GET SSE listener attempt after a short bounded startup window so POST-only request/response servers can finish initialization.
Fixes#1460
- Replaced 4-hex content-derived file hashes with 3-hex opaque tags minted by InMemorySnapshotStore, making tags session-bound pointers rather than content fingerprints.
- Removed lru-cache dependency; replaced LRU-bounded per-path rings with a flat 4096-slot global ring using a scrambled permutation to prevent LLM tag extrapolation.
- Made SnapshotStore required in Patcher (was optional); tag resolution now drives stale-anchor detection instead of recomputing hashes at apply time.
- Changed literal payload sigil from `|` to `+` and accepted `^A` shorthand for `^A-A`; added lenient recovery for bare bodies, lone `-` rows, and overlapping bare/concrete block pairs.
- Updated the auth-broker CLI to import the transport setter from the logger module.
- Replaced the logger.setTransports call in runServe with the dedicated setTransports helper.
- Added a `madeExecutable` result field to `WriteToolDetails` to surface executable changes.
- Implemented `maybeMarkExecutableForShebang` to chmod shebang files executable while preserving existing mode bits and swallowing chmod errors.
- Updated write flow and renderer output to return and display when a file was auto-marked executable.
- Replaced anchor shorthand syntax with explicit range format (1: -> 1-1:) and removed ^/v sigils in favor of ^A-B repeat and A-B:- delete operations.
- Added repeat edit kind to support ^A-B syntax for copying lines A through B, and inline delete syntax A-B:- for range deletions.
- Removed after_anchor cursor kind and standalone delete rows; empty anchor blocks now produce blank-line replacements instead of deletions.
- Updated parser, tokenizer, and type system to discriminate literal and repeat payloads, and refactored apply/recovery logic to expand repeat edits into individual inserts.
- Updated coding-agent test fixtures and settings documentation to reflect new hashline syntax and behavior.
- Narrowed the xai-oauth bundled model cast to `Model<"openai-responses">` in its regression test.
- Changed hashline stale-recovery fixtures to use `repl(...)` for both line-replacement payloads instead of `extra(pl(...))`.
A stalled Jina reader request shared the overall reader-mode AbortSignal
with the downstream trafilatura/lynx/native fallbacks. When Jina hung
until the budget timer fired, the shared signal aborted and the catch
handler's signal?.throwIfAborted() re-threw before any local fallback
ran.
- Bound Jina and Parallel extract to their own per-attempt sub-budget
(REMOTE_READER_MAX_MS, capped at 10s) so a remote stall cannot consume
the whole overall reader-mode budget.
- Catch handlers now rethrow only on real userSignal cancellation, not
on remote sub-budget or overall budget expiry.
- Wrap trafilatura/lynx in their own try/catch so a subprocess failure
or abort does not skip the in-process native renderer.
- Always attempt the native renderer last: it works on already-loaded
HTML with no network or subprocess, so even an exhausted overall
budget still yields a result.
Fixes#1449
PR #1422 closes a real corruption window in the coding-agent end-to-end edit path (proven by the new test in packages/coding-agent/test/core/hashline.test.ts), but the coding-agent [Unreleased] section had no Fixed entry. AGENTS.md requires every package-affecting change to land under [Unreleased]; add the entry so the fix is not invisible at release time.
The recovery fallback that replays edits onto current text when the structured-patch 3-way merge refuses guarded only on line-count equality. If a prior in-session edit rewrote the very line a later stale-hash edit re-targets, replay overwrote the new content with the stale-anchored payload and emitted a 'Verify the diff matches your intent' warning that does not block the write.
Concrete window: v0 line 5 = 'L5', v1 = 'L5-CHANGED' (same line count). Edit E2 authored against H0 anchored at line 5 lands on v1 because the line-count gate passes, silently replacing L5-CHANGED with the model's L5-MODEL.
Add a verifyAnchorContent gate: walk every edit's anchors and require previousText[line] === currentText[line]. Any mismatch returns null so the caller raises MismatchError and the model re-reads. The success path now emits the standard RECOVERY_SESSION_CHAIN_WARNING (the hedged REPLAY_WARNING text was only sensible when content was partially aligned; that case is now unreachable, and the constant is removed).
Tests: packages/hashline/test/recovery-session-chain.test.ts pins both the corruption refusal and the safe-replay positive case (anchor on an unchanged line, 3-way merge fails on neighbouring rewritten context, replay succeeds with the standard chain warning). packages/coding-agent/test/core/hashline.test.ts adds an end-to-end through executeHashlineSingle so the production patcher path is covered too.
- Added check to avoid returning DEFAULT_BASE_URL when a custom provider base URL is configured.
- Passed resolvedModel argument to resolveXAIHttpCredentials call in image generation tool.
- Added `openrouterVariant` option to `SimpleStreamOptions` and `OpenAICompletionsOptions` to append routing suffixes (`:nitro`, `:floor`, `:online`, `:exacto`) to OpenRouter model IDs at request time.
- Skips appending when the model ID already carries an explicit colon-suffix.
- Exposed `providers.openrouterVariant` setting in the coding-agent UI under Settings → Providers.
- Plumbed through `pi-native-server` forwarder and `AgentSession` options preparation.
Patch axis: extend
Displacement: net-zero; reuses existing plan reference state instead of adding persistence or overwriting approved artifacts
Rule violations averted: no approved-plan overwrite, no transcript format migration, no public CLI/API expansion
PASS/FAIL: PASS after plan-mode focused tests and package check. Note: system-prompt-templates has an unrelated HOME=/tmp path-shortening expectation failure.
The Surface 1 commit added authStorage.hasNonEnvCredential as a credential
gate inside resolveXAIHttpCredentials, and the Surface 3 commit added
resolveXAIBaseURL which consults getProviderBaseUrl and getAll. The
existing image-gen xAI test mocked only getApiKeyForProvider on
modelRegistry, so the new code paths threw "undefined is not an object"
at runtime.
Add the missing mock surface:
- authStorage.hasNonEnvCredential returns true for "xai-oauth" so the
dedicated-credential gate routes through the xai-oauth branch (which
the test's getApiKeyForProvider mock services).
- getProviderBaseUrl returns undefined so resolveXAIBaseURL falls
through to the XAI_BASE_URL / DEFAULT_BASE_URL leg, preserving the
test's existing expectation that the request hits
https://api.x.ai/v1/images/generations.
- getAll returns [] so the per-model override check in
resolveXAIBaseURL no-ops cleanly.
Op: correct
Restores: ref:feat/xai-grok-oauth@015437534 ref:feat/xai-grok-oauth@2c1abd7fa
The xAI image branch on origin/main always posts to /v1/images/generations
and hard-codes `resolution: "1k"`, contradicting two advertised contracts:
P1: generate_image schema declares `input: z.array(inputImageSchema)`
globally; resolvedImages was populated for every provider but the xAI
branch POST body only forwarded text fields. Image-edit and
multi-reference prompts silently degraded to text-only.
P2: user-supplied image_size was ignored on the xAI path even though
every other provider honors it via resolveOpenAIImageSize /
imageConfig.imageSize.
Fix:
* Add XAIImageReference and XAIImageRequestBase typed interfaces;
combine into a discriminated XAIImageRequestBody union with mutually-
exclusive image / images fields (text-only branch carries
`image?: never; images?: never`).
* Route to POST /v1/images/edits when resolvedImages.length > 0; map
1 source -> `image: {url, type}`, 2-3 sources -> `images: [{url,
type}, ...]` per docs.x.ai. Cap at 3 with a tool-level error; xAI
documents that limit.
* Reuse the existing toDataUrl(InlineImageData) helper for the `url`
field (data: URIs are accepted alongside public URLs per docs.x.ai).
`type: "image_url"` is the OpenAI-compat discriminator every official
xAI code example sends.
* Add resolveXAIResolution(image_size): map OpenAI-style pixel size to
xAI's discrete "1k" | "2k" tier. 1024x1024 -> 1k; anything wider ->
2k. Absent image_size still defaults to "1k", matching hermes-agent
DEFAULT_RESOLUTION (plugins/image_gen/xai/__init__.py:71).
* buildXAIEditPayload uses tuple destructure + explicit guard rather
than `resolvedImages[0]`, staying safe under future
noUncheckedIndexedAccess: true.
No effect on the OpenAI / OpenAI-codex / antigravity / gemini / openrouter
branches.
Op: correct
Restores: ref:feat/xai-grok-oauth@ecedf7c7e
resolveXAIHttpCredentials honored only \$env.XAI_BASE_URL — every
per-model baseUrl pin (models.yml model.baseUrl) and every provider-
level override (providers.xai-oauth.baseUrl) was silently bypassed for
image and TTS HTTP requests, even when the chat path went through the
override correctly via Model.baseUrl on the Responses request.
Add resolveXAIBaseURL: (1) per-model override when merged.baseUrl
diverges from the bundled default, scoped to (provider, id) so xai and
xai-oauth entries with the same id don't cross-route, (2) provider-level
baseUrl from ModelRegistry.getProviderBaseUrl, (3) XAI_BASE_URL env,
(4) DEFAULT_BASE_URL. resolveXAIHttpCredentials takes an optional
modelId; probes pass undefined and fall through to env/default.
Op: correct
Restores: ref:feat/xai-grok-oauth@2c1abd7fa
The cross-provider env fallback (stream.ts: "xai-oauth" → XAI_OAUTH_TOKEN
|| XAI_API_KEY) lets an XAI_API_KEY-only setup silently satisfy the
xai-oauth credential branch in resolveXAIHttpCredentials. Once the
helper enters that branch it resolves baseURL under xai-oauth instead of
xai, bypassing providers.xai.baseUrl overrides for image/TTS traffic.
Add AuthStorage.hasNonEnvCredential — hasAuth minus the env-fallback
leg — and gate the xai-oauth branch on (dedicated credential source ||
$env.XAI_OAUTH_TOKEN). The XAI_API_KEY borrow now falls through to the
xai branch, preserving back-compat while restoring provider-level
baseUrl precedence for users with a dedicated xai-oauth source.
Op: correct
Restores: ref:feat/xai-grok-oauth@015437534
Triple-stacked failure on the same axis (thinking effort) produced the
user-visible
Error: Compaction failed: Thinking effort high is not supported by
xai-oauth/grok-build.
Supported efforts:
(empty list after the colon) whenever the active model was a curated
xAI catalog entry with compat.supportsReasoningEffort: false.
Three defects lined up. (1) Behavior: compaction at four call sites
in packages/agent/src/compaction/compaction.ts hardcoded
reasoning: Effort.High and never threaded session.thinkingLevel —
the user's /model :off selection (and any explicit low/medium) was
silently overridden. On every other model this was invisible.
(2) Validation: requireSupportedEffort threw at the openai-flavored
mapper layer before the wire-side omitReasoningEffort gate in
providers/xai-responses.ts ever ran; two contradictory guards on the
same wire param. (3) Message: when getSupportedEfforts returned [],
the rendered error tail was 'Supported efforts: ' with nothing after
the colon — disappears as a side-effect of fix#2.
Fix#1 — thread ThinkingLevel | undefined end-to-end. Add
SummaryOptions.thinkingLevel and HandoffOptions.thinkingLevel.
Convert via a single exhaustive switch (effortFromThinkingLevel) in
the new resolveCompactionEffort helper:
- Off → undefined (omit reasoning entirely)
- undefined/Inherit → Effort.High → clamp per model (preserves the
historical default for users
who never touched the dial)
- explicit Effort → respect user → clamp per model
resolveCompactionEffort lives in compaction.ts; all four call sites
(generateSummary, generateHandoff, generateShortSummary,
generateTurnPrefixSummary) route through it. agent-session.ts threads
this.thinkingLevel into all three production compaction entry points
(manual /compact at L6201, auto-compaction at L6458 — the most-fired
path, originally missed in plan review — and direct generateHandoff
at L5465). The audit-gate test
(test/agent-session-compaction-thinking-threading.test.ts) scans the
file with a brace-balanced extractor and refuses any unthreaded site.
Fix#2 — silent-clamp at the openai-flavored mapper layer. Extract
exported modelOmitsReasoningEffort(model) in model-thinking.ts as the
single source of truth for compat.supportsReasoningEffort: false on
openai-responses* APIs. getSupportedEfforts now calls it instead of
inlining the check (pure refactor — observable behavior preserved).
resolveOpenAiReasoningEffort in stream.ts early-returns undefined
when the predicate is true, so the wire-side omitReasoningEffort
gate (providers/xai-responses.ts:78) becomes the single source of
truth for the actual strip — no redundant throw.
Three regression tests pin the contract:
- packages/ai/test/xai-oauth-effort-strip.test.ts (5 tests):
modelOmitsReasoningEffort returns true for grok-build and
grok-4.20-0309-reasoning, false for grok-4.3 / Anthropic /
openai-completions.
- packages/agent/test/compaction-thinking-level.test.ts (5 tests):
every ThinkingLevel outcome through generateHandoff — Off stays
undefined (not coerced to High), Low stays Low, Inherit / undefined
default to High, grok-build clamps to undefined regardless of
requested level. Covers the Codex-caught Off-vs-not-provided
distinction.
- packages/coding-agent/test/agent-session-compaction-thinking-threading.test.ts
(2 tests): brace-balanced source scan asserts every direct
compact() / generateHandoff() in agent-session.ts threads
'thinkingLevel: this.thinkingLevel'; floor of 3 threaded sites.
TDD red-green verified for fix#1: temporarily reverted the handoff
call-site back to hardcoded Effort.High → compaction-thinking-level
went 2 pass / 3 fail (Off coerced, Low overridden, grok-build throws);
restored → 5 pass / 0 fail.
Verified:
- packages/agent: 127 pass / 0 fail
- packages/ai: 1061 pass / 337 skip / 0 fail
- packages/coding-agent (focused): 179 pass / 5 skip / 0 fail
- biome + tsgo --noEmit clean across all three packages
Out of scope (follow-ups):
- branch-summarization.ts:307 already passes no reasoning — no edit.
- The empty-list error message at model-thinking.ts:296 is now
structurally unreachable from the openai-responses path.
- modelOmitsReasoningEffort and grokSupportsReasoningEffort
(xai-responses.ts:22) overlap; collapse into a single predicate
in a future commit.
Op: correct
Restores: spec:compaction-honors-session-thinking-level
Restores: spec:xai-oauth-grok-build-compaction-no-throw
(cherry picked from commit e07b47ee46769053c658819437e2478389a4cee0)
- Added `extraBody` merging into OpenAI Responses request params.
- Fixed xAI OAuth redirect URI to fail fast on port conflicts.
- Exposed `antigravity` and `xai` as explicit `providers.image` options.
- Added `isImageProviderPreference` guard, replacing inline string checks.
- Fixed TTS tool to resolve output path relative to cwd and require write approval.
Five symbols in packages/ai/src/utils/oauth/xai-oauth.ts were exported
but only consumed inside the file itself:
- xaiOAuthDiscovery (used 3x internally)
- XAIOAuthDiscovery (return type of xaiOAuthDiscovery)
- buildXAIAuthorizeUrl (used 1x internally)
- BuildXAIAuthorizeUrlOptions (arg type of buildXAIAuthorizeUrl)
One symbol was both exported and fully unused (zero internal or external
references, no test coupling): XAI_ACCESS_TOKEN_REFRESH_SKEW_SECONDS.
The accompanying comment claimed it was 'used by AuthStorage to refresh
ahead of expiry' but no such call exists; AuthStorage uses the standard
5-minute client-skew baked into the OAuthCredentials.expires field via
ACCESS_TOKEN_CLIENT_SKEW_MS.
packages/coding-agent/src/lib/xai-http.ts exported XAICredentials, but
no consumer imports the type by name (callers use type-inference from
resolveXAIHttpCredentials' return type). Unexporting it keeps the
public surface minimal.
bun check baseline 53 errors preserved; bun test xai-oauth 9/9 passing.
Op: compress
The XAI_ASPECT object mapped each key to itself and held two keys
(3:2, 2:3) the aspect_ratio schema can never produce. The lookup
XAI_ASPECT[params.aspect_ratio ?? "1:1"] is semantically equivalent
to params.aspect_ratio ?? "1:1" — the schema's enum
["1:1", "3:4", "4:3", "9:16", "16:9"] already constrains the
type, and xAI's /v1/images/generations accepts those strings directly.
bun check baseline 53 errors preserved (no new TS errors).
Op: compress
Notes the new xAI Grok OAuth provider in /login, the XAI_OAUTH_TOKEN
env-var fallback, the four new optional fields on OpenAIResponsesOptions,
the generate_image xai branch, and the new tts tool.
Op: extend
Adds packages/coding-agent/src/tools/tts.ts: a CustomTool that POSTs to
https://api.x.ai/v1/tts using the shared xAI credentials helper
(supports both SuperGrok OAuth and plain XAI_API_KEY).
Built-in voices: ara, eve (default), leo, rex, sal. xAI also accepts
custom voice IDs (the schema does not enum-restrict voice_id). Output
codec inferred from output_path suffix (.wav → wav, else mp3). Max
15,000 characters per request. Composes the callers abort signal with
a 60s timeout fence.
Wired into sdk.ts immediately after the image-gen tool registration,
matching the await logger.time(...) pattern.
Ported from NousResearch/hermes-agent (MIT) — tools/tts_tool.py
L167-171 (constants) and L896-959 (_generate_xai_tts).
Op: extend
Adds packages/coding-agent/src/lib/xai-http.ts: a shared credential
resolver used by image generation (this commit) and TTS (next commit).
Tries the xai-oauth SuperGrok token first via
ModelRegistry.getApiKeyForProvider (refresh cascade lives there);
falls back to XAI_API_KEY. Ported from NousResearch/hermes-agent (MIT).
Extends imageGenTool with a "xai" provider branch that POSTs to
https://api.x.ai/v1/images/generations with the Grok Imagine surface:
grok-imagine-image (default, $0.02/image) or
grok-imagine-image-quality ($0.05/image). Aspect ratios 1:1, 16:9,
9:16, 4:3, 3:4, 3:2, 2:3. Resolutions 1k/2k. Decoded via the existing
saveImagesToTemp helper — no new image-handling code paths.
Op: extend