974 Commits
Author SHA1 Message Date
10a9190bea fix(ui): keep SpinnerModeGlyph visible inside status parens (#2047)
* fix(ui): keep SpinnerModeGlyph visible inside status parens

Always render the ↑/↓ mode glyph for leader spins so early requesting
and thinking-only phases are not blank, and place it as the first
status part inside the parentheses next to other activity cues.

Closes #2033

* fix(ui): preserve narrow-terminal thinking with mode glyph

Restore a second-chance width gate for leader thinking-only status under
the new inside-parens glyph layout, and suppress the mode glyph when the
row cannot fit minimal status chrome.

* fix(ui): prefer bare thinking over glyph-only on narrow rows

When leader thinking-only cannot fit glyph+thinking chrome, fall back to
bare (thinking) instead of empty mode-glyph status. Tighten glyph residual
budget to account for GlimmerMessage trailing space.

* fix(ui): budget glimmer space in bare thinking fallback

Bare leader thinking-only residual must reserve the GlimmerMessage
trailing space so equality-width terminals do not overflow by one column.

* fix(ui): nest teammate bare thinking under reduced motion

Apply the bareThinkingOnly nested (thinking) wrap in both shimmer and
dimColor branches so teammate thinking-only status keeps parentheses
when reduced motion disables the shimmer arm.

* test(ui): assert exact SpinnerAnimationRow status rows

Fix TS1355 from invalid null as const in baseProps and replace partial
toContain/regex checks with full ANSI-stripped row equality for the
glyph placement regressions CodeRabbit requested.

* fix(ui): prefer status content over empty mode-glyph chrome

When reserving the SpinnerModeGlyph would drop tokens/timer from the
status row, drop the glyph instead. Keep thinking full-chrome recovery,
default unknown modes to down-arrow, and tighten exact-row tests for
typecheck plus CodeRabbit feedback.

* fix(ui): preserve spinner tokens when glyph crowds status

* fix(ui): suppress empty glyph chrome and preserve token recovery

Skip glyph-only status when numeric thinkingStatus cannot fit on narrow terminals, and refuse glyph-free recovery that would swap visible tokens for a timer-only layout.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ui): tighten glyph recovery for duration and timer bands

Exclude numeric post-thinking duration from glyph-only status (including requesting), prefer streaming tokens over duration when both cannot fit, and recover timer+token rows at the col-30 boundary.

* fix(ui): harden SpinnerAnimationRow glyph recovery priorities

Prefer tokens over timer/duration on mid-narrow rows, recover exact-fit
token columns, keep full effort text when glyph chrome fits, and prefer
active thinking over timer-only status. Tests now use production
Thinking… message width and frozen-clock exact row assertions.

* fix(ui): address CodeRabbit SpinnerAnimationRow recovery nits

Document token-over-thinking tie-break and > vs >= bare-pass split, drop
redundant suppressModeGlyph assignments on token-only fallbacks, and make
exact-row tests use PROD_MESSAGE plus an explicit padded mode-glyph helper.

* fix(ui): drop overflowing spinner suffix for tokens/thinking

Recover mid-narrow status when stop-hook/tool suffixes overflow bare
chrome, prefer live tokens over duration after the drop, and cover long
production verb column bands plus suffix regressions.

* fix(ui): recover status after SpinnerAnimationRow suffix overflow

Re-gate tokens when thinkingStatus is null after dropping a crowding
suffix, restore teammate nested thinking on the same path, and drop the
mode glyph before truncating a suffix that still fits under bare parens.

* fix(ui): complete SpinnerAnimationRow suffix and glyph recovery

Restore timer symmetrically after suffix drop, drop crowding suffixes
when preferTokens would overflow, keep already-visible thinking when
tokens unlock, and prefer tokens over a bare-fitting suffix that cannot
share the row.

* fix(ui): harden SpinnerAnimationRow recovery against wrap cliffs

Budget timer co-restore against all visible parts, re-gate tokens onto
timer-only rows after tokens-over-suffix, prefer thinking over a crowding
bare-fit suffix, and restore the mode glyph only after a suffix-keep
cascade.

* fix(ui): close SpinnerAnimationRow mid-narrow recovery cliffs

Prefer tokens over thinking when they cannot share bare chrome, tighten
thinking-over-suffix exact-fit to avoid a one-column suffix cliff, and
restore the mode glyph whenever recovered leader content fits.

* test(ui): cover SpinnerAnimationRow cliff and glyph-restore cases

* fix(ui): restore tokens beside thinking after suffix recovery

* fix(ui): close SpinnerAnimationRow suffix recovery cliffs

Budget timer and rendered thinking width in suffix-fit predicates so
widening does not drop the elapsed timer or streaming tokens. Co-restore
teammate tokens when thinking crowds timer-only rows, clear the mode glyph
when thinking+token recovery cannot fit glyph chrome, and budget full
effort text before keeping a stop-hook suffix.

* refactor(ui): collapse redundant SpinnerAnimationRow suffix-fit branches

Rely on the combined all-visible suffix budget check instead of
duplicate tokens-only paths. Keeps timer+thinking and no-token
thinking fallbacks unchanged.

* fix(ui): re-gate recovered spinner glyph

