Commit graph

1522 commits

Author SHA1 Message Date
Aditya Vikram Singh
912281e9b6 fix(models): merge models report rows by canonical id, not display name
Maintainer review on #1053: display-name keys collide for distinct
SKUs (GPT-5 / GPT-5.3 Codex / Kimi K2 Thinking / Opus 4.6). Key on
the alias-resolved canonical id, keep the first raw id, and partial-sum
Codex credits when a merge mixes rated and unrated buckets.
2026-08-21 17:26:40 +05:30
Aditya Vikram Singh
9e361175bb fix(models): keep merged baseline empty after a conflict
Extra High on #1053: three raw ids sharing a provider + display
name cleared then refilled savingsBaselineModel because empty
meant both "none seen" and "conflict". Track distinct baselines
in a Set and emit one only when a single non-empty value remains.
2026-08-20 03:08:29 +05:30
Aditya Vikram Singh
28be2472ce fix(models): resolve and merge raw ids in models report
codeburn models asked each provider for a label and bucketed by raw
id, so gpt-5.6-* and Fireworks path-form ids stayed raw and two
ids that share a display name became two rows.

Keep provider-first labels. Fall back to the global short-name
table on a local miss, then merge by provider + display name.
2026-08-20 02:25:50 +05:30
Resham Joshi
7862aabd47
Merge pull request #1039 from avs-io/fix/hermes-glm53-pr-links
fix(hermes): unwrap known routers; fail closed on unknown models
2026-08-19 12:29:21 -07:00
Resham Joshi
496b3f258e
Merge pull request #1041 from avs-io/fix/plan-budget-not-live-quota
fix(plans): call sticker-price bars a budget, not live quota
2026-08-19 12:27:34 -07:00
iamtoruk
fec3519ff1 Merge remote-tracking branch 'origin/main' into pr1041-rebase
# Conflicts:
#	CHANGELOG.md
2026-08-19 12:08:25 -07:00
iamtoruk
02059c961c Merge remote-tracking branch 'origin/main' into pr1039-rebase
# Conflicts:
#	src/models.ts
2026-08-19 12:07:02 -07:00
Resham Joshi
96fc940798
Merge pull request #1037 from avs-io/fix/menubar-keychain-credential-cache
fix(menubar): migrate Claude/Codex caches to namespaced Keychain
2026-08-19 12:05:47 -07:00
Resham Joshi
fe94377a13
Merge pull request #1046 from getagentseal/ci/mac-menubar-swift-tests
ci: run the macOS menubar Swift tests on every mac/ change
2026-08-19 12:05:34 -07:00
Resham Joshi
cab937c2c7
Merge pull request #1038 from avs-io/fix/tui-resize-coalesce
fix(tui): coalesce resize bursts without dropping updates
2026-08-19 12:05:03 -07:00
Resham Joshi
0290c3ee49
Merge pull request #1048 from getagentseal/fix/mimo-alias-landing
fix(models): alias bare MiMo 2.5 ids to the LiteLLM Xiaomi rows (lands #1042 + review fixes)
2026-08-19 12:04:40 -07:00
iamtoruk
dba9a45825 test(mac): stop the concurrent-timeout test starving the pool it waits on
The test blocked on a DispatchSemaphore with a 15s deadline, commented as
running "on a real thread, not the cooperative pool". Swift Testing invokes
synchronous test bodies from a task on the cooperative pool, so the wait
parked one of activeProcessorCount workers on the very task group it was
waiting for. With 16 cores locally there is slack; on the 3-core macos-latest
runner, alongside the rest of the parallel suite, the group made no progress
at all and the wait expired.

Await the group directly instead, which also lets the compiler reject the
blocking wait (unavailable from async contexts), and bound the test with
.timeLimit rather than a hand-rolled wall clock. Assert each child came back
with a signal status, so the test now proves the timeout killed every hung
process instead of only that the group returned.

Reproduced by parking all but 3 cooperative threads for the run: 3/3 failures
at 15.0s before, 3/3 passes after. 10x full suite under CPU load: 160/160.
2026-08-19 11:58:39 -07:00
iamtoruk
0dff0b66d5 docs(menubar): state what the Keychain move actually guarantees
The Codex settings copy implied the cached credential was app-private. It is
a normal login-Keychain item: reachable by programs running as you, with no
per-app ACL. The real win is that it is no longer a world-readable 0644 file,
so say that instead.

Also documents why readAfterSecuringPermissions repairs permissions before
validating content (validating first would read the secret while it is still
world-readable, which is the window the function exists to close), and why the
Keychain service names are deliberately not derived from CFBundleIdentifier
(the Electron app hardcodes the same strings).

Adds the #1037 changelog entry.
2026-08-19 11:43:44 -07:00
iamtoruk
0f7bfb3eb2 fix(menubar): make a failed disconnect leave a consistent state
disconnect() cleared the usage block before anyone knew whether the delete
had worked, and AppStore then returned early on failure — so a failed
disconnect cleared some state, left the rest, and still posted
subscriptionDisconnected. It also carried a second !isSuccess branch that the
early return had already made unreachable.

Both services now return the delete result and only clear the usage block on
success, so a failure changes nothing at all: the provider stays connected,
Disconnect stays available, and the banner asks for a retry. That matches the
success path's ordering instead of half-applying it.

Errors reaching the generic catches now render localizedDescription rather
than String(describing:), so a Keychain failure shows its message instead of
an enum dump with the raw item name in it.
2026-08-19 11:43:44 -07:00
iamtoruk
0fd8419bfd fix(menubar): keep the newer credential copy and drop the sticky retry flag
Three fixes in the store read path.

A locked keychain no longer reads as a disconnect. currentRecord() treated
any failure from readOurCache() as fatal, and a nil as "the item vanished",
which cleared isBootstrapCompleted. .unavailable now falls back to the last
known record and leaves the flag set.

Recency. A Keychain hit always won and the legacy file was then unlinked,
even when the file was newer. This service name has been in use since May
2026, so an upgrading install can hold a months-old item beside a file the
pre-migration build wrote today; the older token won and the newer copy was
deleted. Both stores now compare first (expiresAt for Claude, lastRefresh for
Codex) and adopt the later one before anything is removed. Codex matters most
here: serving a spent rotating refresh token ends in a terminal invalid_grant.

lastLegacyCleanupFailed is gone. It was set on every cleanup path and read
only by tests, never surfaced. The retry it was meant to signal already
happens, because the unlink is attempted on every successful read.

Also serializes migrate + unlink under the existing SafeFile.withExclusiveLock
so two menubar instances cannot race on the same legacy file, and drops a
leftover no-op local.
2026-08-19 11:43:32 -07:00
iamtoruk
e213e192b4 fix(menubar): never let a Keychain read raise UI on the refresh timer
Cache reads run on the background quota timer, so they must not be able to
put a panel on screen. Measured on macOS 15 against a throwaway keychain:
with the keychain locked, SecItemCopyMatching blocks on an unlock panel even
when the query carries kSecUseAuthenticationUI: ...Fail or a non-interactive
LAContext. Both of those govern the data-protection keychain; unlocking a
file-based keychain is something securityd drives itself. The only reliable
suppression is not issuing the read, so check lock state first and report
.unavailable instead.

.unavailable is separate from readFailed on purpose: a locked keychain means
"cannot look right now", not "the item is gone", and callers must not turn it
into a disconnect. It also carries a readable errorDescription so a -25308
reaching the UI reads as "Keychain unavailable" rather than a struct dump.

SecKeychainGetStatus is soft-deprecated with no replacement that reports
file-keychain lock state; annotating the warning away only moves it to the
call site, so it is left visible with a comment.

Adds the first test that touches a real Keychain, against a throwaway service
name no build reads, skipped when the host has no usable Keychain.
2026-08-19 11:43:22 -07:00
iamtoruk
bb5b71dff1 feat(models): name the MiMo v2 Flash row
It was the only MiMo row still rendering as its raw slug next to
"MiMo v2.5" and "MiMo v2.5 Pro". SORTED_SHORT_NAMES is longest-first, so
the two v2.5 entries keep their own labels.
2026-08-19 11:33:13 -07:00
iamtoruk
6558a4feee docs(changelog): note the plan budget copy fix
User-visible on two surfaces and the branch had no entry.
2026-08-19 11:31:47 -07:00
iamtoruk
9752f02636 fix(hermes): memoize the origin lookup; cover PR links end to end
githubOwnerRepoFromRoot re-read .git/config once per session, so every
session in the same repo paid for the same two syscalls. Memoize it per repo
root for the life of the process.

The prLinks plumbing from a provider call through the session cache into the
session summary had no test above the provider boundary; add one that runs
the real parseAllSessions pipeline against a temp HERMES_HOME. It fails
against the pre-change parser.
2026-08-19 11:31:40 -07:00
iamtoruk
e6cac54661 fix(models): derive the vendor-namespace allowlist from the pricing catalog
The hand-written KNOWN_NAMESPACES set dropped pricing for vendor prefixes
LiteLLM itself indexes: x-ai/, nousresearch/, zhipu/, litellm_proxy/ and
openai_like/ all priced on main and went unpriced here. Derive the set from
the loaded pricing keys instead, so a vendor the catalog knows is never lost
to a stale list, and keep only the spellings no catalog carries as explicit
extras: the routing wrappers, the client-side kimi/ and mimo/ prefixes, and
the litellm_proxy/ + openai_like/ routes. Local runners are excluded on
purpose, so an unlisted ollama tag cannot strip down to a priced cloud row.

xiaomi/ stops being a routing wrapper: it is the namespace LiteLLM prices
MiMo under, and BUILTIN_ALIASES maps the bare MiMo ids INTO it, so peeling
pulled against the alias. It stays known via the derived set.

Also: a user price override for a bare id now wins over the catalog row a
routed spelling of it would otherwise hit, and a namespaced GLM-5.3 is no
longer LABELLED GLM-5.2 by the sibling alias it prices through.

Tests assert the allowlist through a price override on a synthetic id, so
they cannot rot with the snapshot; the glm-5.4 assertion that pinned on the
snapshot NOT carrying a model is dropped.
2026-08-19 11:31:34 -07:00
iamtoruk
911bd3f486 fix(plans): say monthly budget, not calendar month, and fit 80 columns
The budget window comes from computePeriodFromResetDay, which builds an
anniversary period from plan.resetDay (1-28, settable per plan with
`codeburn plan set --reset-day`). "Calendar-month budget" and "Next
calendar reset" are therefore wrong for anyone who moved the reset day,
which is the same class of inaccuracy this change set exists to remove.
Say "budget" and "Next budget reset" instead, and use one wording across
the TUI and the desktop cards.

Both TUI lines truncate end-first at the terminal width. The headline had
grown past the point where an 80-column terminal still showed the
percentage, so it drops "vs ... /mo" for "/ $300.00 budget", and the
status line drops the clause repeating "budget" from the headline. At 80
columns the longest label (custom plans carry their provider) now fits
the percentage, and the status line still shows the projection.
2026-08-19 11:30:07 -07:00
iamtoruk
6bd86dbc36 ci: build, test and package the macOS menubar on every mac/ change
release-menubar.yml only packages; the 170+ Swift tests gated nothing.
2026-08-19 11:29:14 -07:00
iamtoruk
ef040a4c4d test(models): pin the shipped MiMo v2 Flash crash; name the base 2.5 row
The `mimo-v2-flash -> xiaomi/mimo-v2-flash` alias shipped before this
branch and already cycled through display-name resolution, so
getShortModelName threw RangeError on every real MiMo v2 Flash session.
The new cycle-safe resolver fixes it, but nothing pinned the ids that
actually crashed in production: cover the four spellings found in a real
session cache, including the unnamespaced `mimo/mimo-v2-flash`.

Add the base `mimo-v2.5` display name so the row reads next to
"MiMo v2.5 Pro" instead of showing a raw slug; SORTED_SHORT_NAMES is
longest-first, so the Pro tier still wins its own entry.
2026-08-19 11:26:22 -07:00
iamtoruk
a4384d28f2 Merge remote-tracking branch 'origin/main' into pr1041-rebase 2026-08-19 11:19:11 -07:00
iamtoruk
6f1098d215 Merge remote-tracking branch 'origin/main' into pr1042-rebase 2026-08-19 11:14:22 -07:00
iamtoruk
1246c963c1 Merge remote-tracking branch 'origin/main' into pr1039-rebase 2026-08-19 11:13:32 -07:00
iamtoruk
1ced7fe225 Merge remote-tracking branch 'origin/main' into pr1037-rebase 2026-08-19 11:12:54 -07:00
Resham Joshi
c9ad778cbc
Merge pull request #1036 from getagentseal/ci/upgrade-path-verification
ci: verify the 0.9.20 upgrade path on every platform
2026-08-19 11:10:41 -07:00
Aditya Vikram Singh
827a41241c fix(models): keep getShortModelName unary for Array.map
CI typecheck failed: sessions-report maps getShortModelName, and
the Extra High cycle Set was a second parameter. Array.map fed
the index as `seen`.

Cycle tracking stays on an internal helper. Display and alias
behavior unchanged. No second Extra High.
2026-08-19 21:27:13 +05:30
Aditya Vikram Singh
d5ced78595 fix(models): cycle-safe short names; keep user-alias display
Extra High MERGE AFTER FIX on d3f86f5. mimo-v2.5 aliased to
xiaomi/mimo-v2.5 then last-segment recursed forever. Looking up
SHORT_NAMES before resolveAlias also froze user remaps of known
ids (gpt-4o still displayed as GPT-4o).

Follow user aliases first. Break strip→alias→leaf cycles.
Do not invent a Kimi rate. Do not paper over this with a
mimo-v2.5 SHORT_NAMES row.
2026-08-19 21:10:33 +05:30
Aditya Vikram Singh
98d109d425 fix(menubar): fail closed on leftover JSON and partial Disconnect
Extra High MERGE AFTER FIX on 252ea92. Pathname chmod was
unverified. Disconnect hid retry when only the legacy file
survived. Secure read stopped at exactly maxBytes.

Tighten leftovers via opened-fd fchmod+fstat. Keep bootstrap
unless both Keychain and legacy deletes succeed. Read
maxBytes+1 so growth past the limit is rejected.
2026-08-19 20:56:07 +05:30
Aditya Vikram Singh
610c70ecb9 fix(plans): update desktop FX budget-copy assertion 2026-08-19 20:53:18 +05:30
Aditya Vikram Singh
3e93741f6f fix(hermes): require origin identity and fail-closed fences 2026-08-19 20:50:19 +05:30
Aditya Vikram Singh
d3f86f5d16 fix(models): alias bare MiMo 2.5 ids to the LiteLLM Xiaomi rows
Hermes and token-plan sessions store mimo-v2.5-pro. The snapshot
row is xiaomi/mimo-v2.5-pro. Same class as the existing
mimo-v2-flash alias. No invented rate.

Looking up the display name on the stripped leaf before following a
pricing alias, so cline-pass/mimo-v2.5-pro cannot recurse
strip → alias → last-segment forever.
2026-08-19 20:44:18 +05:30
Aditya Vikram Singh
fae4db0008 fix(hermes): restore z-ai pricing; require owner/repo; reject UNC
Extra High held 91754c5. z-ai/glm-5.2 went unpriced (Cline's real
vendor spelling). Forward-slash UNC became a workspace on POSIX.
PR matching used basename, so evil/codeburn collided. Tilde fences
were still scanned.

Add z-ai to known namespaces. Reject // UNC on POSIX. Attribute
PRs only when origin yields owner/repo. Strip ``` and ~~~ fences.
Bump Hermes parse version to v4.
2026-08-19 20:32:51 +05:30
Aditya Vikram Singh
91754c5f54 fix(hermes): fail closed on unknown namespaces, relative cwd, fenced PRs
Extra High held #1039 again. An unknown provider/model still became a
bare-model price via getCanonicalName's first-segment strip. Relative
cwd values like '.' could inherit the invoking repo. PR scrape treated
fenced dumps and other repos as attribution.

Peel only known vendor/router namespaces. Require an absolute
platform path before a Hermes cwd is a workspace. Ignore fenced URLs
and, when a git root exists, keep only that repo's pull links.
Bump the Hermes parse version so old cache rows reparse.
2026-08-19 20:19:29 +05:30
Aditya Vikram Singh
9646890638 fix(plans): call sticker-price bars a budget, not live quota
SuperGrok Heavy is a $300/mo preset compared to parsed API-equivalent
spend on a calendar month. CodexBar's live weekly window is a different
class. TUI and desktop copy now say budget / calendar month and deny
a live provider window. No Grok Connect is invented.
2026-08-19 20:07:02 +05:30
Aditya Vikram Singh
252ea92d3b fix(menubar): keep Keychain failure paths from leaving plaintext
A valid Keychain item plus a leftover JSON used to skip chmod, so a
failed unlink could leave 0644 secrets on disk. Failed Disconnect also
cleared bootstrap and hid the retry. Repair leftover files to 0600,
keep bootstrap when Keychain delete fails, revalidate the opened fd,
and loop the secure read.
2026-08-19 20:02:44 +05:30
Aditya Vikram Singh
10fdec6037 fix(hermes): fail closed on unknown GLM, namespaces, and ACP identity
Extra High review held #1039. Stop pricing every future glm-5.x as 5.2,
stop collapsing unknown provider/org/model trees onto a priced leaf, and
stop calling ACP Buzz — source=acp is a transport and the DB has no
client field. Strip trailing punctuation on PR URLs and bump the Hermes
parse version so old cache rows reparse.
2026-08-19 19:42:42 +05:30
Aditya Vikram Singh
66a776acfa fix(hermes): treat routing prefixes and session surfaces as classes
Price any OmniRoute/Cline/cmd/antigravity wrapper by peeling prefixes
and, for GLM 5.x, falling back to the newest priced sibling. Do not
require a new alias per model id.

Classify Hermes sessions by surface and workspace: ACP is the Buzz
app; project comes from git root or a real cwd, never $HOME or a
profile name.
2026-08-19 18:46:11 +05:30
Aditya Vikram Singh
0abbbd1b4a fix(hermes): only attribute PRs mentioned by the user or assistant
Tool dumps (gh pr list, changelog grep) were smearing one session
across dozens of pull URLs. Capture links from user and assistant
text only.
2026-08-19 18:28:07 +05:30
Aditya Vikram Singh
f97d084109 fix(models): price gateway-prefixed glm-5.3 ids
Hermes stores Cline Pass as cp/cline-pass/glm-5.3. One prefix strip
leaves cline-pass/glm-5.3, which missed the bare glm-5.3 alias and
stayed $0. Price the last path segment through the same aliases.
2026-08-19 18:20:01 +05:30
Aditya Vikram Singh
0658308448 fix(hermes): price glm-5.3, label default as hermes, capture PR links
GLM-5.3 is missing from LiteLLM, so Hermes/Cline sessions priced at $0.
Alias it to the glm-5p2 sibling and keep the display name GLM-5.3.

The root Hermes profile was shown as project "default". Use "hermes"
for that profile only; named profiles and cwd inference are unchanged.

Scan Hermes transcripts for https GitHub pull URLs and attach them so
the Pull requests page can attribute those sessions.
2026-08-19 18:06:51 +05:30
Aditya Vikram Singh
cdaa5b7ed3 fix(menubar): migrate Claude/Codex caches to namespaced Keychain
Stop writing OAuth caches as Application Support JSON. Persist
CodeBurn-owned items in Keychain, secure-read and migrate leftover
0644 files only after read-back verification, and keep Claude from
storing a refresh token.
2026-08-19 15:21:28 +05:30
Aditya Vikram Singh
ad406cf10f fix(tui): coalesce resize bursts without dropping updates
Hold Ink stdout columns/rows frozen during a SIGWINCH burst and emit
one settled resize, then rerender. Do not intercept writes, so a
mid-burst state update still paints even when net size is unchanged.

Fixes #977.
2026-08-19 13:48:50 +05:30
iamtoruk
a336b6bbe5 Merge remote-tracking branch 'origin/main' into ci/upgrade-path-verification 2026-08-18 12:48:30 -07:00
Resham Joshi
bb1974dab8
Merge pull request #1035 from getagentseal/fix/scoped-save-republish
Some checks failed
CI / semgrep (push) Waiting to run
Tests / test (22) (push) Waiting to run
Tests / test (22.13.0) (push) Waiting to run
Windows Menubar CI / check (ubuntu-latest) (push) Has been cancelled
Windows Menubar CI / check (windows-latest) (push) Has been cancelled
cache: stop republishing unchanged month shards on ranged runs
2026-08-18 12:48:16 -07:00
iamtoruk
3093887a14 Merge remote-tracking branch 'origin/main' into fix/scoped-save-republish
# Conflicts:
#	CHANGELOG.md
2026-08-18 12:48:02 -07:00
Resham Joshi
6325b428d4
Merge pull request #1031 from getagentseal/fix/pre-release-hardening
hardening: cache-scope escape hatch, promptSource on large lines, distinct Grok 4.5 rows
2026-08-18 12:47:59 -07:00
iamtoruk
ed9057f230 Merge remote-tracking branch 'origin/main' into fix/scoped-save-republish
# Conflicts:
#	CHANGELOG.md
2026-08-18 12:43:52 -07:00