* test(ui): tighten spinner layout coverage

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-29 09:40:32 +08:00
JATMNandGitHub 2fe1e1b148 refactor(openai-shim): extract generic and Gemini stream conversion (#2008)
* test(openai-shim): anchor JSON fallback ownership

* refactor(openai-shim): extract stream conversion

* fix(openai-shim): preserve stream completion semantics

* fix(openai-shim): finalize incomplete streams

* fix(openai-shim): reject incomplete tool streams

* test(openai-shim): cover split raw tool text

* fix(openai-shim): reject incomplete fallback tools
2026-07-29 09:39:48 +08:00
0xfandomandGitHub 3925f2791c feat(auth): opt-in loopback proxy hosts that keep subscription (OAuth) auth (#2050)
* feat(auth): opt-in loopback proxy hosts that keep OAuth first-party

Pointing ANTHROPIC_BASE_URL at any host other than api.anthropic.com switches
the client to API-key mode, dropping a signed-in subscription session. That
blocks running the CLI through a local transparent proxy (compression,
inspection, caching) that forwards auth headers to Anthropic unchanged.

Add ANTHROPIC_FIRST_PARTY_PROXY_HOSTS: a comma-separated host[:port] allowlist
that extends first-party detection. It is honored only when the base URL points
at a loopback host, and only loopback entries are considered -- both checks are
redundant by design so a misconfigured non-loopback entry can never widen
first-party status to an off-machine host. Default behavior is unchanged.

Closes #2016

* docs(auth): document ANTHROPIC_FIRST_PARTY_PROXY_HOSTS

* fix(auth): harden loopback proxy allowlist matching

Normalize the base URL port to its scheme default (80/443) before
comparing an explicit allowlist port, so a `127.0.0.1:80` entry matches
`http://127.0.0.1`. Reject embedded credentials and non-http(s) schemes
up front so an OAuth session is never attached to a URL carrying userinfo
or a non-proxy scheme.
2026-07-28 22:39:41 +08:00
0xfandomandGitHub 580a6b1197 fix(sdk): report a permission timeout as a timeout (#2028)
* fix(sdk): report a permission timeout as a timeout

On timeout the handler called denyPendingPermission and then fell through
to the fallback. The deny resolves the promise registered by
registerPendingPermission, but Promise.race has already settled with
{timedOut: true}, so nothing is awaiting it and the decision is discarded.

The fallback is createDefaultCanUseTool, whose contract is that the host
supplied no permission callback at all. A host that wired up
onPermissionRequest and simply answered too slowly therefore got the tool
result 'no canUseTool or onPermissionRequest callback provided. Pass
canUseTool in options', plus the matching warning on stderr -- both false,
and both pointing at a configuration problem that does not exist. It also
consumed the one-shot warning latch, so a genuinely misconfigured later
query in the same process is never warned.

Return the timeout decision directly. The permission_timeout event and the
existing deny are unchanged.

* test(sdk): move the timeout cases into the existing permissions suite

tests/sdk/permissions.test.ts pinned the old behavior -- it asserted the
timeout result was the fallback's message, with a comment describing the
fall-through as intended. It is not: that message claims no permission
callback was provided, which is false whenever onPermissionRequest is
wired up. Assert the timeout reports itself instead.

The new cases live in that suite rather than a new file: a separate test
file adds a slot to bun's sequential file ordering, which shifted which
suite runs before which and surfaced an unrelated mock leak in CI
(taskReport git metadata and the /ads command).

* test(sdk): drive the permission-timeout case off a mocked clock

The no-callback-fallback-on-timeout test relied on a real 10ms wait, so
the deny hinged on scheduling. Use fake timers and advance the clock by
the timeout window instead, making the timer the deterministic cause of
the denial.
2026-07-28 22:38:49 +08:00
3c5856a004 feat(integrations): add Ling 3.0 Flash free to the Opengateway catalog (#2057)
* feat(integrations): add Ling 3.0 Flash free to the Opengateway catalog

inclusionai/ling-3.0-flash:free — 124B MoE reasoning model, 262K
context, 32K max output, tool calling verified through the gateway.
Free window on the gateway runs until 2026-08-03; the gateway delists
it automatically after that.

* test(model): include Ling 3.0 Flash in the Opengateway picker expectation

The static descriptor picker asserts the exact Opengateway catalog;
inclusionai/ling-3.0-flash:free now sits between Nemotron and HY3.

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-07-28 22:33:47 +08:00
12994f2c97 feat(ui): single-row centered startup logo with ANSI Shadow wordmark (#2053)
* feat(ui): single-row centered startup logo with ANSI Shadow wordmark

Render OPEN and CLAUDE side by side as one centered 6-line block on the
pre-Ink startup screen, redrawn in ANSI Shadow letterforms (consistent
shadow corners, D-shaped D, clean N). Terminals narrower than the 94-col
row fall back to two stacked blocks, each centered as a unit so rows stay
aligned. The tagline, provider box, and version line are centered to
match.

The Ink welcome panel wordmark (constants/brand.ts) becomes a matching
single row: letter-spaced caps flanked by shade-gradient accents,
keeping the shimmer/brand two-tone split.

Adds layout unit tests (one-row vs stacked switchover, block centering,
box centering) and brand wordmark invariant tests; updates the D-shape
glyph regression for the new font.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* test(ui): address CodeRabbit review — wordmark render test, row centering asserts

Extract the LogoV2 wordmark row into a WordmarkRow component and add a
focused render test asserting segment order and the shimmer/brand color
split (left accent + OPEN in brandShimmer, CLAUDE + right accent in
brand), via renderToAnsiString with chalk pinned to truecolor.

Extend the startup-screen layout test to assert the tagline and version
rows are centered, alongside the existing provider-box check.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-07-28 13:51:08 +08:00
JATMNandGitHub 9d5b77db89 fix(query): do not trip tool-failure guard on parallel same-turn failures (#2048)
* fix(query): count parallel tool failures once per turn

Prevent the tool-failure loop guard from stopping a response when a single model turn fans out parallel tool calls that share an error signature, category, or path. Cross-turn accumulation still trips after the model has a chance to adapt.

Fixes #2035

* fix(query): emit one advisory per signature in a parallel batch

Once-per-turn counting left the penultimate advisory path repeating for every duplicate failure in the same batch. Gate advisory emission on newly seen signatures this turn.

* test(query): lock parallel once-per-turn failure counting

Cover same-path parallel batches and assert the post-batch streak so a
partial double-count regression cannot slip past the suite.
2026-07-28 13:45:37 +08:00
JATMNandGitHub ca29d4454f refactor(openai-shim): extract XML and response conversion (#2007)
* refactor(openai-shim): extract XML and response conversion

* fix(openai-shim): align facade tests with XML extraction

* fix(openai-shim): restore shared XML tool-call sequencing

Wire parseXmlToolCalls through the façade sequence counter, reuse the
shared extractBalancedJson helper, and restore production dependency
coverage in conversion and façade tests.

* fix(openai-shim): restore façade coverage and require XML sequencer

Restore non-streaming convert and HY3 JSON-fallback e2e seams so façade
dependency wiring stays exercised, require an injected XML id sequencer,
and assert consecutive shared-sequence ids.

* fix(openai-shim): restore provider coverage and array content guard

Keep relocated openaiShim suite tests in test:provider, and reject
non-object array content parts the same way the inline converter did.

* fix(openai-shim): preserve mixed XML/HY3 tool-call order

Sort HY3 and standard XML candidates by source offset before ID
assignment, isolate focused test sequencers, and restore fetch after
the Gemini non-streaming façade test.
2026-07-28 13:44:56 +08:00
0xfandomandGitHub 83440a6fe6 fix(stats): stop over-counting totalDays by one (#1953)
* fix(stats): stop over-counting totalDays by one

The /stats 'active X of Y days' denominator (totalDays) diffed the first and
last session ISO timestamps raw, ran Math.ceil over the millisecond gap, then
added 1. firstSessionDate/lastSessionDate are full timestamps, so ceil rounds
any sub-day remainder up to a whole day and the +1 double-counts it: all
activity on a single calendar day reported 2 days (50% active instead of
100%), and every non-24h-multiple span was one day long.

Extract inclusiveCalendarDaySpan, which snaps both endpoints to their UTC date
(via toDateString, the same basis activeDays/dailyActivity use) before
differencing, so the gap is an exact multiple of 24h and the inclusive +1 is
correct. Use it at both call sites (cacheToStats and
processedStatsToClaudeCodeStats).

* fix(stats): guard day-span helper against unparseable persisted dates

A same-version stats cache can carry a structurally-valid but unparseable
firstSessionDate (e.g. "not-a-date"). That value reached inclusiveCalendarDaySpan,
where new Date(...).toISOString() throws RangeError and aborts the whole /stats
render instead of degrading. Parse the endpoints up front and fall back to 0
(the same value callers use for a missing endpoint) when either is invalid.

* fix(stats): reject malformed persisted dates instead of trusting Date.parse

Date.parse accepts far more than the shapes this pipeline persists: "2026-07",
"2026", "123" and "01/01/2026" all resolve to real dates, so a truncated or
foreign-format cache value produced a plausible-but-wrong totalDays rather than
being rejected. Require the value to start with a full ISO calendar date — the
two shapes actually written are a session.timestamp instant and a bare
dailyActivity YYYY-MM-DD key — and fall back to 0 otherwise.

* test(stats): guard the off-by-one with spans that separate the formulas

The multi-day, identical-timestamp and adjacent-midnight cases return the same
value under both the old Math.ceil(gap)+1 and the new calendar-day formula, so
they could not catch a regression. Add the canonical failing spans (1.5 and 3.5
raw days, where the old formula reported 3 and 5 instead of 2 and 4), a bare
dailyActivity date-key case, and the malformed-date rejections.

* fix(stats): reject impossible calendar dates, not just non-date shapes

The corruption guard only checked for a date-shaped prefix, so Date.parse still
normalized impossible values — 2026-02-30 parsed as March 2 and
inclusiveCalendarDaySpan('2026-02-30', '2026-03-02') returned 1 instead of the
documented 0 fallback, letting a corrupt cache date fabricate the /stats
denominator. Validate the spelled year/month/day against the real calendar
(leap years included) before trusting the parse. Regression covers rollover,
month/day bounds, and Feb 29 in leap vs non-leap years.

* fix(stats): accept only the two persisted timestamp shapes

The prefix check also matched a space-delimited value such as
'2026-07-13 23:30:00', which Date.parse then read as a host-local
timestamp. That is neither a bare dailyActivity date key nor an ISO
instant emitted by the pipeline, so the computed span depended on the
machine's timezone — inclusiveCalendarDaySpan('2026-07-13 23:30:00',
'2026-07-14T00:30:00.000Z') returned 2 under UTC and 1 under
America/Los_Angeles instead of the documented 0 fallback for corrupt input.

Anchor the pattern at both ends and require the zone designator on the
instant form.

* fix(stats): select session endpoints chronologically and reject bad clocks

Both aggregation paths picked firstSessionDate/lastSessionDate by string
comparison, but offset-qualified instants do not sort that way:
2026-07-13T23:30:00-10:00 is later than 2026-07-14T00:00:00+14:00 while
sorting earlier, so it was chosen as the first endpoint and the span came
out 0 for two sessions that occupy different UTC days. Compare parsed
epochs, falling back to string order only for values that do not parse so
selection stays total.

The persisted-date guard also let out-of-range clock components through.
Date.parse normalizes 2026-07-13T24:00:00.000Z to midnight on July 14, so
a corrupt timestamp fabricated a day of span instead of taking the
documented 0 fallback. Validate hours/minutes/seconds and the offset the
same way the calendar components are already validated.

* fix(stats): order the cached first session chronologically too

The cache writer is the companion to the endpoint selection this PR fixed,
and it had the same lexical comparison. Offset-qualified timestamps do not
sort by the instant they denote, so merging 2026-07-14T00:00:00+14:00 (UTC
July 13) with 2026-07-13T23:30:00-10:00 (UTC July 14) stored the later one
as the cache's first session. A later cached /stats run has no session list
left to correct that, so it reports one total day for activity spanning two
UTC dates.

The persisted-date helpers move to statsCache.ts and are re-exported from
stats.ts: the cache writer needs them, and stats.ts already depends on that
module, so importing the other way would be a cycle.

Also assert the exact two-day span in the offset test instead of merely a
positive result, which a regression to 1 would have passed.

* fix(stats): heal a corrupt persisted firstSessionDate seed

Both first-session selection loops seed firstSessionDate from the persisted
cache, which can hold a corrupt value. A garbage seed that sorts lexically
before every real timestamp (e.g. "1") is never displaced by
comparePersistedDates, so the corruption -- and the wrong totalDays it drives
-- persists across every later run. Treat an unparseable seed as absent so
the first valid session date replaces it.

* fix(stats): accept the ISO spellings the ingestion path persists

parsePersistedDateMs required seconds and a colon in the numeric offset, so it
returned NaN for valid instants that processSessionFiles stores verbatim --
`2026-07-13T12:00Z` and `...+0000`. The session was still counted in
dailyActivity but the span came out 0, so /stats reported zero total days for
real multi-day activity. Make the seconds group and the offset colon optional
while keeping the zone requirement and range/calendar validation. Also use the
explicit leap rule instead of Date.UTC(year, ...), which maps years 0-99 to
1900-1999 and judged year 0000 inconsistently with the Date.parse result.
2026-07-28 13:44:07 +08:00
JATMNandGitHub 53d3cf7f1d fix: apply ultrathink effort to provider requests (#2046)
* fix(query): apply ultrathink effort to API requests

* fix(attachments): honor opt-out for speculative effort

* test(attachments): isolate ultrathink feature mock
2026-07-27 09:43:55 +08:00
JATMNandGitHub a65201396a fix(cache-probe): omit unsupported cache request fields (#2044)
* fix(cache-probe): omit OpenAI cache fields on compatible APIs

* fix(cache-probe): preserve Azure cache extensions

* fix(cache-probe): support Azure-style cache probes

* test(cache-probe): type fetch mock correctly

* test(cache-probe): cover supported cache payloads

* fix(cache-probe): support responses compatibility payloads
2026-07-27 09:43:19 +08:00
JATMNandGitHub 0652f0524b refactor(openai-shim): extract tool conversion (#2006)
* test(openai-shim): mark extraction ownership seams

* refactor(openai-shim): extract tool conversion and parsing

* fix(openai-shim): harden extracted tool helpers

* fix(openai-shim): normalize parsed text tool arguments

* fix(openai-shim): require object text tool arguments
2026-07-27 08:59:32 +08:00
a3dc345f12 test(user): restore real modules from a pre-mock snapshot (#2031)
* test(user): restore real modules from a pre-mock snapshot

This suite's teardown re-installed its own mocks instead of undoing them.

`import * as realExeca from 'execa'` is a live namespace binding, and
mock.module repoints it. By the time afterEach ran, `realExeca` WAS the
mock, so `mock.module('execa', () => realExeca)` reinstalled the stub -- and
mock.module lasts for the life of the process, so every test file loaded
afterwards got it.

The stub returns { exitCode, stdout } with no stderr, which is what made it
visible elsewhere: collectTaskReportGitMetadata does
`inside.stderr.trim()` and threw "undefined is not an object". The two
task-report CLI handler tests and the two /ads command tests failed on any
run where this file happened to be ordered before them, which is why the
same four went red on unrelated PRs and intermittently on main itself
(6bef0e16, 0ff1d1cb).

Snapshot each module surface into a plain object at load, before any mock is
installed, and restore through the snapshots. The stub definitions build on
the snapshot too -- a bare `import('execa')` inside the helper resolves to
whatever mock is current, so each stub was being layered on the last.

* chore(test): drop stray VCR fixture from mock-teardown fix

The fixtures/734ad7.json capture was accidentally recorded while running
the SDK suite locally and is unrelated to the mock-teardown repair. It
replays an empty response for the 'test undefined reason' lifecycle path
(hiding regressions) and embeds an environment-dependent agent-listing
reminder. Remove it to keep this PR focused.

* test: harden user mock teardown and stabilize interrupt lifecycle

Use win32 for the analytics platform mock (env.Platform contract) and
include stderr on the async execa stub so a future leak fails soft.

Rewrite the undefined-reason interrupt lifecycle assertion onto the
deterministic queryLoop + stop-hook path so it no longer depends on an
empty VCR fixture or SDK model-startup races after fixture removal.

* test(sdk): drop duplicate stop-hook default-abort lifecycle clone

The rewritten "undefined reason" interrupt test was an exact copy of the
existing Stop-hook default-abort regression in the same file. Keep the
single deterministic coverage path.

---------

Co-authored-by: jatmn <the@jat.mn>
2026-07-26 12:54:45 +08:00
JATMNandGitHub 3f443414c5 refactor(openai-shim): extract message content conversion (#2005)
* refactor(openai-shim): extract message conversion

* test(openai-shim): complete message conversion test extraction

* test(openai-shim): preserve text-only image guard

* test(openai-shim): retain main coverage after extraction

* fix(openai-shim): guard malformed content blocks
2026-07-24 08:54:39 +08:00
0xfandomandGitHub 62d15d40a6 fix(commands): insert slash-command argument text literally, not as regex refs (#1966)
* fix(commands): insert slash-command argument text literally, not as regex refs

substituteArguments filled $ARGUMENTS and named-$foo placeholders by passing
the user's argument string as the replacement operand of String.replace/
replaceAll. When the replacement is a string, JS interprets $-sequences in it
($$, $&, $`, $', $n), so argument text containing those was mangled: `$100$$`
lost a dollar, `$&` re-inserted the matched placeholder, etc. The indexed
($ARGUMENTS[n], $n) paths right beside these already use function replacers and
were unaffected. Switch the two string-replacement sites to function replacers
so the value is inserted verbatim.

* fix(commands): stop later placeholder passes from rewriting inserted values

Each substitution pass wrote into the same content the following passes then
re-scanned, so an argument value that legitimately contained a placeholder token
was substituted twice: with args '"$1" second', a $name (or $ARGUMENTS[0]) whose
value is the literal $1 came back as 'second' instead. Park each substituted
value behind a salted, NUL-delimited slot token and swap the real values back in
after every pass has run, so a value is only ever inserted, never interpreted.

* fix(commands): write the slot-token NUL delimiter as an escape, not a raw byte

The slot template and restoration regexp contained literal 0x00 bytes, which
makes Git classify the whole tracked file as binary — git diff can only say the
file differs and GitHub cannot render or review changes to it. Spell the
delimiter as \x00 escapes; the evaluated strings are unchanged, so the tokens
remain NUL-delimited at runtime while the source stays text.
2026-07-24 08:53:45 +08:00
Xiang HanandGitHub c23b6e1c64 fix(query): keep long-running tools active (#2022)
* fix(query): keep long-running tools active

* test(mcp): cover silent-server heartbeat in activity regression

* fix(mcp): guard heartbeat callbacks and merge partial server progress

* fix(ui): treat waiting_for_task heartbeats as ephemeral progress

* fix(mcp): guard terminal progress callbacks in tool call path

* fix(mcp): guard forwarded progress and reset cache on session retry

* fix(agent): guard forwarded subagent progress and reset cache on elicitation retry

* fix(mcp): clear progress before URL elicitation wait

* fix(mcp): contain started progress callback failures
2026-07-24 07:28:12 +08:00
JATMNandGitHub 01a01fb033 fix(ui): show streaming token count immediately (#2030)
* fix(ui): show streaming token count immediately

* test(ui): cover reduced-motion token override
2026-07-23 10:47:23 +08:00
022f057a3c feat(aimlapi): add passwordless client methods and response-shape guards (2/N) (#2020)
* feat(aimlapi): add passwordless client methods and response-shape guards

* fix(aimlapi): surface malformed auth and key responses as AimlapiApiError

* fix(aimlapi): redact error bodies and tighten checkout and account guards

* fix(aimlapi): redact submitted credentials and complete response guards

* fix(aimlapi): treat any successful non-JSON acknowledgement as delivered

* test(aimlapi): assert the sign-in code request contract

* fix(aimlapi): redact escaped, overlapping and cancelled-request secrets

---------

Co-authored-by: Lookoff123 <bataryshkinairina@gmail.com>
2026-07-23 08:19:34 +08:00
JATMNandGitHub 6bef0e1604 refactor(openai-shim): extract Ollama adapter (#2004) 2026-07-23 07:18:25 +08:00
0xfandomandGitHub 0ff1d1cb7b fix(memory): match nested directories on path boundaries, not name prefixes (#1974)
* fix(memory): match nested directories on path boundaries, not name prefixes

getDirectoriesToProcess documents nestedDirs as "Directories between CWD and
targetPath", but tested containment with currentDir.startsWith(originalCwd).
A sibling whose name merely begins with the CWD's name satisfies that: with cwd
/work/myapp, reading /work/myapp-backend/src/a.ts collected /work/myapp-backend
and its subdirectory, so their CLAUDE.md loaded as Project memory. Renaming the
directory to /work/backend loads nothing — same layout and same permission
grant, different behavior purely because of how the name is spelled.

Route the check through pathInWorkingPath, the helper already used for path
containment elsewhere in this file's module graph, so the comparison happens on
path boundaries.

* test(memory): build nested-dir fixtures with path helpers for Windows

getDirectoriesToProcess resolves its inputs, so on Windows the outputs carry a
drive letter and backslashes; hardcoded POSIX fixture strings would never match
and the walk-to-CWD comparison could not terminate. Construct every fixture and
expectation with resolve/join so they follow the platform.

* fix(memory): use native case semantics for the nested-directory check

pathInWorkingPath case-folds both operands on every platform so that
case-variant spellings cannot slip past a permission check. That is the
wrong direction for memory traversal: on a case-sensitive filesystem
/work/MyApp and /work/myapp are two unrelated projects, and folding them
together made getDirectoriesToProcess('/work/myapp/src/a.ts', '/work/MyApp')
return the /work/myapp ancestors, loading the other project's
CLAUDE.md/AGENTS.md as nested project memory.

Use a local boundary check built on relative(), which keeps the platform's
native case semantics while still comparing on path boundaries rather than
string prefixes.

* fix(memory): compare the relative path on segment boundaries

The containment check used rel.startsWith('..'), which is the same
string-prefix mistake this PR set out to fix: a directory legitimately named
'..hello' yields the relative path '..hello', so a genuinely nested
directory was dropped and its CLAUDE.md never loaded.

Match '..' exactly or followed by a separator instead, and add a regression
for the dotted-name case.

Also skip the case-variant assertion on Windows: path comparison there is
case-insensitive, so /work/MyApp and /work/myapp really are the same
directory and treating them as nested is correct.

* fix(memory): keep directory containment case-faithful on Windows

path.win32.relative() compares components case-insensitively, so it
returns "src" for C:\\work\\MyApp -> C:\\work\\myapp\\src. NTFS supports
per-directory case sensitivity, so those can be distinct project trees,
and the containment check would load the other project's CLAUDE.md and
rules as nested memory for a session rooted at the first.

Rebuild the child from the parent and compare exactly: the boundary logic
relative() provides is kept, the lexical case distinction is restored. The
path implementation is injectable so the Windows semantics are covered on
every host rather than skipped outside Windows.
2026-07-23 07:17:47 +08:00
JATMNandGitHub df85369b60 refactor(openai-shim): extract provider compatibility (#2003)
* test(openai-shim): mark extraction ownership seams

* refactor(openai-shim): share executor planner retry state

* test(openai-shim): add stable extraction seams

* refactor(openai-shim): extract provider compatibility

* fix(openai-shim): honor disabled NIM thinking
2026-07-22 22:14:47 +08:00
SuyunandGitHub 5f6c60851a fix(fs): tolerate EPERM from mkdir on Windows drive roots (#2026)
On Windows, writing a file directly at a drive root (e.g. writing
content to D:\foo via the Write tool) fails with EPERM: dirname('D:\foo')
is 'D:\' itself, and mkdir('D:\', { recursive: true }) always fails —
the kernel cannot create a root that already exists, and libuv maps
that to EPERM rather than EEXIST. The error propagated to the Write
tool as a spurious 'permission denied' on mkdir D:\.

Treat EPERM like EACCES in NodeFsOperations.mkdir/mkdirSync: swallow it
only when the directory already exists. Genuine permission failures
still propagate — the existsSync guard limits the no-op to cases where
there was nothing to create, and a present-but-unwritable directory
still fails at the subsequent file write.

The regression tests use spyOn + mock.restore() rather than
mock.module(): module mocks are process-global and leak across test
files in the same bun process (neither mock.restore() nor
re-registering the real module clears them in bun 1.3.x).
2026-07-22 21:54:50 +08:00
JATMNandGitHub 4bb94d01e4 refactor(openai-shim): extract stream control (#2002)
* test(openai-shim): mark extraction ownership seams

* test(openai-shim): stabilize independent extraction seams

* test(openai-shim): anchor façade extraction boundaries

* refactor(openai-shim): anchor shared tool-call sequence

* test(openai-shim): anchor JSON fallback ownership

* test(openai-shim): anchor provider and message seams

* test(openai-shim): add stream extraction seams

* test(openai-shim): isolate stream normalization ownership

* test(openai-shim): isolate schema ownership seam

* test(openai-shim): stabilize executor extraction seams

* test(openai-shim): isolate split executor seams

* refactor(openai-shim): share executor planner retry state

* refactor(openai-shim): extract stream control

* fix(openai-shim): preserve final SSE frame at EOF

* test(openai-shim): cover extraction seams

* fix(openai-shim): cancel SSE source after done
2026-07-22 08:16:48 +08:00
0xfandomandGitHub a6b3d7a209 fix(bridge): truncate derived session titles on grapheme boundaries (#1982)
* fix(bridge): truncate derived session titles on grapheme boundaries

deriveTitle cut the title with flat.slice(0, TITLE_MAX_LEN - 1), a
UTF-16 code-unit slice. When an emoji or astral-plane character in the
user's first message straddles the cut, the slice keeps its high
surrogate and drops the low one, leaving a lone surrogate. The title is
PATCHed to the claude.ai backend and UTF-8-serialized, so that lone
surrogate is transmitted as the U+FFFD replacement character and the
remote/mobile session list shows mojibake.

Route through truncateToWidth, the grapheme-safe helper deriveSessionTitle
in bridgeMain.ts already uses for the identical purpose.

* test(bridge): drop lookbehind from the lone-surrogate check

The source regex in initReplBridge.ts avoids lookbehinds to stay within
YARR/JSC (the engine Bun uses); mirror that in the test by matching an
unpaired low surrogate with a leading non-high-surrogate alternation
instead of a negative lookbehind.

* fix(bridge): bound the derived title in characters, not display width

TITLE_MAX_LEN caps the session-title API field in characters, but
truncateToWidth measures terminal columns. That charged 2 columns per wide
glyph, so 30 CJK characters — well inside the 50-char field — were cut to
24 plus an ellipsis, while zero-width graphemes cost 0 columns and removed
the cap entirely (100,000 code units passed through as a title).

Walk graphemes and accumulate against the code-unit length instead. That
keeps the surrogate pair and any combining marks intact, which is what the
original raw slice broke, while still enforcing the documented character
bound.
2026-07-22 08:16:00 +08:00
0xfandomandGitHub 722e0c31ce fix(output-style): resolve style names by own-property (#2023)
settings.outputStyle is a free-form z.string() with no enum, and the style
maps are plain object literals, so the name reached a bare index and
resolved inherited Object.prototype members. The trailing '?? null' does not
neutralize that — the Object constructor is not nullish — so the documented
'unknown style falls back to the default' contract was skipped and a
function was handed on as if it were a config.

With outputStyle set to 'constructor' the model's system prompt gained

  # Output Style: Object
  undefined

and the output_style attachment announced 'Object output style is active'.
getSimpleIntroSection also branches on the config being non-null, so it told
the model to follow an output style that does not exist.

Route both lookups through a shared resolveOutputStyle helper gated on
Object.hasOwn.
2026-07-21 05:32:22 +08:00
JATMNandGitHub e3fb051775 feat(kimi): add Kimi K3 context variants (#1989)
* feat(kimi): add K3 context variants

* fix(moonshot): default K3 to 1M

* fix(kimi): restore K3 model name

* fix(kimi): preserve K3 route metadata

* fix(kimi): recommend the selected context variant

* fix(kimi): preserve max shim reasoning effort

* fix(kimi): validate K3 reasoning levels

* fix(kimi): reconcile K3 route metadata

* test(integrations): accept catalog default ids

* fix(kimi): preserve K3 runtime limits and effort aliases

* fix(kimi): preserve override context limits

* fix(kimi): canonicalize K3 reasoning to max

* fix(moonshot): preserve K3 query reasoning

* fix(effort): scope K3 xhigh normalization

* fix(kimi): scope max-only override effort

* docs(kimi): qualify HighSpeed plan claim

* fix(kimi): preserve documented K3 effort levels

* fix(kimi): preserve disabled thinking for K3

* fix(kimi): avoid disabled-thinking fallback

* fix(compact): honor route runtime output limits

* fix(rebase): preserve transport compression routing

* fix(kimi): preserve route defaults and runtime controls

* fix(kimi): retain selected runtime limits for overrides
2026-07-20 22:25:17 +08:00
JATMNandGitHub 8f81e48f0e feat: add LongCat as first-class OpenAI-compatible provider (#1986)
* feat: add LongCat as first-class OpenAI-compatible provider

Register LongCat-2.0 in the integration catalog with LONGCAT_API_KEY auth,
/provider preset support, and zai-compatible thinking controls that emit
thinking:{type} while stripping unverified reasoning_effort fields.

* fix: complete LongCat provider integration

* fix: complete LongCat provider integration

* test: isolate LongCat provider environment

* test: isolate LongCat environment in provider tests

* test: isolate LongCat environment in route tests

* test: isolate LongCat environment in utility tests

* fix: harden LongCat provider integration

* fix: complete LongCat transport support

* fix: keep LongCat requests text-only

* fix: normalize LongCat endpoint URLs

* fix: reject malformed LongCat base URLs

* fix: harden LongCat text-only transport

* fix: scope LongCat transport hardening

* fix: scope generic OpenAI credentials by route

* fix: preserve required provider API formats

* fix: align LongCat with documented tool support

* fix: harden LongCat environment routing

* fix: enable LongCat tool calling

* Revert "fix: enable LongCat tool calling"

This reverts commit d8b1802ed1.
2026-07-20 21:57:09 +08:00
fff83a1a7f feat(onboarding): first-run experience for third-party providers (#1864)
* feat(onboarding): first-run experience for third-party providers

Two gates in showSetupScreens were keyed on usesAnthropicAccountFlow(),
so users of any non-Anthropic provider skipped onboarding entirely:

- Onboarding (theme + security notes) now runs for all providers. The
  component already drops its preflight/OAuth steps when Anthropic auth
  is not enabled, so third-party users get theme -> security notes ->
  terminal setup with no login screens.
- The trust dialog now runs for all providers. Workspace trust is
  orthogonal to the API provider — an untrusted repo is exactly as
  dangerous over a local model as over Anthropic. (The block comment
  even said "always show"; the inner gate contradicted it.)

Also: the login-method screen now detects OPENAI_BASE_URL+OPENAI_MODEL
in the environment and offers "Use current environment configuration"
as the first (default) option. Selecting it saves and activates a
provider profile via addProviderProfile — env vars alone do NOT
activate the OpenAI route (resolveActiveRouteIdFromEnv requires
CLAUDE_CODE_USE_OPENAI or a saved profile), a gap previously masked in
manual testing by a stray legacy .openclaude-profile.json in the cwd.

Verified live (tmux, scratch config dir, mock OpenAI server):
fresh 3P first run walks theme -> security -> trust -> REPL; env
option saves "Local OpenAI-compatible", the session completes a real
turn against the env endpoint, and the profile persists across
relaunch. Second launch shows no onboarding.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(onboarding): address review — testable gating seam + env-profile dedup

- The first-run screen decisions move into src/utils/setupScreenGates.ts,
  a provider-free importable seam (showSetupScreens' import chain cannot
  be loaded under bun test — the same constraint and pattern as the
  dev-channels registration seam). Behavioral tests cover the gate matrix
  (fresh install, completed install, theme-missing re-show, trust
  independence, claubbit skip); the bugfixes.test.ts checks now assert
  the wiring (both dialogs consult the seam, no provider gate at the
  call sites) instead of only regexing for the removed string.
- The "use current environment configuration" onboarding option dedupes:
  an existing profile matching the env base URL + model is re-activated
  via setActiveProviderProfile (which also re-applies profile env and
  syncs the startup profile file) instead of appending a near-identical
  profile on every pass through the flow.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(onboarding): refresh reused profile credentials + accurate env var label

- The reuse branch now refreshes the stored credential from the
  environment before activating: a rotated OPENAI_API_KEY would otherwise
  leave the flow running on the profile's stale key. Falls back to the
  existing key when the env no longer carries one, so a working
  credential is never blanked. Status text says "Activated" for reuse and
  keeps "Saved" for a newly created profile.
- The environment option's label names the variable the value actually
  came from (OPENAI_BASE_URL vs OPENAI_API_BASE) instead of hardcoding
  the former, so troubleshooting points at a variable that is really set.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(onboarding): preserve profile fields and verify the credential refresh

Follow-up on the reuse path added last round:

- updateProviderProfile REPLACES the profile (toProfile builds a fresh
  object rather than merging), so passing only name/baseUrl/model/apiKey
  silently dropped any configured apiFormat, azureStyle, authHeader,
  authScheme, authHeaderValue, customHeaders, or maxContextLength. Spread
  the existing profile and override only the refreshed credential.
- A null return from updateProviderProfile (env values failing profile
  validation) no longer falls through to activation: reporting
  "Activated" while still running on the stale key is worse than routing
  the user to guided setup, which is what the create path already does.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(onboarding): redact credential-bearing endpoints before display

OPENAI_BASE_URL / OPENAI_API_BASE are credential-bearing in the wild
(userinfo like https://user:pass@host/v1, or ?token=/?api_key= query
params), and both the option label and the completion status message
rendered the raw value straight into terminal scrollback.

The derivation moves into src/utils/envProviderOption.ts, which owns the
disclosure boundary explicitly: `displayBaseUrl` is passed through the
codebase's existing redactUrlForDisplay and is the only form the UI may
render, while the raw `baseUrl` is retained for profile creation and
activation so the saved profile still authenticates. Both rendered sites
now use the redacted value.

Regression coverage: envProviderOption.test.ts asserts userinfo and
sensitive query params never reach displayBaseUrl (including via the
non-URL fallback path) while baseUrl stays intact, plus var-name and
availability cases; a wiring guard in bugfixes.test.ts fails if either
rendered site is ever pointed back at the raw endpoint.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-07-20 16:23:14 +08:00
83d54b0ac8 feat(perf): tier1 token optimization — universal tool compression, doom loop detection, configurable compaction (#1869)
* feat(perf): tier1 token optimization — universal tool compression, doom loop detection, configurable compaction

- Extend compressToolHistory to Anthropic-native transports (firstParty/
  bedrock/vertex/native-GitHub), gated to runs where prompt caching is
  inactive: rewriting old messages as they cross tier boundaries diverges
  the request prefix every call, so cached native sessions keep relying on
  the cache-aware microCompact instead. Shim-routed traffic (OpenAI-compatible
  env providers, per-agent providerOverride, Codex) still compresses at its
  own layer, where the local fast-path opt-out applies. compressToolHistory
  is now idempotent (skips its own stub/truncation markers) so layered call
  sites can never re-mangle output.

- Add doom loop detection: blocks after 3 consecutive identical tool calls
  (same name + input signature). State is keyed per agent (main thread and
  each subagent separately) so concurrent subagents neither trip nor reset
  each other's counters. Resets at the start of each agent's query turn.

- Add configurable compactTailTurns in GlobalConfig, wired into autoCompact's
  relevance pruning (default: 3, clamped to positive) and exposed in the
  /config UI next to the other compaction settings.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(perf): address review — tier-aware idempotency, full-input signatures

Post-rebase reconciliation with #1958's compressToolHistory rework (the
guard's extractText call no longer even typechecked against the new
signature) plus the CodeRabbit findings:

- Tier-aware idempotency replaces the blanket already-compressed skip,
  which permanently blocked mid→old upgrades: stubs stay terminal, and a
  truncated block left alone while mid-tier still upgrades to a stub when
  later exchanges age it into the old tier. The upgraded stub's
  omitted-chars count is recovered from the truncation marker (visible
  length + marker's omitted count = exact pre-truncation length), so it
  reports the tool's real output size — asserted equal to what a fresh
  single-pass stub would have produced. Regression tests cover the
  aging upgrade, recovered length, and same-input no-op.
- computeSignature hashes the FULL serialized input (sha-256, fixed-size
  stored signature) instead of comparing a 2KB prefix, which treated
  distinct calls sharing a long prefix (e.g. Write calls differing only
  in trailing content) as identical — a false-positive block on
  legitimate work. Regression test included.
- DEFAULT_COMPACT_TAIL_TURNS shared constant replaces the `3` duplicated
  across autoCompact, pruneByRelevance's default, and the /config UI.
- Doom-loop block path: added a tengu_doom_loop_blocked analytics event
  (false-positive rates become observable for threshold tuning) and the
  nudge now tells the model a deliberate repeat is fine once something
  observable has changed. The blocked yield's message shape mirrors the
  sibling pre-execution error paths, preserving tool_use/tool_result
  pairing.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(perf): normalize compactTailTurns everywhere + native-routing coverage

Second CodeRabbit round:

- normalizeCompactTailTurns (relevancePruning.ts, next to the shared
  default) is now the single rule for the hand-editable config: finite
  values >= 1 floor to integers, everything else falls back to the
  default. The /config UI displays AND persists through it, so the picker
  shows exactly what autoCompact preserves (a hand-edited 2.5 no longer
  displays as 2.5 while running as 2; 0/negatives no longer display as
  selected while running as 3). Also fixes a real edge in the previous
  inline clamp: 0.5 passed the `> 0` check and floored to a ZERO-message
  tail, pruning everything. Unit tests cover the boundary matrix.

- shouldCompressNativeToolHistory extracted from queryModel and exported:
  the request-mutating routing decision is now parameterized-tested
  across all four native transports (first-party, Bedrock, Vertex,
  GitHub-native-Anthropic) x caching on/off, the providerOverride
  exclusion, and non-native providers — queryModel itself needs a live
  client, so the predicate is the honest testable seam.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(perf): guard custom first-party endpoints + strict config coercion

Third CodeRabbit round:

- shouldCompressNativeToolHistory now requires an Anthropic first-party
  base URL before accepting the firstParty provider as native, mirroring
  the exact guard getPromptCachingEnabled uses. Without it, a custom
  ANTHROPIC_BASE_URL (proxy / compatible endpoint) reported firstParty
  with caching disabled and had every request's messages compressed —
  an assumption we cannot make about arbitrary endpoints. Test added for
  the custom-base-URL exclusion.

- normalizeCompactTailTurns only coerces numbers (persisted config) and
  strings (the /config picker channel); other hand-edited shapes no
  longer smuggle a tiny tail through Number() coercion (true → 1,
  [2] → 2) and fall back to the default instead. Boundary tests added.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-07-20 14:26:12 +08:00
cb460e516b feat(codex): add GPT-5.6 family models and fix saved-model rehydration (#2014)
* feat(codex): add GPT-5.6 family models and fix saved-model rehydration

Add the GPT-5.6 family (gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna) to the
Codex OAuth provider: alias map with default reasoning efforts, /model
picker options, display names, and Codex-route context metadata. Bare
`gpt-5.6` resolves to the flagship Sol tier at parse time (Codex CLI
convention) — matched on the base name so a ?reasoning=/?thinking= query
suffix cannot defeat the rewrite — keeping context sizing and display on
real descriptor metadata.

Context windows reconcile with the #1961 direct-OpenAI routing: the
gpt.ts descriptors pin the ~272k effective Codex input cap (issue #1118
precedent; the Codex base URL resolves to a catalog-less route that
reads descriptors), while the openai vendor catalog keeps the true
1.05M window for direct api.openai.com /v1/responses traffic. The
gpt-5.6 alias-default reasoning effort is likewise Codex-transport-only:
OPENAI_API_BASE gateways do not inherit first-party effort metadata
(explicit /effort and ?reasoning= picks still flow everywhere).

Fix startup rehydration for Codex profiles: profileSupportsModel is now
authoritative for Codex-backend profiles — it accepts every Codex alias
and Codex-eligible gpt-5.x free-text pick (shared isCodexEligibleGpt5Model
predicate), so a /model choice (e.g. gpt-5.6-terra) survives restart
instead of silently reverting to codexplan/gpt-5.5. A trailing [1m] tag
is normalized off before matching, so tagged picks stick too. Foreign
leftovers (kimi-k2.6) and API-only tiers the backend does not serve
(gpt-5-mini/-nano) still fall back to the profile default instead of
400ing.

Also: generalize the picker's custom-model recovery to keep curated
labels for all Codex models across provider switches ([1m]-tolerant,
single lookup), and add GPT-5.6 cases to the display-name maps.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(codex): address CodeRabbit review — [1m] tag parsing + coverage

- parseModelDescriptor now strips a trailing [1m] context tag (whole-string
  or base-id position) before parsing: the tag is a client-side 1M opt-in,
  never a wire model id, so tagged aliases keep their mapping and effort
  defaults and resolvedModel never leaks the bracket suffix to the backend
  (pre-existing gap for e.g. gpt-5.5[1m], now fixed at the parse layer).
- The bare-gpt-5.6 rewrite keeps a [1m] tag TRAILING after a preserved
  query (gpt-5.6?reasoning=medium[1m] → gpt-5.6-sol?reasoning=medium[1m]);
  the previously emitted tag-before-query form broke the request-time
  base-model split. End-to-end regression tests cover parse + request.
- New coverage per review: alias effort defaults are asserted suppressed on
  a custom OpenAI-compatible gateway (non-Codex transport) while explicit
  ?reasoning= overrides flow; picker-recovery tests assert a persisted
  gpt-5.6-terra[1m] under a non-Codex provider keeps its exact tagged value
  with the curated label/description instead of a "Custom model" entry.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-07-20 12:47:53 +08:00
ca7a7e0791 feat(install): enforce and guard the zero-warning npm install contract (#2019)
* feat(install): enforce and guard the zero-warning npm install contract

`npm install -g @gitlawb/openclaude` is verified zero-warning today, but
nothing kept it that way: the runtime deps were caret ranges resolved
fresh on every user install (the published tarball ships no lockfile),
no CI step ever installed the package, and registry-side drift (a
transitive dep deprecated after we ship) is invisible to file-based CI.

Static contract (fast, offline, every PR via `bun run build`):
- Pin the 3 runtime deps to exact versions so the verified resolution IS
  the shipped resolution.
- New validators in scripts/externalsValidation.ts (unit-tested):
  dependencies must equal RUNTIME_DEPENDENCY_CONTRACT exactly (no ranges,
  no unreviewed additions), no consumer-run install hooks or funding
  field, engines.node pinned. Wired into validate-externals.ts.

Runtime verification (scripts/verify-clean-install.ts, `install:verify`):
- Tarball mode (release gate) and published mode (registry watch), each
  running cold-install and upgrade-over-previous scenarios in throwaway
  prefixes with a cold cache and normalized env/flags.
- Strict output whitelist (summary lines only) with network failures
  retried and reported as infra (exit 2), never as a hygiene verdict.
- Structural authority over the installed tree: any package declaring
  install scripts fails, the installed manifest must match the static
  contract, tarball payload/size asserted.
- Boot must be silent: --version prints the exact packed version;
  --help (which, unlike the --version zero-import fast path, loads the
  real bundle) must exit 0 with empty stderr.

CI: release publishes only after the verify passes on Node 22 (npm 10,
the supported floor — warning phrasing and EBADENGINE behavior differ
from npm 11) and Node 24, plus a final gate on the publishing machine
replacing `npm pack --dry-run`. A daily install-hygiene workflow
re-verifies the published @latest on {ubuntu, macos, windows} x
{Node 22, 24} — the only defense against post-release registry drift,
and the OS matrix covers the per-platform @vscode/ripgrep packages.

Found-by-the-guard fix: a fresh machine printed "Warning: ignoring saved
provider profile. OPENGATEWAY_API_KEY is required..." on every command
(even --help) because the injected fresh-install Opengateway default
fails validation without a key (#1651 chose ignore+warn). The default
env is still ignored, but the warning now only fires for genuinely
persisted profiles; published 0.24.0 carries the old noise, so the
verify script exempts exactly that version until the next release.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(install): address CodeRabbit review on the install-hygiene guard

- release.yml: pin install-verify to least-privilege `contents: read` and
  disable credential persistence on its checkout; same persist-credentials
  hardening on the install-hygiene cron checkout.
- verify-clean-install: previousPublishedVersion now follows the same
  retry/infra discipline as installWithRetry — transient registry failures
  retry and then exit 2 (infra) instead of silently skipping the
  upgrade-scenario coverage; a clean not-published answer still skips.
- providerProfile: the fresh-install warning suppression now keys on
  explicit provenance (persisted profile resolved once in
  applyStartupEnvFromProfile) instead of sniffing the
  DEFAULT_STARTUP_PROVIDER_ENV_VAR marker, which a persisted profile's
  env can inherit from a parent CLI process; regression test covers the
  marker-collision case.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* test(install): cover previousPublishedVersion retry/skip/infra branches

CodeRabbit follow-up: the branches deciding whether the upgrade-install
scenario runs, skips, or aborts as infra were untested. Extract the loop
as resolvePreviousPublishedVersion with injected effects (runView,
onRetry, onInfraFailure) per the repo's dependency-injection testing
convention, guard main() behind import.meta.main so the test import does
not launch a real verification, and add regression tests: first-try
success, transient-infra retry then success, clean E404 → null skip
without retries, persistent infra → onInfraFailure (exit 2 in the real
wiring), and unparseable version output → null.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-07-20 12:24:55 +08:00
7674d4d73e feat(aimlapi): provider foundation (1/5) — config, catalog, ambient-key gate (#1995)
* feat(aimlapi): provider foundation — config, catalog, ambient-key gate

First layer of the AI/ML API onboarding split. Self-contained: config
endpoints + partner-header resolution, gateway catalog entry, runtime
metadata and artifact-generator wiring, and the regenerated integration
manifest.

Also lands the P1 security fix in providerProfiles: an ambient AIMLAPI
key is only forwarded when the profile targets the canonical inference
endpoint, so a proxy/staging profile can never leak the key elsewhere.

No client/checkout/UI changes here — those stack in later PRs.

* fix(aimlapi): harden credential gating and restore renamed-preset test path

* fix(aimlapi): harden credential gating and finish renamed-preset path

* fix(aimlapi): finish aimlapi.com rename in provider UI; gate generic proxy credential

* fix(aimlapi): close remaining proxy credential and attribution leaks

* fix(aimlapi): withhold ambient credentials and attribution from proxies

* fix(aimlapi): withhold ambient credentials and attribution from proxies

* fix(aimlapi): stop forcing profile credentials onto retargeted endpoints

* fix(aimlapi): withhold ambient custom headers from proxy launches

---------

Co-authored-by: Lookoff123 <bataryshkinairina@gmail.com>
2026-07-20 10:02:00 +08:00
5399a11d3c fix(openai): compress tool history on Responses requests (#1958)
* fix(openai): compress tool history on Responses requests

* fix(openai): tighten transport preprocessing

* fix(openai): preserve structured compressed tool results

* fix(openai): tier parallel tool results independently

* fix(openai): preserve Ollama image endpoint fallback

* fix(openai): omit old inline image payloads

* fix(openai): bound inline image history

* fix(openai): preserve local image fallback

* fix(openai): bound data URL tool images

* fix(openai): match parameterized image data URLs

* fix(openai): bound adjacent tool images

* fix(openai): bound leading tool images

* fix(openai): size structured history per transport

* fix(openai): scope attached image compression

* fix(codex): match tool text serialization budget

* fix(openai): recompress GitHub Responses fallback

* fix(openai): preserve user images in history compression

* fix(openai): scope history image omission to result ownership

* fix(messages): preserve image owners after media retry

* fix(messages): retain image ownership through normalization

---------

Co-authored-by: jatmn <the@jat.mn>
2026-07-20 09:53:20 +08:00
JATMNandGitHub 1ddb7d6839 test: isolate PowerShell governance policy settings (#2000) 2026-07-19 09:24:45 +08:00
JATMNandGitHub 630fc2d980 fix(compaction): honor disabled auto-compact under memory pressure (#1999)
* fix(compaction): honor disabled auto-compact under memory pressure

* fix(compaction): preserve overflow recovery

* test(compaction): cover disabled config setting
2026-07-19 09:24:31 +08:00
Xiang HanandGitHub 86fb6db85c fix(session-title): ignore API error responses (#1992) 2026-07-19 09:23:58 +08:00
3808d19da4 fix(api): enforce API_TIMEOUT_MS for OpenAI-compatible headers (#1940)
* fix(api): enforce API_TIMEOUT_MS for OpenAI-compatible headers

* test(api): cover Copilot responses fallback deadlines

* fix(api): redact secrets in timeout URL paths

* fix(api): harden Copilot response deadlines

* fix(api): prevent header-timeout request replay

* fix(api): harden timeout cleanup and redaction

* fix(api): redact encoded transport credentials

* fix(api): harden deadline retries and URL redaction

* fix(api): preserve aborted fetch reasons

* fix(api): preserve caller abort reasons

* test(api): clear caller abort timer

* docs(api): clarify API_TIMEOUT_MS transport scope

* docs(api): explain timeout env loading

* fix(api): reset deadline for proxy retries

* fix(api): type deadline fetch adapter

* fix(api): honor abort cleanup and request signals

* fix(api): do not block proxy retries on body cancellation

---------

Co-authored-by: jatmn <the@jat.mn>
2026-07-19 09:21:14 +08:00
eb72c770c4 fix(repl): add correction context after interruption (#1936)
* fix(repl): add correction context after interruption

* test(repl): exercise interruption correction lifecycle

* refactor(repl): bind correction tracker to query guard

* fix(repl): retain initialized correction tracker

* fix(repl): track pre-query interruption corrections

* fix(repl): exclude remote turns from correction context

* fix(repl): harden correction reminder lifecycle

* fix(repl): keep correction reminder request-scoped

* test(query): cover absent request-only context

* fix(repl): clear correction context on compaction

* fix(repl): preserve interruption correction recovery

* fix(repl): clear stale correction state on boundary removal

* fix(messages): recover merged attachment retries

* fix(messages): recover attachment retries safely

* fix(messages): recover ambiguous attachment retries

* fix(repl): clear stale correction reminders

* fix(query): clear correction context after reactive compaction

* fix(repl): declare restore callback dependencies

* fix(repl): preserve correction state across rewrites

* fix(repl): scope correction tracking to model calls

* test(repl): remove structural lifecycle assertions

* fix(repl): preserve interruption correction reminders

* fix(repl): retain reminder before queued dispatch

* fix(repl): retain correction reminder until model dispatch

* fix(repl): preserve reminders for queued corrections

* fix(repl): arm correction only at provider dispatch

* fix(repl): scope interruption correction to model requests

* fix(repl): retain reminders until model dispatch

* fix(repl): ignore queued slash commands on interrupt

* fix(messages): strip rejected tool-result media

* fix(messages): apply nested media retry stripping

* fix(messages): preserve empty tool results after media retry

* fix(repl): preserve correction and media retries

* fix(repl): retain correction through tool execution

---------

Co-authored-by: jatmn <the@jat.mn>
2026-07-18 22:48:00 +08:00
de76950f60 feat(provider): route GPT-5.6 models to the OpenAI Responses API (#1961)
* feat(provider): route GPT-5.6 models to the OpenAI Responses API

GPT-5.4/5.5/5.6 (incl. gpt-5.6-sol/terra/luna) reject function tools +
reasoning_effort on /v1/chat/completions, so an agent CLI (which always
sends tools) can't use them. Add a model+base predicate
(modelRequiresResponsesApi) that auto-selects the existing /v1/responses
transport for these models on api.openai.com and Azure OpenAI hosts.
Precedence: explicit responses/responses_compat > catalog
requiredApiFormat > explicit chat_completions > predicate > default. The
gpt-5.6 catalog entries deliberately set no requiredApiFormat so the
chat_completions escape hatch works for them. Register the gpt-5.6
descriptors and openai-vendor catalog entries (with reasoning metadata so
buildResponsesBody emits nested reasoning.effort).

Also fix a latent bug: the responses branch of buildRequestUrl emitted a
bare ${base}/responses and skipped Azure handling, so a forced/auto
responses route 404'd on Azure. It now mirrors buildChatCompletionsUrl —
deployment-style bases get the deployment path + api-version, bases
already containing /deployments/ keep their path and gain api-version,
while the modern Azure v1 surface (.../openai/v1) is preserved as
${base}/responses.

* fix(provider): honor OPENAI_AZURE_STYLE in the responses gate and use the Azure v1 responses surface

CodeRabbit review on #1961: the responses auto-route gate only checked
hostnames, ignoring the OPENAI_AZURE_STYLE override the shim honors for
custom/private Azure endpoints (APIM-fronted, private link). The Azure
detection is now a single shared predicate (isAzureStyleBaseUrl) used by
both the gate and the shim: OPENAI_AZURE_STYLE truthiness first, then
hostname matching.

Per Microsoft's docs, the Responses API exists only on the Azure v1
surface ({resource}/openai/v1/responses, model in the request body, no
api-version, no deployment-scoped form), so buildResponsesUrl now
normalizes any Azure-style base to that surface instead of mirroring the
chat builder's deployment-path + api-version form, which built endpoints
that do not exist.
https://learn.microsoft.com/en-us/azure/foundry/openai/how-to/responses

* fix(provider): address maintainer review on GPT-5.6 responses routing

Narrows the responses auto-route to verified variants (gpt-5.4/5.5/5.6
minus -mini/-nano, two-digit minors deliberately unmatched), corrects the
gpt-5.6 context window to 1,050,000 per the OpenAI model pages, documents
OPENAI_AZURE_STYLE's routing effect in .env.example, and hardens
buildResponsesUrl normalization (trailing-slash strip, stacked Azure
suffixes stripped until stable). Also pins --max-concurrency=1 on the
default test script and adds direct coverage for isAzureStyleBaseUrl, the
override-driven responses URL, and the gpt-5.6 catalog metadata.

* test(provider): pin responses predicate behavior for patch and suffixed ids

Pins gpt-5.4.1 (patch of a verified family, routed), gpt-5.41 (two-digit
minor read, not routed), and gpt-5.6-mini-high (mini variant, not routed)
so the predicate's edge behavior is asserted rather than implied.

* fix(provider): responses-contract test, regional OpenAI hosts, Azure deployment docs, env allowlist

Updates the providerOverride gpt-5.4 effort test to the Responses contract
the auto-route now sends (nested reasoning.effort, /responses URL); widens
the auto-route host check to OpenAI-controlled *.api.openai.com regional
endpoints; documents and regression-tests the explicit
OPENAI_API_FORMAT=responses path for arbitrary Azure deployment names; and
allows OPENAI_AZURE_STYLE through the --provider-env-file allowlist.

* fix(provider): narrow responses auto-route to verified minors and carry GPT-5.6 reasoning metadata on Azure

Narrows the model-name auto-route predicate from gpt-5.[4-9] to gpt-5.[4-6]
so unverified future minors (5.7/5.8/5.9) are not auto-routed, and syncs the
comment plus the two remaining "5.4+" phrasings in .env.example.

Fixes GPT-5.6 reasoning metadata on Azure and regional OpenAI bases: those
hosts resolve to route 'custom' (empty catalog), so resolveCatalogReasoningMetadata
returned undefined and the request dropped its default 'high' effort and the
reasoning.encrypted_content include. It now falls back to the openai vendor
catalog by model name on route 'custom', so gpt-5.6 carries its advertised
default 'high' and xhigh instead of incidental legacy controls.

* fix(provider): gate the custom-route reasoning fallback to verified OpenAI/Azure bases

The round-3 custom-route fallback also fired for arbitrary OpenAI-compatible
gateways (which resolve to route 'custom' too), injecting a default
reasoning_effort:high on a chat_completions request those gateways may reject
— a behavior change on third-party gateways the PR promised not to make.
Gate the fallback on baseUrlSupportsResponsesAutoRoute (the same verified
OpenAI/Azure surfaces the Responses auto-route uses), threading the request
base via the reasoning context (process.env fallback for the upstream path).

* test(provider): isolate OPENAI_API_BASE/OPENAI_AZURE_STYLE in the gpt-5.6 reasoning tests

The Azure/regional/gateway reasoning tests snapshot-restored only
CLAUDE_CODE_USE_OPENAI/OPENAI_BASE_URL/OPENAI_API_KEY. A leaked
OPENAI_AZURE_STYLE from another test would make isAzureStyleBaseUrl treat
the gateway base as Azure-style, firing the fallback and flipping the
'no injected default' assertion. Snapshot both keys and delete them before
each test's setup so a leaked value cannot corrupt the result.

* fix: preserve GPT-5.6 fallback and Azure routing

* fix: cover GPT-5.6 Azure edge cases

* fix: cover GPT-5 forced chat tools

* fix(provider): narrow Azure-style responses routing

* fix(provider): isolate agent overrides from Azure mode

* fix(provider): isolate override reasoning from Azure mode

* fix(provider): isolate override API format

* fix(provider): preserve responses effort routing

* fix(provider): restore safe context and clear Azure mode

* fix(provider): preserve Azure routing state

* fix(provider): preserve Azure profile routing

* fix(provider): preserve automatic Responses routing in profiles

---------

Co-authored-by: jatmn <the@jat.mn>
2026-07-18 22:47:24 +08:00
0effa0f42b test: seal mock.module leaks in awaySummary and diff smoke tests (#1981)
* test: restore compact test module stubs

* test: restore remaining compact mock modules

* test: isolate auto compact from compact mocks

* test: seal mock.module leaks in awaySummary and diff smoke tests

awaySummary.test.ts stubbed ./api/claude.js and
./SessionMemory/sessionMemoryUtils.js, and diff.test.ts stubbed
src/services/analytics/index.js, each with an incomplete module that was
never restored. bun evaluates every test file's module-level imports up front
and shares one process across the whole suite, so these stubs leaked into all
downstream importers (claude.js: ~22, analytics/index.js: ~245), producing
order-dependent failures in the smoke run.

Register the stubs in beforeAll (not at module load) and unmock() in
afterAll, and acquire sharedMutationLock so the real modules are already
cached for every other file at startup. Verified: a downstream importer now
sees the real queryModelWithStreaming / logEventAsync, and the full suite
failure count drops 73 -> 70.

Co-Authored-By: Claude <noreply@anthropic.com>

* test: restore module mocks under shared lock

* test: guard mock restoration against lock races

* test: retain lock ownership through compact cleanup

* test: retain lock ownership through auto-compact cleanup

* test: restore mocks after setup failures

* test: isolate compact task output cleanup

* test: preserve full compact mock restoration

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-17 12:37:39 +08:00
JATMNandGitHub 47123b4b38 test: restore global mock isolation (#1980) 2026-07-17 00:07:56 +08:00
JATMNandGitHub 2516eef039 test: restore compact mock isolation (#1979)
* test: restore compact test module stubs

* test: restore remaining compact mock modules

* test: isolate auto compact from compact mocks
2026-07-17 00:07:25 +08:00
JATMNandGitHub 7ad96e917e test: isolate flaky smoke assertions (#1978) 2026-07-16 10:17:17 +08:00
BogdanandGitHub 507ba4b804 perf(tools): preserve UTF-8-safe head and tail in persisted previews (#1960)
* perf(tools): preserve UTF-8-safe head and tail in persisted previews

* fix(tools): derive persisted preview size from file

* fix(tools): report exact persisted preview bytes

* fix(tools): bound and sanitize persisted previews

* fix(tools): reconcile sanitized preview metadata
2026-07-16 08:19:41 +08:00
JATMNandGitHub 487cae7185 fix(api): self-heal tool_stream rejection from non-Z.AI gateways (#1950) (#1951)
* fix(api): self-heal `tool_stream` rejection from non-Z.AI gateways (#1950)

GLM-5.2 served through NVIDIA NIM (`integrate.api.nvidia.com`) is
rejected with `400 Unsupported parameter(s): tool_stream` because
`tool_stream` is a Z.AI-proprietary streaming extension.

Changes:
- Add a `tool_stream_unsupported` OpenAI-compatibility failure category
  that detects the `tool_stream` rejection (the existing
  `tool_call_incompatible` matcher only matches `tool_call`, not
  `tool_stream`).
- Self-heal in the OpenAI shim: when a gateway rejects `tool_stream`, drop
  only that parameter and retry with tools intact (streaming tool calls
  simply aren't streamed on such gateways). This is a defensive net for any
  provider that slips the parameter through, alongside the existing
  catalog/runtime gating that suppresses it for non-Z.AI routes.
- Give remote requests one self-heal attempt budget so the retry can run.
- Surface a friendly assistant message for the new category.

Tests:
- Regression test: NVIDIA NIM GLM streaming+tools never sends `tool_stream`.
- Regression test: shim self-heals a `tool_stream` 400 by retrying without it.
- Classification tests for `tool_stream_unsupported`.
- Runtime metadata regression test for NVIDIA NIM GLM-5.2 (no `tool_stream`,
  reasoning shim preserved).

* fix(shim): scope tool stream retry budget

* fix(api): clarify tool stream fallback error

* fix(api): prioritize tool stream rejection

* fix(api): match unrecognized tool_stream arguments

* fix(api): harden tool stream self-heal

* fix(api): accept quoted tool stream errors

* fix(api): match tool stream parameter variants

* fix(api): recognize structured tool stream errors

* fix(api): avoid tool stream name false positives

* fix(api): classify gateway tool stream rejections

* fix(api): exclude tool name validation errors

* fix(api): handle structured tool stream validation

* fix(api): harden tool stream error classification

* fix(api): harden tool stream error classification

* fix(api): narrow tool stream recovery classifier

* fix(api): support FastAPI tool stream errors

* fix(api): accept wrapped tool stream errors

* fix(api): harden tool stream recovery classifier

* fix(api): harden tool stream recovery classifier

* fix(api): distinguish tool stream schema diagnostics

* fix(api): handle structured tool stream errors

* fix(api): avoid tool stream schema false positives

* test(api): isolate watchdog client mock

* test(api): restore watchdog client mock

* test(api): reset watchdog module mock

* test(api): scope watchdog client stub
2026-07-16 08:18:33 +08:00
BogdanandGitHub 1f20e92c2e fix(permissions): enforce read-only plan mode (#1938)
* fix(permissions): enforce read-only plan mode

* fix(permissions): narrow hook approval types

* fix(permissions): harden plan-mode decision boundaries

* fix(permissions): close plan-mode hook races

* test(permissions): clarify platform path case coverage

* fix(permissions): guard permission hook rewrites

* fix(permissions): close remaining plan-mode escapes

* test(speculation): satisfy strict context typing

* fix(permissions): close hook update race windows

* fix(permissions): close plan approval races

* fix(permissions): align prompt decision type

* fix(permissions): close plan transition races
2026-07-16 08:15:43 +08:00
JATMNandGitHub 46e80568be fix(provider): support custom Anthropic bearer auth (#1929)
* fix(provider): support custom Anthropic bearer auth

* feat(provider): add custom Anthropic profile flow

* fix(provider): restore custom Anthropic tokens on startup

* fix(provider): preserve custom Anthropic env setup

* fix(provider): clear stale custom Anthropic tokens

* fix(provider): preserve custom Anthropic API-key env setup

* fix(provider): cover custom Anthropic auth routing

* test(api): isolate custom Anthropic client routing

* test(api): cache-bust client provider imports

* feat(provider): clarify custom provider presets

* fix(provider): address custom Anthropic review feedback

* fix(provider): preserve custom Anthropic headers

* fix(provider): require custom Anthropic token

* fix(provider): isolate custom Anthropic credentials

* fix(provider): guard custom Anthropic setup

* fix(provider): complete custom Anthropic integration

* fix(provider): classify custom Anthropic proxies

* fix(provider): gate proxy cache extensions

* fix(provider): preserve custom Anthropic isolation

* fix(provider): retain direct proxy model option

* fix(provider): honor custom endpoint boundaries

* fix(provider): keep proxy credentials local

* fix(provider): disable proxy fast mode

* fix(provider): preserve first-party route identity

* fix(provider): isolate custom Anthropic endpoints

* fix(provider): gate remaining first-party features

* fix(provider): isolate custom Anthropic proxy features

* test(web-search): make Brave timeout mock abort-aware

* fix(provider): address custom Anthropic review feedback

* test(provider): cover first-party beta gates

* fix(provider): complete custom Anthropic isolation

* fix(provider): complete custom Anthropic routing

* fix(provider): address custom Anthropic review followups

* fix(provider): close custom Anthropic review gaps

* test(provider): keep custom Anthropic mock helpers isolated

* test(provider): isolate model options gateway mocks

* fix(provider): stabilize custom Anthropic model option display

* fix(provider): address remaining review threads

* fix(provider): synchronize active profile persistence

* fix(provider): preserve custom Anthropic API key auth

* fix(provider): avoid forwarding inherited Anthropic keys

* fix(provider): guard custom auth selection

* fix(provider): require first-party Anthropic port

* test(web-search): avoid duplicate shared lock

* fix(provider): resolve remaining review findings

* fix(provider): harden custom Anthropic routing

* fix(provider): simplify Anthropic thinking gate

* fix(provider): preserve custom proxy routing and secret permissions

* fix(mcp): isolate Claude.ai config cache by provider

* fix(model): keep custom endpoints out of first-party UX

* fix(provider): scope Opus off switch to Anthropic

* fix(provider): disable tool search for custom proxies

* fix(provider): close custom Anthropic review gaps

* fix(provider): reject Anthropic staging custom profiles

* fix(webfetch): classify custom Anthropic endpoints

* fix(provider): block bearer auth at Anthropic origin

* fix(provider): keep custom auth off staging OAuth

* test(provider): strengthen auth regression coverage
2026-07-16 08:14:22 +08:00
626c4873ab feat(statusline): show token counts in context bar (ctx 74K/200K (37%)) (#1967)
* feat(statusline): show token counts in context bar (ctx 74K/200K (37%))

Instead of just percentage, show actual token usage and context window
size with integer K/M prefixes (uppercase, en-US locale).

- BuiltinStatusLine: added contextInputTokens + contextWindow fields
- buildBuiltinStatusSegments: formats "ctx {used}/{window} ({pct}%)"
- format.ts: added formatTokenCount() — integer, uppercase K/M
- All 18 tests pass

* fix(statusline): call getCurrentUsage once and reuse result

* fix(statusline): mark estimated token counts with ~ prefix

When getCurrentUsage() returns is_estimated:true (provider reported
all-zero usage), show "ctx ~74K/200K (37%)" instead of "ctx 74K/200K
(37%)" so built-in statusline preserves the estimate distinction
exposed by the custom-statusline contract.

* fix(openai-shim): send stream_options for non-Ollama local providers

Previously stream_options was disabled for all local URLs (127.0.0.1,
192.168.x.x, etc.), preventing llama-server and other self-hosted
OpenAI-compatible servers from returning usage in SSE streams. Now only
Ollama (localhost:11434) is excluded, since it rejects stream_options.
All other providers including llama-server receive stream_options and
their prompt_tokens/completion_tokens are correctly mapped via
buildAnthropicUsageFromRawUsage.

---------

Co-authored-by: Andrey Bezborodov <andrey@getdataflow.ru>
2026-07-15 23:58:44 +08:00
a32781537f fix(query): bound per-turn latency growth in long REPL sessions (#1949) (#1952)
* fix(query): bound per-turn latency growth in long REPL sessions (#1949)

Addresses the progressive latency regression where consecutive prompts in a
single session grow non-linearly (2nd prompt ~10s, 3rd 10+ min) due to
unbounded message accumulation with no proactive compaction and no per-prompt
turn cap on the main thread.

- Cap the interactive REPL main thread at 50 turns per prompt (DEFAULT_REPL_MAX_TURNS).
  Headless/print mode and the SDK are unchanged (--max-turns flag / SDK callers
  still control it), preserving the SDK API contract.
- Default maxMessagesCompactionThreshold to '200' so message-count compaction
  runs well before the context window fills, instead of 'off'.
- Lower the auto-compact threshold buffer from 13k -> 30k so compaction fires
  earlier with less accumulated history. The effective-context floor buffer is
  kept at 13k and getAutoCompactThreshold() falls back to it for small-context
  models, so the threshold can never go negative (no #635 regression).

Test updates: isolate the hard-cap override test from the new 200-message
default, and correct an outdated constant reference in the autoCompact test.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(query): repair REPL latency guard

* fix(query): cover resume and default guard paths

* fix(query): enforce cap across interactive paths

* docs(compaction): clarify disabled message limits

* fix(query): retain explicit message thresholds

* fix(query): enforce explicit threshold recovery

* fix(query): honor legacy active-message limit

* fix(doctor): report effective message compaction limit

* fix(config): share message threshold validation

* test(doctor): cover disabled message compaction

* fix(compact): preserve latency guard coverage

* test(repl): exercise turn cap defaults

* fix(compact): honor disabled default message guard

* fix(swarm): honor disabled auto compaction

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-15 23:06:53 +08:00
0xfandomandGitHub 1d053b2a3d fix(resume): read the session tag from its own entry, not a tool's tag input (#1975)
readLiteMetadata scanned the raw 64KB tail for "tag":"..." without scoping to
the {"type":"tag"} entry, so any nested occurrence matched too. A tool_use input
carrying a tag parameter (Docker image tags, git tags, cloud resource tags) is
stored as literal JSON inside an assistant entry appended after the tag entry,
and last-occurrence wins — so the tool's value was reported as the session tag.
In /resume that surfaces a phantom tag tab and misfiles the session away from
its real one; an untagged session could also acquire a tag it never had.

The other two tag readers already type-scope for exactly this reason
(listSessionsImpl.ts:132, sessionStorage.ts:782, both with comments naming the
tool_use collision), and sessionBranch in this same function is scoped via
SESSION_BRANCH_ENTRY_PREFIX. This reader just missed it; mirror them.
2026-07-15 22:57:25 +08:00