| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
Provider: Add Hicap gateway provider (#979) * Add Hicap provider and gateway auth presets * Fix Hicap compatibility preset coverage * Authenticate ripgrep download in PR checks * Use Opus 4.7 as Hicap default * Address Hicap review feedback * Address provider review blockers * Clarify gateway header UI docs * Remove Hicap endpoint from README | 4 个月前 | |
feat(partners): add Concentrate and Exa to partner roster (#2141) Adds Concentrate (concentrate.ai) and Exa (exa.ai) to the README partners table and the web landing page, with light/dark logo variants self-hosted under docs/assets/ and web/public/partners/. Co-authored-by: OpenClaude <openclaude@gitlawb.com> | 1 个月前 | |
fix(openai-shim): request streaming usage from compatible local providers (#2144) * fix(openai-shim): request streaming usage from local providers * test(openai-shim): verify cached streaming usage accounting * fix(openai-shim): retry strict streams without usage options | 14 天前 | |
feat(web-search): add Ollama backend (#2234) * feat(web-search): add Ollama backend * fix(web-search): harden Ollama routing * fix(web-search): preserve Ollama profile identity * fix(web-search): align Ollama diagnostics * fix(web-search): align Ollama endpoint diagnostics * fix(web-search): honor Ollama endpoint aliases * fix(web-search): reject Ollama key placeholders * fix(web-search): complete Ollama configuration guidance | 14 天前 | |
fix(agent): forward subagent permission prompts to parent session (#2235) * fix(agent): forward interactive permission prompts * fix(agent): preserve nested permission prompts * fix(agent): isolate permission prompt state * fix(agent): preserve root state for forked commands * fix(agent): close permission handoff races * fix(agent): settle forwarded permission cancellations * fix(agent): keep permission prompts session-owned * fix(agent): preserve origin session permission policy * fix(agent): retain permission owner across delayed starts * fix(agent): isolate concurrent permission prompts * fix(agent): retain inactive owner permission prompts * fix(agent): preserve owner decisions across switches * fix(agent): scope channel permission requests * fix(agent): validate permission mode updates against root context Subagent persist paths were validating setMode against the agent-local context while applying to the root session, which could enable fullAccess or bypassPermissions when only the child marked bypass available. | 14 天前 | |
feat(aimlapi): passwordless onboarding and resumable card top-up (3/3) (#2032) * feat(aimlapi): add checkout state persistence and sign-in key cache * fix(aimlapi): cover lock recovery and complete the reset receipt * test(aimlapi): cover CAS result, reset receipt and sign-in key permissions * fix(aimlapi): make stale-lock recovery ownership-safe across processes * test(aimlapi): pass a file URL specifier to the lock workers * fix(aimlapi): use proper-lockfile and harden checkout-state persistence * fix(aimlapi): preserve issued keys and survive stale-lock steal races * fix(aimlapi): surface swallowed lock-retry conditions * feat(aimlapi): resume interrupted checkouts in the top-up entry points * fix(aimlapi): keep resumable checkouts through transient and paid states * fix(aimlapi): surface a lost settled-receipt write to the caller * fix(aimlapi): harden checkout resume across CLI resume, idempotency and transient errors * fix(aimlapi): resume settling checkouts and preserve records on ambiguous reads - Treat 'exchanging' as a paid/resumable status so a run interrupted between payment and receipt resumes the exchange instead of opening a second, chargeable checkout (matches pollUntilPaid). - Preserve the recorded checkout on a malformed-but-successful status read (AimlapiApiError status 200), alongside the existing transient-error path. - Print the full recovery key in the receipt-write-failed warning; a masked key is useless as a last copy. - Re-register the real client/providerProfile modules in afterAll so the test stubs cannot leak into later files (mock.restore does not undo mock.module). - Bound each lock worker's exit before draining its pipes, and loosen the held-lock timeout assertion to any errno (Windows is not always ELOCKED). * fix(aimlapi): stop mock leak, fail closed on spent sessions, clear GUI receipt - topup.test.ts: capture the real client/providerProfile modules through a cache-busting query so afterAll restores the genuine module instead of the corrupted (stub-mutated) reference. Without this the './client.js' stub bled past afterAll and failed 25 client.test.ts cases whenever it ran first. - resolveCheckoutSession: fail closed when a resumed session is already 'exchanged' but no settled receipt survived locally, matching pollUntilPaid, instead of opening a second chargeable checkout for a one-shot key that is already gone. - provisionAimlapiKey: return a clearReceipt closure; ProviderManager now calls it only after persistDraft actually saves, so a second GUI top-up opens a fresh checkout instead of short-circuiting to a stale key or throwing. - claimAimlapiTopupState: refuse to replace a stored record that still has an open resume token for a different intent, so a changed amount cannot strand a still-payable checkout and open a second one. - Mask the issued key in the CLI receipt-write-failed warning; a paid-for credential should not land in scrollback. - index.ts: note the live CLI/GUI callers and the clear-receipt obligation. - Regression coverage for exchanged fail-closed, the claim guard, and the GUI receipt clear. * fix(aimlapi): inject the topup transport instead of mocking client.js globally The prior mock.module('./client.js') stub in topup.test.ts leaked past its afterAll into client.test.ts (25 failures when this file ran first in CI, bun 1.3.13). Replace it with a local injection seam: - topup.ts exposes setAimlapiTopupTestDoubles, and both entry points create their client / write their profile through it (defaults unchanged, so production behaviour is identical). - topup.test.ts injects a stub transport through that seam and no longer calls mock.module at all, so nothing can bleed OUT to client.test.ts. - It still loads topup.js through a cache-busting ?ts= query so it stays immune to ProviderManager.test.tsx's mock.module('../integrations/aimlapi/index.js'), which mock.restore() does not undo and which would otherwise replace the shared provisionAimlapiKey binding (verified: without the query the barrel stub reaches this file and every topup test fails). * fix(aimlapi): converge racing checkouts and preserve state on ambiguous reads - Close a post-claim race: two runs of the same intent converge on one payment id, then each can open a session before either records one, and the second save overwrote the first's resume token (two payable checkouts). Add recordAimlapiCheckoutSession, a compare-and-swap that only records while the token is empty; resolveCheckoutSession adopts the winner's session and abandons the one it just opened, so pay() converges idempotently on a single charge. - Resume path now preserves the record on any ambiguous getSession error (transient, malformed-200, auth/4xx) and retires it only on a definitive 404/410 gone-session, instead of clearing on every non-transient error. - pollUntilPaid retries a malformed-but-successful (status 200) body instead of aborting, matching the resume path. - Regression coverage for the peer-records-first race, ambiguous-error preserve, 404 replace, and poll-retry-on-200. * fix(aimlapi): re-validate an adopted peer session before paying it When two racing runs converge and this one adopts the peer's recorded session, route that session through the same status classification as the initial resume: return it only while resumable, fail closed on 'exchanged', and surface a re-run error on any other terminal status instead of calling pay() on a dead session. Add regression coverage for an adopted session that is exchanged or cancelled in the race window. * test(aimlapi): make abandoned-lock recovery test deterministic The stale-lock recovery test asserted that all racing claims return the same payment id. Under a stale-lock steal two recoverers can briefly hold the lock and mint distinct ids, so that assertion was flaky in CI. A diverged claim is harmless — it is refused at its next compare-and-swap — so the invariant that actually matters is that exactly ONE checkout gets established. Drive the workers through the full claim->record flow and assert a single winner, which the single-slot store plus the record CAS guarantee deterministically. * fix(aimlapi): acquire checkout-state off the interactive thread and recover fresh orphans The interactive top-up flow acquired the checkout-state lock synchronously, parking the Ink event loop (UI, timers, SIGINT) on Atomics.wait for up to the 5s timeout, and that timeout was shorter than the 30s stale window so a lock orphaned by an interrupted holder could not be recovered on an immediate resume. - Add withStateLockAsync + async variants of the state mutators, sharing the same inner operations. It acquires with the timer-free lockSync (a sub-ms mkdir) but yields via await between retries, so the UI stays live, and its longer deadline (15s) covers the stale window so a fresh orphan is reclaimed once stale rather than timing out. Shrink the stale window to 8s (sub-ms sections never approach it) so recovery is quick. - Route topup.ts (CLI + GUI) through the async variants; provisioned.clearReceipt is now async, and ProviderManager awaits it best-effort so a cleanup failure cannot surface an error that invites a retry (a duplicate provider profile). - Regression coverage for async orphan recovery (mutation-checked: a deadline below the stale window fails to recover). Generous per-file test timeout since the now-async provisioning yields to a loaded runner's event loop. * test(aimlapi): cover the async state mutators and speed up the orphan-recovery test - Add thin contract tests for saveAimlapiTopupStateAsync (CAS accepted/rejected), recordAimlapiCheckoutSessionAsync (compare-and-swap on the empty resume token), and clearAimlapiTopupStateAsync (ownership-scoped clear); the CLI/GUI flow now routes through these and only claimAsync was exercised. - Back-date the orphaned lock to just inside the stale window so the async recovery test reclaims it in ~2s instead of burning the full 8s window. * fix(aimlapi): fail closed on corrupt state, deliver keys on receipt failure, add a reset action - Fail closed when the checkout-state file is present but unreadable/schema- invalid instead of reading it as absent: a claim would otherwise overwrite an open/paid checkout or an exchanged key and open a second chargeable one. - Never strand a one-shot exchanged key: a receipt-write that throws (lock timeout / fs / corrupt state), not just a lost CAS, is caught so the CLI still writes the profile and the GUI still returns the key; the post-delivery clear is best-effort too. - Add an explicit discard/reset escape hatch — discardAimlapiCheckoutState, an "openclaude aimlapi reset" command, and a GUI "Start over" on the top-up error screen — so a terminal checkout (whose resume token blocks a different intent) or a corrupt state file can be cleared without editing internal files. - Surface a failed GUI receipt retirement: retry a few times, then show a non-blocking warning instead of swallowing it silently. - Only chmod a config directory this flow actually created (via mkdirSync's return); never tighten a pre-existing OPENCLAUDE_CONFIG_DIR root. The state file's own 0600 mode protects the credential. - Tests for each, incl. corrupt/schema-invalid fail-closed, receipt-write-throw key delivery (CLI + GUI), discard semantics, retried/surfaced GUI cleanup, and a POSIX check that an existing config dir keeps its mode. * fix(aimlapi): surface the CLI recovery-receipt clear failure and cover the reset handler - The CLI finishCliTopup clear failure only went to logForDebugging (debug-only), invisible to the user, unlike the loud receipt-write warning and the GUI warning. Print a [warn] line pointing at "openclaude aimlapi reset" so a stranded receipt that blocks a different top-up is not hidden. - Add tests for the aimlapiReset CLI handler (discards a stored checkout / reports when there is nothing to discard). - Assert the CLI clear-failure warning is surfaced in the receipt-write-failure test (mutation-checked). - Document "openclaude aimlapi reset" and the GUI Start over recovery in docs/aimlapi-setup.md. * fix(aimlapi): protect a settled receipt from reset and bind method into the checkout identity - Discard (CLI reset / GUI Start over) no longer deletes a settled receipt — the only copy of a paid-for, one-shot key — unless forced. discard now returns 'discarded' | 'kept-settled' | 'none'; the CLI adds --force and the GUI refuses and points back at the recovering retry. - Add the payment method to AimlapiTopupIntent so a card->crypto (or reverse) restart is a different intent and cannot adopt the prior checkout's reused idempotency id on the wrong rail; covered by a changed-method resume test. - Narrow the sign-in-key cache: it is a persistence primitive for the follow-up guided passwordless flow with no in-tree consumer, so drop it from the public barrel and document the scope (kept in topupState.ts for that follow-up). - Regression + mutation coverage for the settled-receipt protection and the method-scoped intent. * fix(aimlapi): back off receipt-clear retries, expand the discard API, and test Start over - Add a 150ms backoff between the GUI clearReceipt() retries so the loop can actually ride out a lock-contention window instead of exhausting three back-to-back attempts. - Re-export AimlapiDiscardResult and add resetAimlapiCheckoutSessionAsync so barrel consumers can name the discard outcome and use a non-blocking reset, matching the other mutators. - Cover the GUI Start over recovery: it is 'r' (settings:retry) on the top-up error screen — the Settings context has no confirm:yes and Enter closes the panel; tests drive both the discard and the kept-settled refusal path. - Rename the test's stale-age constant (LOCK_AGE_WELL_PAST_STALE_MS) so it no longer reads as the source's 8s window. * fix(aimlapi): serialize the one-shot key exchange and recover receipts before login Address three checkout-state review findings: - [P1] Serialize the non-idempotent key exchange behind an exchange lease so racing same-intent processes mint and record the credential exactly once. The elected lease holder exchanges and records the settled receipt; a peer that loses the election waits for that receipt and resumes from it instead of exchanging in parallel; a lease abandoned by a crashed holder goes stale (past the client request timeout) and is reclaimed on a later attempt. The lease is released on a failed exchange so a retry is not blocked for the stale window. - [P1] Recover a settled local receipt BEFORE authenticating in both the CLI and guided entry points. A run interrupted after the one-shot exchange but before the profile write leaves the paid-for key only in that receipt; requiring a fresh login to reach it stranded the key whenever the password had changed or the auth service was down. The receipt read is side-effect free and needs no token, so it now runs first and only authenticates when a checkout must be created/resumed/exchanged. - [P2] Fix the guided-recovery key in the setup guide: Start over is bound to r, not Enter (Enter closes the settings panel). Covered by state-layer lease tests (acquire/held/stale-steal/settled/gone/ release), a two-process race asserting the exchange runs exactly once with the loser resuming the receipt, and no-auth-on-settled tests for both entry points. * test(aimlapi): harden the exchange-lease coverage Address review follow-ups on the exchange-lease tests (all test-only): - Gate the two-process race on the loser's own "waiting" status signal instead of a fixed sleep, and assert it fired, so the test deterministically exercises the held -> wait -> resume path rather than possibly reading settled directly. - Type seedPersistedState's overrides as Partial<AimlapiPersistedTopup> so a misspelled/wrong-typed seed key is a typecheck failure instead of a record that silently reroutes the test down another branch. - Cover re-acquiring your own lease (leaseOwner === owner) so the guard that keeps a caller from mistaking its own fresh lease for a live peer's — and self-blocking until the stale window — cannot regress unnoticed. * fix(aimlapi): fence a superseded profile write, protect unreadable state, resume the receipt model Address three checkout-state review findings: - [P1] Fence an in-flight checkout before it writes its key to the provider profile. If a reset (or a fresh top-up) replaces the stored slot while an abandoned flow is awaiting client.exchange(), that flow's settled-receipt CAS now misses; previously it still went on to write its stale key and could clobber the profile the new top-up created. recordSettledReceipt now reports recorded | superseded | errored, and the exchange path aborts (rejecting, and pointing the user at rotating the orphaned key) on `superseded` while still delivering on a transient `errored` so a paid-for key is never stranded. - [P1] Do not discard an unreadable/corrupt state file without --force. Such a file could be a damaged receipt holding the sole copy of a one-shot key, so discardAimlapiCheckoutState now returns `kept-unreadable` and keeps it unless forced — matching the safety promise (and existing settled-receipt protection) that reset never loses an issued key. The CLI and guided "Start over" surface the new outcome and point at `reset --force`; docs updated. - [P2] Use the settled receipt's model when a peer completed the exchange. The settled lease branch dropped lease.state.model, so a loser resuming another run's receipt configured its own --model instead of the one actually provisioned; it now propagates the receipt's model through both callers. Covered by: a superseded-mid-exchange fence test, corrupt-discard-needs-force tests (state layer + CLI + guided GUI), and a two-process race asserting the loser adopts the winner's provisioned model. All three are mutation-proven. * test(aimlapi): drop the flaky third guided Start-over drive test The guided "Start over on a kept-unreadable discard" test added a third consecutive full Ink mount+drive to ProviderManager.test.tsx. On the CI-pinned bun (1.3.13) that destabilises the Ink stdin harness (`stdin.ref is not a function`), so the 'r' keypress never reaches the handler and the awaited discard call never fires — a timeout unrelated to the code under test (it passes on local bun 1.3.14). The two-drive configuration is green on CI. The kept-unreadable behaviour stays covered where it is deterministic: the state layer (kept-unreadable unforced, discarded on --force) and the CLI handler (the `reset --force` guidance). The guided keybinding→discard→refusal plumbing is covered by the identical-structure kept-settled drive test; the kept-unreadable GUI branch is a trivial mirror of it. A comment records why the third drive is intentionally omitted. * wip(aimlapi): converge integration + CLI to the passwordless card-only flow (#1988) Mid-port checkpoint. Rewrites the AI/ML API integration backend and CLI to the canonical passwordless, card-only design (target PR #1988), in the current code style. NOTE: the branch does not yet compile — the GUI (ProviderManager passwordless rewire) and the aimlapi test suite still reference the removed API and are the remaining work. Done (typecheck-clean in these files): - config.ts: add payBaseUrl + verificationBaseUrl endpoints, buildPartnerReturnUrl. - topupState.ts: reduce to the #1988 shape — intent keyed on payBaseUrl/ verificationBaseUrl (no `method`); sync lockfile-based state; drop the exchange-lease, discard/reset-command surface, async variants and fail-closed-on-corrupt (corrupt reads as absent). - client.ts: drop the password path (signup/login) and PaymentMethod/crypto; pay() is card-only. - topup.ts: rewrite to the passwordless phase-machine flow (checkAccount -> code sign-in / new-account -> provision or by-key top-up; resolveTopupSession; pollUntilPaid / pollUntilExchangeSettled / pollUntilByKeyToppedUp). Keeps the DI test seam (no cross-file mock.module). Profile env writes AIMLAPI_API_KEY mirror + CLAUDE_CODE_PROVIDER_ROUTE_ID. - onboarding.ts, messages.ts (canonical copy), validation.ts, index.ts (lean barrel), providerManagerAimlapi.ts (GUI indirection layer): new. - CLI: aimlapiCommand.ts (registerAimlapiCommand, --email/--code/--amount, no --method/reset), main.tsx wiring, handlers/aimlapi.ts (redacted errors). Mandatory attribution headers wired on EVERY aimlapi request: X-AIMLAPI-Source (agent/openclaude) + X-AIMLAPI-Partner-ID — in client.ts request() (auth/ checkout) and the config attribution path (inference/catalog). * wip(aimlapi): port the passwordless provider-manager GUI (#1988) Rebase ProviderManager.tsx on the #1988 passwordless aimlapi flow (email -> 6-digit code -> low-balance -> top-up / paste-existing-key -> done), replacing the old password/method/Start-over flow and importing the aimlapi surface from providerManagerAimlapi.js. Re-apply the current newer-main, non-aimlapi `apiFormat: 'auto'` feature that the rebase would otherwise revert (form metadata, toDraft default, display label, startCreateFromPreset default, the API-format picker's Automatic option, and persistDraft's selectedApiFormat 'auto' -> undefined branch, kept alongside #1988's deferNavigation/onSaved). Re-export resolveRouteCredentialValue from integrations/index for the GUI. All source now typechecks; the aimlapi test suite is the remaining work. * test(aimlapi): port integration + CLI tests to the passwordless flow - topupState/topup/onboarding/aimlapiCommand tests: port #1988's coverage, adapting the transport to `globalThis.fetch` stubbing and the profile/prompt doubles to the module's `setAimlapiTopupTestDoubles` DI seam (no process-global mock.module, which leaks across files in this repo). - client/config tests: keep the current repo's stricter versions (complete session contracts), pruning the removed password/crypto cases. - Add mandatory-attribution-header coverage on all four request classes: the client sends X-AIMLAPI-Source + X-AIMLAPI-Partner-ID on auth/checkout, and the config attribution path sends both on inference/catalog and strips them for a non-canonical proxy endpoint. - Remove the reset-based CLI handler test (reset no longer exists). Full aimlapi integration + CLI suite green (67 tests). ProviderManager GUI test is the remaining piece. * test(aimlapi): port the passwordless provider-manager GUI tests (#1988) Rebase ProviderManager.test.tsx on #1988's version for the passwordless aimlapi GUI tests (email -> code -> low-balance -> top-up / paste-existing-key), which mock ./providerManagerAimlapi.js. Re-apply HEAD's newer-main apiFormat 'auto' test cases (API-mode picker, token field, OpenAI/GPT-5/MiniMax presets) since the source keeps that feature, and restore the current preset list ('LongCat') in the test's PRESET_ORDER so navigateToPreset indexes match the real presets. ProviderManager suite green (42 tests). * docs(aimlapi): rewrite the setup guide for the passwordless card-only flow (#1988) Describe the passwordless /provider flow (saved-key continue, new-user email + 6-digit code, paste-existing-key, low-balance top-up) and the card-only CLI `aimlapi topup --email/--code/--amount` (no --method, no reset). Document the full endpoint override set and the two mandatory attribution headers (X-AIMLAPI-Source + X-AIMLAPI-Partner-ID) sent on every request, stripped for a non-canonical proxy endpoint. * refactor(aimlapi): let tests inject prompt doubles into the top-up flow * fix(aimlapi): lock the partner id and complete mandatory-header coverage Lock the partner id to OpenClaude's own attribution id: drop the --partner-id CLI flag and the AIMLAPI_PARTNER_ID env override so rebate/revenue-share attribution can never be redirected. resolvePartnerId() now always returns the built-in id; the mandatory X-AIMLAPI-Partner-ID header itself is unchanged. Assert the mandatory X-AIMLAPI-Source header on the catalog/discovery and inference paths (discoveryService, bootstrap, runtimeMetadata) — the header was already sent, only the test expectations lagged. Refresh stale password-era copy in the interactive prompt and a top-up comment left over from the removed flow. * fix(aimlapi): address CodeRabbit review — settlement, key-safety, redaction - Wait for a resumed sign-in top-up to settle before returning: the account non-exchange path now mirrors the by-key flow, so a credited balance is never reported while the billing operation is still in flight. - Preserve a freshly minted sign-in key when the balance read is aborted, so an abort cannot orphan a paid credential and mint a second key on the next run. - Clear the first-run env-key adoption markers when validation fails, so a retry re-validates instead of short-circuiting into persisting the unvalidated key. - Never crash the top-up success screen on an amount parse edge — fall back to the raw entered amount after the payment has already cleared. - Show all four API-format options (visibleOptionCount 3 -> 4). - Document that guided provisioning requires the canonical inference endpoint. - Add regression tests: resumed-sign-in settlement, aborted-balance key retention, failed-env-key re-validation, and CLI error redaction. * test(aimlapi): wait for the masked code frame instead of a fixed delay The AIMLAPI code-screen assertion captured output after a fixed 25ms sleep, which is too short on a slower CI runner (Node 24) and intermittently missed the freshly rendered mask characters. Wait for the masked frame instead so the assertion is deterministic. * test(aimlapi): harden the provider-manager GUI flows against CI timing The GUI top-up flow intermittently failed on a loaded CI runner: the final keystroke on the success screen was sent in the same tick as the render, before Ink attached the input handler, so it was dropped and the flow stranded on the done screen. Add the same input settle the other steps already use. Also raise the shared waitForCondition default timeout (2s -> 5s). The predicate is polled every 10ms and returns as soon as it is met, so this only adds patience for a slow runner and never slows a passing wait — keeping the Ink-driven flows deterministic under CI load. * fix(aimlapi): gate attribution headers by trusted AI/ML API host The client sent the mandatory source/partner headers on every request, but the auth/app/pay/inference base URLs are all env-overridable — so a request pointed at a user proxy (notably the balance probe against an overridden inference URL) leaked OpenClaude's partner/source identity. Send them only when the resolved request host is aimlapi.com (production or staging, over HTTPS), mirroring the inference/catalog stripping contract in resolveAimlapiAttributionHeaders. Adds an isTrustedAimlapiRequestUrl predicate plus canonical-sends / proxy-withholds regression tests. * test(aimlapi): wait for the done screen to settle before the final keystroke Replace the fixed 25ms delay before the success-screen keystroke with an observable frame-stability wait, so a slow CI runner cannot drop the keystroke before Ink has committed the render and attached its input handler. * fix(aimlapi): durable receipts and atomic election for concurrent top-ups - Restore the atomic checkout-session election dropped during the passwordless convergence: recordAimlapiCheckoutSession is a first-writer-wins CAS, so two concurrent runs of the same intent settle on ONE payable checkout — a loser adopts the winner's token and abandons the session it just opened instead of leaving two chargeable checkouts. Wired through resolveTopupSession (the create branch elects then adopts; the resume branch notifies once) and both the CLI and GUI onSession callbacks. - Persist the settled receipt (apiKey / apiKeyId / model / settled) in the GUI BEFORE the profile write, so an interrupted or failed write resumes with the paid, one-shot exchanged key instead of stranding it (mirrors the CLI). - Clear the sign-in key cache with the just-minted key id on a sufficient-balance sign-in: persistDraft runs onSaved synchronously, so the aimlapiIssuedKeyId state setter has not applied yet — pass the id explicitly. Adds regression tests for the election (first-writer-wins + loser adoption), the settled-receipt ordering, and the sufficient-balance cache clear. * fix(aimlapi): abort on a lost election and keep the receipt write best-effort - Treat a null recordAimlapiCheckoutSession result as "the slot was cleared by a sibling that already completed this top-up" and abort, instead of silently proceeding to pay a second, unrecorded checkout (both the CLI persistSession and the GUI reportSession). Closes the residual double-charge race. - Make the GUI settled-receipt write best-effort: the payment already cleared, so a receipt-write failure (lock contention, full/read-only disk) must not divert the flow into the top-up error path — the profile write is what matters. - Align the recordAimlapiCheckoutSession test double with the real semantics: match on intent + payment id only and return null on a non-matching slot. Adds a regression test that a sibling clearing the checkout mid-flow aborts before any /pay call. The sufficient-balance sign-in test now waits for the code screen to settle before typing (the transition dropped the first keystroke). * test(aimlapi): settle after each awaited frame so keystrokes aren't dropped The provider-manager GUI tests type on the line after waitForFrameOutput matches a new screen, but Ink registers input handlers in an effect that runs after the render commits. On a loaded CI runner the first post-transition keystroke could be dropped, stranding the flow and timing out (seen intermittently on Node 22). Add a short settle after every frame match — returning the same matched frame, so no assertion changes — which lets the input handler attach before the caller types. Fixes the class instead of patching individual call sites. * fix(aimlapi): recover a settled GUI receipt and harden checkout/key edge cases - Recover a settled checkout receipt in the provider-manager GUI before provisioning: if a prior run paid + exchanged the key and saved the receipt but was interrupted before the profile write, finish that write with the retained key instead of re-entering provisioning against the now-exchanged session (which fails in resolveTopupSession and strands the paid credential). Mirrors the CLI. - Reject a non-HTTPS checkout payUrl at the client response boundary (the validator required only "openable"), so a session is never retained with an address the flow refuses later and then polls with no usable link. - Do not discard a freshly minted sign-in key when its cache write fails: copy the key into memory before persisting and make the sign-in-cache / top-up-state writes best-effort, in both the GUI and the CLI, so a lock/permission/disk failure can't force a second key on retry. - Narrow the setup guide: the canonical-inference requirement applies to new-account onboarding + key provisioning; the existing-key top-up runs against the configured endpoint. Adds regression tests for the settled-receipt recovery (no re-provision) and the non-HTTPS payUrl rejection. * fix(aimlapi): HTTPS checkout callbacks, per-email key cache, safer edges - Require a credential-free HTTPS base for the checkout return URLs (they embed the resumable session token) and for the browser return/landing URL, so a cleartext AIMLAPI_PAY_URL/AIMLAPI_RETURN_URL override can't leak the token or break the documented HTTPS return-target contract. - Store sign-in recovery keys as an email-keyed collection instead of a single global record, so a concurrent/interrupted sign-in for one account no longer evicts another's key (which forced a duplicate mint). Old single-record files migrate on read; clear stays per-email ownership-aware. - Treat post-success receipt cleanup as best-effort in both the CLI (finishProfile) and the GUI (resetAimlapiCheckoutIntent): the profile is already saved, so a lock/permission/IO failure clearing the receipt must not report failure. - Reject scientific-notation amounts: parseAimlapiAmountUsd now requires a plain decimal with at most two fractional digits, closing the "20.001e0" sub-cent bypass that silently rounded to a wrong charge. - Add the pay/verification/return env vars to the config-test snapshot so a set override can't pollute default-endpoint assertions. Adds regression tests for each. * fix(aimlapi): async checkout-state clear for the Ink flow + reject malformed bases - Clear the checkout receipt through an async lock in the provider-manager GUI: restore withStateLockAsync + clearAimlapiTopupStateAsync and fire it best-effort (unawaited) from the save callback, so a contended lock no longer blocks Ink input/timers/SIGINT after the profile is already saved. The CLI keeps the sync clear (one-shot command). - Reject a query string or fragment in the checkout/return base URLs: a base like https://pay.aimlapi.com/#x would swallow the appended /checkout?...sessionToken into the fragment, so the token never reaches the callback as a query param. - Surface a non-fatal CLI note when receipt cleanup fails (the profile is already saved and the stale receipt reconciles on the next run). Adds regression tests: async clear ownership, query/fragment rejection, and the legacy single-record sign-in-cache migration. * fix(aimlapi): reject bare ?/# delimiters in checkout and return base URLs url.search / url.hash are empty for a bare delimiter (e.g. https://pay.aimlapi.com/? or .../#), so those slipped past the query/fragment guard and still corrupted the appended /checkout?...sessionToken=... . Reject any raw ?/# in the candidate in both safeHttpsBaseUrl and requireHttpsBaseUrl, and cover the bare-delimiter cases. * fix(aimlapi): harden checkout recovery — exchange lease, retry modes, payable guard Addresses a fresh review round on the checkout state machine: - Restore the cross-process exchange lease (dropped in the passwordless convergence): the one-shot key exchange is serialized so two processes resuming the same paid sign-up session cannot both exchange and strand the credential — the lease winner exchanges, peers wait for its settled receipt. - Persist the exchange mode in the receipt so a retry that has since become sign-in still exchanges the paid session instead of minting an unrelated key and clearing the paid checkout (CLI + GUI). - Recover the checkout URL on a pending_payment resume by re-issuing the idempotent pay/top-up (the stable paymentSessionId prevents a double charge) instead of polling a session the user can never open. - Route settled-receipt recovery through persistExistingAimlapi for an existing saved profile / AIMLAPI_API_KEY top-up, so it updates the selected profile (preserveEnv) rather than minting a new one and copying the env key. - Confirm before abandoning an already-open checkout: editing amount/auto-top-up after a checkout URL was opened now requires an explicit re-submit (the old browser tab stays chargeable and no endpoint can cancel it). - Treat a credentials/query/fragment inference base as non-canonical so a `.../v1#x` override cannot be written as OPENAI_BASE_URL and break the shim. Tests: exchange-lease election + failed-exchange release, retry-exchanges-the- paid-session, idempotent URL recovery on resume, canonical-gate rejection, and the re-edit confirmation. * fix(aimlapi): repair exchange-lease liveness and the re-edit abandon guard - exchange lease: a peer that finds a live foreign lease now re-attempts on each poll instead of only watching for a settled receipt, so it resumes the moment the holder settles OR frees the lease (failed/crashed) rather than hanging the full 20-minute poll window; folds the wait into the lease loop. - exchange lease: treat a future-dated exchangeLeaseAt (backwards clock jump or an edited state file) as stale and reclaim it, instead of reading a negative age as perpetually fresh and deadlocking every peer. - re-edit guard: reset the abandon acknowledgement when a new checkout opens so a further edit to a different amount/auto-top-up is confirmed again instead of silently abandoning the freshly-opened chargeable tab; clear the opened-checkout tracking once payment settles so a later re-edit never warns about a paid tab. - tests: lease release is owner-scoped and preserves a settled receipt; a future-dated lease is reclaimed; the GUI re-edit warning re-arms after a second edit. * test(aimlapi): sync re-edit test on rendered amount; guard vacuous lease seed - re-edit GUI test: submit only once the edited amount is reflected in the rendered frame instead of after a fixed 25ms delay, so Enter is never processed against the stale amount on a slow runner. - future-dated lease test: assert the seed compare-and-swap actually persisted the lease before acquiring, so the reclaim path can never pass vacuously. - exchange lease: record a swallowed release failure via file-backed debug logging (safe on the Ink GUI path) so a lock/permission problem behind a slow takeover is diagnosable. * test(aimlapi): match the complete edited amount in the re-edit frame wait Prefix matching let "$250" match a stray "$2500" (and "$2500" match "$25000"), so a wrong-amount input regression could pass unnoticed. Pin the complete value with a negative lookahead on a trailing digit. * fix(aimlapi): make the one-shot key exchange crash-durable and per-operation Three checkout-recovery correctness fixes: - Persist the exchanged key under the CAS BEFORE returning it. The lease winner used to hand the /exchange key to the caller, which wrote the receipt only afterward; a crash in between left the checkout exchanged but its only key unpersisted, so a retry re-ran (and was rejected by) the spent one-shot exchange. exchangeKeyWithLease now records the settled receipt via recordAimlapiSettledKeyAsync (merges over the record, clears the lease) as soon as the exchange succeeds. - Use a per-operation exchange-lease owner instead of a module-global id. Two overlapping top-ups in the same process shared one owner, which the acquire treats as self and immediately reclaims, so both could POST the non-idempotent /exchange concurrently. A fresh owner per operation makes the second observe the first's lease as foreign and back off; a retry within one operation keeps its owner and still reclaims the lease it released. - Never serialize an empty apiKey/apiKeyId. The existing-key top-up path reports apiKeyId: '', which the reader rejects, making the whole settled receipt (and the paid key it records) unrecoverable. The save path now coerces an empty key/id to absent so the receipt stays readable. * fix(aimlapi): refuse to overwrite an unfinished checkout when the intent changes claimAimlapiTopupState backs a single slot, so rerunning with a different amount, auto-top-up, or endpoint used to unconditionally replace the stored record. When the prior checkout had opened a session (a resume token — possibly already paid but not yet exchanged) or held a settled key not yet written to a profile, that dropped the only handle to a paid session/key and stranded it permanently. claim now refuses a changed intent while such a record exists, with an actionable message to finish or cancel the earlier top-up first (re-running the same intent still resumes it). A never-advanced claim — empty resume token, unsettled, no key — is still replaced. The CLI surfaces the message directly; the interactive flow already clears the prior record on edit, so normal re-edits are unaffected. * fix(aimlapi): never settle a keyless receipt; keep the paid key reaching the profile Addresses a further review batch: - recordAimlapiSettledKeyAsync now refuses to mark a receipt settled (and clear the lease) when no key resolves from the call or the stored record. A keyless settled receipt would make a peer resume from a spent one-shot exchange with no credential; the record and its lease now survive so a retry can still exchange. - The CLI's pre-profile settled-receipt save is now best-effort (try/catch + a dim note), matching the earlier saves. A lock/permission/IO failure there no longer throws before finishProfile, so the paid, exchanged key still reaches the provider profile. - startCreateFromPreset drops aimlapiPersistedIntentRef on a fresh flow entry (in-memory only) so a later resetAimlapiCheckoutIntent can never clear a previous flow's on-disk receipt against a stale payment id. - Prompt copy: "Do you have an aimlapi.com key?" / "I already have an aimlapi.com key" (missing article). Tests: keyless settle is rejected and leaves the lease intact; the CLI forwards explicit --amount/--model; the settled-receipt recovery renders the top-up (not "ready") done copy. * test(aimlapi): assert the exchange lease stays held on a keyless settle attempt Tighten the keyless-settlement guard test: a "not settled" assertion also passes if the lease were wrongly cleared (a peer would then see 'acquired'). Assert the peer acquisition returns 'held' so the test pins that a keyless settle preserves the lease for a retry. * fix(aimlapi): poll the checkout token, not the auth bearer, while waiting on a resumed exchange pollUntilExchangeSettled was called with the passwordless-auth bearer instead of the partner checkout-session token, so it polled the wrong resource. A terminal error there clears the recovery receipt, stranding a paid one-shot sign-up exchange. * fix(aimlapi): keep the checkout receipt resumable through an unconfirmed amount/auto-top-up edit Editing the amount or auto-top-up cleared the persisted checkout intent and durable receipt immediately, before the abandon-ack confirmation that gates actually starting a new payment session. A user who edits and backs out (or completes the still-open browser checkout) before confirming lost the only mapping to that chargeable checkout, so a later run would open a new one instead of resuming the paid session. The reset now happens only once the user has explicitly confirmed abandonment: claimAimlapiTopupState takes an `abandonExisting` option that atomically overwrites the retained record under the same lock acquisition, instead of racing a separate async clear against a synchronous claim. * test(aimlapi): sync on the rendered email before submitting in the new receipt-resume test A fixed sleep doesn't guarantee the TextInput has processed the typed email before Enter is sent; a loaded runner can drop the submit. Wait for the typed value to actually render, matching the amount-edit sync already used later in this same test. * docs(aimlapi): describe checkout retention as durable, not session-scoped The prior wording ("retained while the provider flow remains open") undersold what topupState.ts actually does: the payment identity and any issued key are persisted to disk, so a restart resumes the same checkout too, and a prior paid+exchanged run finishes the profile write on the next run instead of re-provisioning. * fix(aimlapi): unify error-status extraction, drop dead top-up state, tighten wrappers - Extract aimlapiApiErrorStatus as the one place that reads an HTTP status off a caught error, structurally (not `instanceof AimlapiApiError`) since some callers surface a duck-typed error with a bolted-on `status` instead of the real class; use it at both call sites that previously duplicated (and disagreed on) this check. - Remove aimlapiPaymentSessionId/isAimlapiTopupRunning: both were write-only state (declared with a blank destructure slot, never read), so every setter call scheduled a render for no observable effect. - Switch providerManagerAimlapi.ts's wrappers to `...args` forwarding so an implementation gaining a parameter can't silently get it dropped by a wrapper that still names the old ones positionally. - Stop exporting pollUntilPaid from the aimlapi barrel; nothing imports it through there (topup.test.ts imports it directly from topup.js), so keep it out of the public surface. - Normalize the email key while rebuilding the sign-in key store's collection branch on read, matching the legacy single-record migration branch right above it - a hand-edited or older-build file with a mixed-case key would otherwise be invisible to loadAimlapiSignInKey and mint a duplicate key. * test(aimlapi): cover resetAimlapiCheckoutSession, by-key top-up args, and error edges - resetAimlapiCheckoutSession: refreshes the payment session while preserving a minted key, and is a no-op when there's no key to preserve. - ProviderManager: a low-balance saved key that gets topped up charges the EXISTING key via topUpAimlapiByApiKey (apiKey, non-empty paymentSessionId, empty resumeSessionToken) instead of opening a new passwordless-account checkout - previously only exercised through the default test mock, with no assertion on the call. - The three negative assertions in the top-up progress-frame check tested strings that don't exist anywhere in this GUI (CLI-only or pure invention), so they could never fail; add a check against the real failure copy so a regression that silently fails at that point is actually caught. - CLI: pin the --no-open default (false) when the flag is absent, and cover the non-Error (thrown string) branch of the handler's credential-redaction path - both previously only exercised through the Error/AimlapiApiError branches. * fix(aimlapi): close checkout-state concurrency and exchange-lease races - saveAimlapiTopupState now merges resumeSessionToken like the other retained fields instead of spreading the caller's value verbatim. A caller saves this record at points where its in-memory copy is still empty (right after sign-in, before a checkout session exists); a concurrent peer running the same intent can have already elected and recorded a real token in that window, and the unconditional spread was overwriting it with "", stranding the peer's chargeable checkout. - The exchange lease is sized for a single POST (EXCHANGE_LEASE_STALE_MS, 75s) but a resumed wait-exchange holder can sit in a read-only poll for up to POLL_TIMEOUT_MS (20 minutes) before ever reaching that POST. Without refreshing, a peer would see the lease go stale mid-wait, reclaim it, and risk a second concurrent /exchange on the same one-shot session. Add refreshAimlapiExchangeLeaseAsync and call it every poll iteration. - When a peer finishes /exchange and records the settled key WHILE this process holds the lease and is polling/exchanging, the poll seeing the session flip to 'exchanged' threw a hard failure instead of resuming from that peer's settled receipt. Re-check for a settled receipt before releasing the lease and rethrowing. - claimAimlapiTopupState's abandonExisting no longer drops an already-minted (but not yet paid) existing-account key when overwriting a retained checkout for a different amount/auto-top-up - it now merges apiKey/apiKeyId/model in, matching resetAimlapiCheckoutSession's retain-key pattern. A fully settled (paid + exchanged) credential is refused unconditionally regardless of abandonExisting, since that confirms giving up an UNPAID checkout, never an already-paid one. * fix(aimlapi): guard GUI checkout abandonment and receipt recovery - The abandon-ack gate only armed once a checkout URL surfaced (aimlapiOpenedCheckoutRef), but resolveTopupSession can already elect and persist a resumeSessionToken before that point. Backing out in that window then editing the amount hit claimAimlapiTopupState's generic refusal instead of the same confirm-to-abandon flow. Extend the gate to also cover a persisted (not yet opened) intent. - Persist an existing-account key minted at sign-in into the top-up receipt itself (mirrors the CLI), not just the separate sign-in-key cache, so a restart before settlement can resume from one self-contained record instead of depending on two files staying consistent. - reportSession's terminal branch (a cancelled/expired/dead session) always fully wiped the receipt; mirror the CLI's persistSession, which retains an already-minted key (fresh payment session, dead token dropped) and only falls back to a full clear when there's no key to keep. - Submitting the email screen unconditionally reset the whole onboarding identity, silently abandoning a chargeable checkout on an accidental Esc-back-and-resubmit of the same email. Require the same explicit confirmation an amount edit does when a resumable checkout exists. - "Set up a new key or switch account" only cleared in-memory fields, leaving a durable receipt from an earlier interrupted top-up (this mount's refs were never populated for it, since it may be from an earlier process) to hit the same refusal on the next onboarding attempt with no way to recover short of deleting the file by hand. Force the next claim to override it once. - existingAimlapiCredential() rejected saved-profile discovery whenever the AMBIENT AIMLAPI_INFERENCE_URL wasn't canonical, even for a profile that was itself saved against the canonical endpoint. Narrow the canonical requirement to what it's actually protecting: reading the ambient env key, and sending a saved key to a non-canonical endpoint (the existing per-profile check). - The post-signup success screen claimed a magic link was emailed; this flow is passwordless email-code sign-in, no magic link is ever sent. Point at the dashboard instead. * docs(aimlapi): note the interactive/CLI auto-top-up default mismatch The guided GUI flow pre-selects auto-top-up on; the CLI's --auto-top-up only enrolls when explicitly passed. Left both defaults as-is (auto-top-up is a real billing behavior, not something to flip unilaterally) and documented the asymmetry so it's not a surprise either way. * test(aimlapi): sync on the settled frame before confirming switch-account A fixed sleep doesn't prove the Select's focus actually moved to the second option; on a loaded runner the following Enter could land on "Continue with your saved API key" instead and assert against the wrong branch. Wait for the frame to stop changing, matching the settle-poll pattern already used elsewhere in this file. * fix(aimlapi): stop the exchange poll when a peer reclaims the lease The periodic lease refresh added to pollUntilExchangeSettled discarded its result (`.catch(() => false)`), so a peer reclaiming the lease mid-wait was silently ignored: the poll kept going, returned normally once the session left 'exchanging', and the caller walked straight into the non-idempotent /exchange POST with no ownership check of its own — racing whatever the peer was doing with the same one-shot session. The comment claiming this was safe ("resolves on this function's next outer retry") was simply wrong: there is no outer retry on the success path, control goes directly to the POST. Distinguish a thrown refresh (transient lock contention — best-effort, retry next iteration) from an explicit `false` result (the lease is definitively no longer ours) and bail out on the latter, so the caller's existing catch block re-checks for the peer's settled receipt (or fails the run, requiring a re-run) instead of racing it. * test(aimlapi): require the settle-wait frame to actually differ from before the keypress waitForCondition polls every 10ms; on a loaded runner two consecutive polls can both land before Ink has processed the keypress at all, so the "stable frame" check was satisfied by the unchanged PRE-keypress frame, sending Enter before focus ever moved to the second option. Snapshot the frame before the keypress and require the settled frame to differ from it, not just be internally stable. * fix(aimlapi): elect the retained key atomically, stop blocking Ink on claim - Two concurrent sign-ins for the same intent could each mint their own existing-account key before either save landed, and saveAimlapiTopupState (last-writer-wins) let whichever saved last silently overwrite the other's key on disk while both runs kept using their own in-memory copy. Elect apiKey/apiKeyId first-writer-wins (same as resumeSessionToken already is), re-check the receipt right before minting so a losing run adopts the winner's key instead of minting a second, and re-check again after a save that lost the election so the run's own in-memory key matches what's actually on disk. Apply the same first-writer-wins election to the separate GUI sign-in-key cache (saveAimlapiSignInKey), which had the identical last-writer-wins gap. - The GUI called the sync claimAimlapiTopupState directly from an event handler; its lock retry blocks the whole event loop (Atomics.wait) for up to LOCK_TIMEOUT_MS on contention, freezing Ink rendering, Esc, and SIGINT — exactly what resetAimlapiCheckoutSessionAsync already exists to avoid for the same reason. Add claimAimlapiTopupStateAsync (sharing the same claim logic via an extracted operation function) and switch the GUI to it, making startAimlapiTopup async. recordAimlapiCheckoutSession (the reportSession/onSession path) has the same sync-lock exposure but is called from a callback whose return value AimlapiProvisionOptions.onSession drives synchronous control flow in several places across both the CLI and GUI provisioning paths; making it async is a larger, riskier contract change deliberately left out of this pass. * fix(aimlapi): never pair a new key with a stale or unrelated apiKeyId saveAimlapiTopupState's apiKeyId fallback still read current.apiKeyId even when current.apiKey was empty (the id-without-a-key case) or when state carried a genuinely new apiKey with its own empty-id sentinel, letting a fresh key get silently tagged with an unrelated leftover id. Gate apiKeyId on the same winner apiKey came from instead of falling back to current independently. * fix(aimlapi): stop cross-account key leaks, lease key-minting, keep GUI CAS async - claimAimlapiTopupState's abandonExisting carried a retained apiKey into ANY differing intent, including a switch from account A to account B (the GUI's forceAbandonExisting path). A B-flow restart before the profile write could then initialize from the receipt and call the B checkout with A's credential — crediting A while B's flow saves A's key. Gate the carry-over on the intent's account/key identity (`email`) matching, not just abandonExisting. - The key-choice screen (I am a new user / I already have a key) reset the whole onboarding identity unconditionally on either choice, even when Esc had backed all the way out from the amount screen past an already-opened, still-chargeable checkout. Apply the same abandon-confirmation gate startAimlapiEmailOnboarding already uses. - POST /v1/keys (minting an existing-account key) had no cross-process serialization: two concurrent runs for the same intent could each observe no retained key and both mint, orphaning whichever key lost the first-writer-wins receipt race. Add a key-mint lease (mirroring the exchange lease's acquire/release shape) so exactly one process ever mints; a peer backs off and adopts the winner's recorded key. - The interactive flow already claimed asynchronously, but still called the synchronous saveAimlapiTopupState and recordAimlapiCheckoutSession directly from an event handler and the onSession callback — either could block the whole event loop for up to LOCK_TIMEOUT_MS under lock contention, freezing rendering, Esc, and SIGINT while a payment session is being created. Add async CAS variants and await them; this needed widening AimlapiProvisionOptions.onSession to allow returning a promise, since its return value drives resolveTopupSession's session election. - An ambient AIMLAPI_API_KEY takes the by-key route with aimlapiExistingUsesEnv, so the eventual profile intentionally stays keyless. The settled-receipt save before that write unconditionally copied the env value into aimlapi-topup.json regardless, expanding a secret's on-disk exposure surface for no recovery benefit (a restart re-reads the same env var). Keep an env-backed receipt credential-free. * fix(aimlapi): preserve the key-mint lease across unrelated CAS writes saveTopupStateOperation and recordCheckoutSessionOperation merged the exchange lease but not the key-mint lease added in the previous commit: AimlapiCheckoutState (what every caller spreads checkoutState from) carries neither lease pair, so an unrelated write - persisting the exchange flag, or electing a checkout session - silently dropped an in-flight peer's key-mint lease. A third process would then see the slot as free and mint its own key, reopening the exact double-mint race the lease exists to close. Fall back to the current lease the same way the exchange lease already does. Also: add a future-dated key-mint lease reclaim test mirroring the exchange lease's, and align the default saveAimlapiTopupStateAsync test mock with the real CAS (match on intent + payment id, keep the first writer's resumeSessionToken/apiKey) so it no longer accepts a write the real store would reject. * test(aimlapi): preserve the key-mint/exchange lease in the mocked GUI CAS writes saveAimlapiTopupStateAsync's and recordAimlapiCheckoutSessionAsync's default mocks spread { ...state } as their write's base, same gap as the real saveTopupStateOperation/recordCheckoutSessionOperation had before the previous commit: neither lease pair survived a write whose state didn't carry them (which is every real caller, since AimlapiCheckoutState exposes neither). Fixing the merge alone wasn't enough — the same two mocks' "does this write still belong to this slot" check also compared lease fields as if they were part of the intent identity, so a write that seeded a lease value failed to match the just-claimed record and silently no-op'd instead of persisting anything. Exclude both lease pairs from that comparison too, matching the real matchingStateOrNull (which only ever compares INTENT_KEYS + paymentSessionId). * fix(aimlapi): recover ambiguous key-mint/exchange outcomes before releasing leases createKey and /exchange are both non-idempotent with no server-side retrieval path, so a lost response after the request actually committed left three races: a retry could exchange (or mint) a second time and orphan the first credential, or the CLI's exchange caller would surface a generic network error instead of the accurate "already exchanged, rotate the key" guidance. exchangeKeyWithLease now distinguishes a genuinely ambiguous transport failure of the /exchange POST itself from other doExchange failures (a pre-POST bail on a reclaimed lease, or a definite rejection): only the former re-checks the session status directly, surfaces the already-exchanged error when confirmed, and otherwise leaves the lease held instead of releasing it into a race. mintExistingAccountKeyWithLease applies the same ambiguous/definite split before deciding whether to release its lease. The GUI sign-in flow had an equivalent gap one step earlier: two concurrent code-verification races could each see an empty key cache and both mint before either save elected a winner, so the loser never adopted the winner's key. completeAimlapiCodeSignIn now serializes the cache lookup and mint behind a new email-scoped lease in topupState.ts, so a losing process waits and adopts the winner's cached credential instead of minting its own. * fix(aimlapi): treat caller-aborted mutations as ambiguous and dedupe the transport helpers client.request rethrows a caller-driven abort as the raw abort error instead of wrapping it in AimlapiApiError, so the ambiguous-outcome checks added for createKey and /exchange missed it: cancelling client-side does not stop a non-idempotent POST from completing server-side, but the lease was still released as if the request definitely failed, leaving the door open to a retry racing a second mint/exchange. All three call sites (the checkout-time key-mint lease, the exchange lease, and the sign-in key-mint lease) now also hold the lease when the caller's own signal fired. Extracted the duplicated abortError/sleep/isAmbiguousTransportApiError helpers shared between topup.ts and onboarding.ts into transport.ts so the ambiguity rule can't drift between the CLI and GUI paths. Switched the GUI sign-in flow's cache save to the async, lock-yielding variant and logged its lease-release failures for parity with the other leases. * fix(aimlapi): close six checkout-state races found across the claim, lease, and recovery paths claimAimlapiTopupState's in-progress check only looked at resumeSessionToken/settled/apiKey, so a receipt claimed just before its non-idempotent POST (/v1/keys or /exchange) still looked blank and replaceable to a different intent. A competing claim could overwrite it mid-flight, leaving the in-flight request's eventual CAS save with no matching record to land in and orphaning the credential it was about to mint or exchange. The claim now also refuses (unconditionally, even under abandonExisting) while either lease is live. The sign-in key-mint lease's 75s stale window exactly matched createKey's worst-case duration (60s) plus the async lock's own timeout (15s) for the cache write that follows, with zero margin for anything else. A legitimately still-working holder could lose the lease to a peer moments before its result was cached. It's now refreshed right after createKey succeeds, giving the cache-write phase its own fresh window. ProviderManager's code-verification path called the synchronous saveAimlapiSignInKey, whose lock retry blocks the whole event loop for up to five seconds on contention — freezing Ink rendering, timers, Esc, and SIGINT right after a sign-in. Switched to the async variant, exported through providerManagerAimlapi.ts alongside the other async cache operations. The three session polling helpers typed onSession as returning void and never awaited it, even though ProviderManager's callback is async and starts receipt cleanup before returning. A terminal session (cancelled/expired/ failed, or a dead session) could let the UI reach the amount screen before the durable receipt was actually reset, so an immediate retry still saw the stale resume token and got rejected as "not yet abandoned." Both sides now await through to completion. The confirmed email-switch flow cleared the in-memory checkout intent and fired an un-awaited, error-swallowing state clear, but derived its later claim's abandonExisting only from refs that clear had just wiped — so a slow or failed clear left the user's explicit confirmation unenforced at the claim itself. It now sets the same one-shot force-abandon signal the "switch account" flow already uses for exactly this kind of on-disk, this-mount-invisible conflict. A cached sign-in key that the server had revoked was indistinguishable from one that was merely unreachable: both collapsed into balanceStatus: 'unknown', which re-cached the same dead key and sent the user to manual-key entry with no way back into the guided flow short of deleting local state. A definite 401/403 against a cached (not freshly minted) key now invalidates the stale cache entry and mints one replacement before falling back to the generic unknown-balance path; every other (ambiguous) failure still leaves the cache untouched. Extracted a shared claim/lease-liveness helper in topupState.ts and added regression coverage for each race — including two that hold a mocked createKey/reset call open to prove the competing operation actually waits instead of just asserting on the end state. * fix(aimlapi): clear the stale force-abandon flag on a fresh preset entry aimlapiForceAbandonExistingRef is armed when the user confirms abandoning a checkout during an email switch, then consumed by the next claim. If that claim never runs — the switch's own onboarding fails and the user backs all the way out to preset selection instead of retrying — the flag stayed armed. Re-entering the aimlapi preset with an unrelated email then passed abandonExisting: true on its first claim with no confirmation for that flow, silently overwriting whatever unpaid checkout was still on disk. startCreateFromPreset now resets the flag alongside the other per-flow refs it already clears on fresh entry. Also swapped a fixed 20ms sleep in the cross-intent concurrency test for a signal fired from the held-open /v1/keys handler, so the test can't flake under CI load waiting for the run to reach the point it needs to race. * fix(aimlapi): close the remaining confirmation, cleanup, and lease gaps in checkout state The API-key-choice screen's own confirm-abandon gate (Enter twice to accept "a checkout from this account is still pending") reset the onboarding identity but never armed the force-abandon signal the email-switch and switch-account flows already use. A contended or failed pre-clear left the next claim to hit the CAS's unconfirmed-conflict refusal despite the user having just confirmed abandonment through this exact screen. reportSession('')'s terminal-session handler discarded the persisted intent ref before its reset/clear attempt settled, and swallowed any failure as success. A lock timeout or I/O error then left the durable receipt exactly as it was, but with no ownership left in memory to retry cleanup or to route a later conflicting claim through the normal confirmation gate — the CAS just rejected it outright. Ownership now only drops once the transition actually commits; a failure is logged and the ref stays populated so the existing gate covers the next claim. The sign-in key-mint lease's stale window already had zero margin for its own refresh call's lock wait (up to 15s) on top of createKey's own worst case (60s) and the cache save's lock wait (another 15s) — 90s with nothing left over. Widened it to 150s and lengthened the losing side's patience to match, and stopped silently ignoring a refresh that reports lost ownership: it's now logged for diagnosability even though the save itself stays safe to attempt regardless (first-writer-wins makes a losing write a no-op). Both onSaved completion paths (persistExistingAimlapi and persistAimlapiKey) called the synchronous clearAimlapiSignInKey from Ink's synchronous save callback, whose lock retry blocks the event loop for up to five seconds on contention — freezing rendering, timers, Esc, and SIGINT right at completion, the same class of bug already fixed for the sign-in save path. Re-exported the async variant through providerManagerAimlapi.ts and switched both call sites to fire-and-forget it instead. * fix(aimlapi): stop the flow instead of risking a stranded key on a receipt-write failure /exchange (and the by-key top-up) is a one-shot operation: once it succeeds, the issued key exists only in memory until a durable copy lands somewhere. Both the CLI and the GUI wrote the local recovery receipt right after that, but treated a failure there as best-effort and proceeded straight into the provider-profile write regardless. If the receipt write failed and the profile write then also failed — or the process was interrupted between the two — the key was gone: nothing durable ever recorded it, and a retry can't re-exchange an already-spent session to get it back. Both paths now treat the receipt as a required checkpoint rather than an optional resume aid: a failure here stops the flow with a clear error pointing at the one real recovery path (rotating the key from the aimlapi.com dashboard) instead of silently continuing. This shouldn't cost much in practice — the underlying CAS write already retries substantially on lock contention before giving up, so a failure this deep signals a real problem rather than a transient blip a fallback write would likely have hit too. Added failure-injection coverage for both paths: the CLI test breaks the config directory (a file where a directory is expected) right as the exchange response lands, so the post-exchange save fails deterministically without relying on OS-specific permission semantics; the GUI test mocks the save to reject directly and asserts the profile write is never reached. * test(aimlapi): strengthen receipt-write-failure coverage and document the recovery path Both the CLI and interactive-flow tests for the post-exchange receipt-write failure only asserted the generic error text, which would still pass if the issued key id — the actual recovery handle the error exists to surface — got dropped from the message later. Both now assert the id appears too. The interactive test also confirms the screen stays usable after the error: a retry reaches the amount-submission path again instead of the flow being stuck. Documented the resulting behavior in the setup guide: since the key exchange is one-shot, a receipt-write failure after a successful payment now stops both flows with an error naming the issued key, rather than continuing silently — recovery is manual, via rotating that key on the aimlapi.com dashboard. * test(aimlapi): move to end-of-line before clearing the email field after Esc Rebasing onto current main picked up the input layer's DEL-coalescing fix, which now correctly respects the cursor position for a backspace run instead of dropping it. These three tests backspaced assuming the cursor sat at the end of the retained email text, but cursorOffset is a single state shared across every screen's text field and was last set for the amount screen (its default "25" is 2 chars) — going back via Esc never resets it, so the cursor was actually stuck mid-string. Sending an explicit end-of-line sequence before the backspaces makes the clear correct regardless of where the stale cursor was left. * fix(aimlapi): close six checkout/onboarding gaps from the latest review pass Treats a malformed-but-2xx key-mint response as ambiguous (not proof of failure) so an unusable receipt no longer releases the mint lease and risks an orphaned credential; fences startAimlapiTopup's cancellation to an epoch created before the state-lock await so Esc/unmount during that wait can no longer barge back in; makes the checkout-receipt read fail closed on a permission/IO/parse/schema failure instead of silently claiming over it; completes (or reconciles) a settled by-key receipt instead of stranding it at the post-payment model picker; retires a sign-in mint lease together with its cache entry so it can't resurface as held once the cache is later cleared; and adds a --code-stdin path plus a deprecation warning so the passwordless code no longer has to travel through shell history or argv. * fix(aimlapi): reconcile env-credential receipts and stop endorsing AIMLAPI_CODE as safe reconcileSettledAimlapiTopupStateAsync matched a stale settled receipt by its stored apiKey, but an env-sourced credential's receipt never persists one, so that path stayed permanently stranded; it now matches on the absence of a stored key when the caller is reusing an env credential. Also stops recommending AIMLAPI_CODE as an equivalently safe alternative to the deprecated --code flag, since typing it inline still lands in shell history, and adds a lock-release assertion to the fail-closed receipt-read tests. * fix(aimlapi): await stale-receipt reconciliation and stop overclaiming --code-stdin's history safety Reconciling a stale settled receipt matches by the by-key credential's apiKey, which a genuinely new top-up for that same credential can also produce — firing the reconcile call without waiting for it left a window where a fresh settlement landing in that gap could be swept up by it. Await it before moving on so the two can no longer interleave. Also narrows the --code-stdin messaging: it only guarantees the code stays out of this process's argv/`ps` output, not shell history in general, since that still depends on how the caller feeds stdin. * fix(aimlapi): keep the configured screen locked until reconciliation finishes Clearing isAimlapiKeyValidating before the reconcile await let the aimlapi-configured screen's Select (and its Esc binding) become interactive while that reconcile was still running in the background — it carries no abort signal of its own, so aborting the surrounding controller only stops this flow from acting on the result, not the reconcile itself. That left a window where the user could start a competing top-up for the same credential and have its fresh settlement caught by the still-in-flight reconcile. Both now stay gated on isAimlapiKeyValidating through the whole wait. * fix(aimlapi): fail closed on the sign-in cache, bind env receipts by identity, and commit minted keys Makes the sign-in key cache and its mint lease match the checkout receipt's fail-closed contract: only ENOENT means no record, so a permission/IO/parse failure can no longer be mistaken for "nothing cached, no lease held" and authorize a second createKey call or a concurrent lease acquisition. Reworks reconcileSettledAimlapiTopupStateAsync to match on the by-key checkout intent's non-secret key fingerprint (already carried in the persisted email field) instead of the raw stored apiKey or its mere absence — an env-backed receipt never stores its key, so absence alone couldn't tell two different env credentials' receipts apart, letting one credential's balance check discard another's still-unrecovered payment. Treats persisting a freshly minted existing-account key as a commit point in the CLI's mint-with-lease path: a write failure now stops the flow with a recovery-oriented error and leaves the lease held, instead of continuing with the key only in memory where an interruption before the later checkout/profile save would orphan it once the lease goes stale. Adds a shared isValidAimlapiSignInCode check so both the CLI and the interactive flow reject a malformed passwordless code (empty, non-numeric, wrong length) before it ever reaches verifySignInCode. * fix(aimlapi): reject an array-shaped sign-in cache/lease file instead of degrading to empty typeof [] === 'object' and [] !== null, so readJsonObjectFile's shape check let a JSON array through as if it were a valid store. Both readers then found no matching entries and returned {}, exactly the "no cached key, no live lease" outcome the fail-closed contract exists to prevent — authorizing a second createKey call or a lease acquisition over a possibly-live one. * fix(aimlapi): commit the sign-in key as a checkpoint and retire a completed mint's lease mintOrAdoptSignInKey swallowed a failed cache commit and returned the minted key only in memory — the same non-idempotent-mutation gap already closed for the CLI's mintExistingAccountKeyWithLease. A commit failure now stops the flow with a recovery-oriented error and leaves the lease held, instead of risking the key becoming unrecoverable once the lease ages out and a retry mints a second one. Also retires the checkout key-mint lease in the same save that elects a freshly minted key, mirroring how the exchange lease is already cleared on settle. Without it, backing out of an unpaid checkout and confirming a different amount right away still hit the "minting or exchanging" refusal for the full 75s stale window even though the mint had already completed. * fix(aimlapi): make key-mint lease retirement owner-checked, preserve error causes Retiring the checkout key-mint lease on a successful mint (the previous commit's fix) cleared it unconditionally, with no check that the save still belonged to the owner that acquired it. createKey has no refresh mechanism, so a slow response can let the lease go stale and be reclaimed by a peer before the original owner's save lands — clearing the lease then would drop that peer's still-live one and let a differently-amounted claim proceed as though minting were done while the peer's mint was still genuinely in flight. Replaces the raw saveAimlapiTopupState call in mintExistingAccountKeyWithLease with a dedicated recordAimlapiMintedKeyAsync that takes the acquiring owner and only retires the lease while it's still theirs — mirroring how recordAimlapiSettledKeyAsync already handles the exchange lease. Also adds `cause` to the three recovery-oriented errors thrown on a receipt-write failure, so the underlying persistence error stays diagnosable instead of being replaced by the wrapper message alone. * test(aimlapi): assert a stale owner's minted key is still persisted The reclaimed-peer-lease regression test only pinned the lease bookkeeping, so a variant regression that skipped the write entirely for a non-owning caller (e.g. an early return on lease-owner mismatch) would still pass while silently discarding a real, non-idempotently minted key. Asserts the receipt retains it regardless of who currently holds the lease. * fix(aimlapi): persist and charge the endpoint a manually-entered key was actually validated against persistExistingAimlapi's no-existing-profile fallback saved the profile with resolveEndpoints().inferenceBaseUrl (the current ambient endpoint) instead of aimlapiInferenceBaseUrl (the endpoint this flow actually validated the key and will charge against). The two diverge for a manually-entered key: after "Set up a new key or switch account" resets aimlapiInferenceBaseUrl to the ambient default, draft.baseUrl (what validateAndPersistAimlapiKey actually calls the balance/top-up endpoints with) keeps the OLD profile's endpoint if it differs (e.g. a canonical saved profile while AIMLAPI_INFERENCE_URL currently points at a proxy). Now captures the validated endpoint into aimlapiInferenceBaseUrl as soon as the low-balance branch is reached, and the fallback save uses that state instead of re-resolving the ambient endpoint — so the top-up charge and the persisted profile both follow the endpoint that was actually validated. * fix(aimlapi): reject stale key-mint results, make the exchange checkpoint mandatory, validate lease pairs recordAimlapiMintedKeyAsync previously let a stale owner's delayed createKey result land beside a peer's reclaimed, still-live lease: since no key was recorded yet, first-writer-wins accepted the stale result outright, so the reclaiming peer's own (equally real, non-idempotent) mint got silently discarded once its own save landed — turning one lost credential into two. It now rejects a result whose ownership was already lost when nothing is recorded yet to adopt instead, surfacing a recovery-oriented error naming the issued key id rather than risking a second orphan. The caller now also returns whichever credential is actually durably recorded, not always its own. exchangeKeyWithLease's own settled-receipt commit — the only durable record of a one-shot /exchange result until the caller's later, separate save runs — was only logged on failure. A crash in that window left the paid session exchanged with its key absent from local recovery state, so a retry could only report the session was already exchanged with no way to recover automatically. The commit is now a required checkpoint: its failure stops the flow with the existing recovery guidance and leaves the exchange lease held, exactly as the analogous key-mint checkpoint already does. The receipt schema validated the exchange lease pair but not the newer key-mint one, and even the exchange check only verified each field in isolation — a one-sided pair (an owner with no timestamp, or vice versa) passed either way. A shared validator now enforces both lease pairs are either fully present or fully absent, so a malformed or partial lease can no longer be silently accepted as "not currently live" and let a claim replace it out from under an in-flight mint or exchange. * fix(aimlapi): fail the exchange when the settled-receipt commit no-ops, not just when it throws recordAimlapiSettledKeyAsync silently returned without writing whenever the CAS no longer matched (checkout cleared/reset mid-flight) or no credential could be resolved to settle with. exchangeKeyWithLease only caught thrown errors, so both no-op paths let a successfully exchanged key return with no durable local receipt. The function now returns a boolean, and the caller treats false exactly like a thrown save error. Also fixes recordAimlapiMintedKeyAsync returning the raw untrimmed apiKeyId instead of the trimmed value it actually persisted. --------- Co-authored-by: Lookoff123 <bataryshkinairina@gmail.com> | 1 个月前 | |
Chore/readme cleanup (#1976) * docs: README cleanup, green wordmark header, Trendshift badges Header: the startup wordmark (src/constants/brand.ts half-block art) rendered as a green two-shade SVG (docs/assets/openclaude-wordmark.svg, textLength-pinned so rows align in any monospace font), with the three Trendshift badges (daily/monthly/repository) centered beneath it. Cleanup (536 -> ~430 lines, nothing lost): - Agent routing, maxSteps limits, and GitHub Copilot sub-agent tuning moved to docs/agent-routing.md; headless gRPC server moved to docs/grpc-server.md; README keeps linked summaries. - Build/test/validation commands were repeated in three sections — consolidated into one Development section; Contributing links to it. - New "Meet Your Buddy" section documenting the companion heroes and their /buddy commands; added to What Works and Why OpenClaude. - Star History moved from the header flow down beside Community. - Setup Guides indexes the new docs pages; fixed a missing blank line before Repository Structure and a curly quote. All relative links, image paths, and internal anchors validated. Co-Authored-By: OpenClaude <openclaude@gitlawb.com> * docs: pure-rect wordmark for crisp rendering; drop broken Star History The wordmark SVG previously drew the half-block art as monospace <text>, which rendered raggedly (font-dependent glyph stretching and seams). Regenerated as pure SVG rects computed from the brand.ts wordmark grid — no font dependence, pixel-crisp at any size, same two-shade green split. Star History chart removed: the badge endpoint errors and displays a broken image. Co-Authored-By: OpenClaude <openclaude@gitlawb.com> * docs: render the wordmark at full README column width Co-Authored-By: OpenClaude <openclaude@gitlawb.com> --------- Co-authored-by: OpenClaude <openclaude@gitlawb.com> | 2 个月前 | |
feat: implement Hook Chains runtime integration for self-healing agent mesh MVP (#711) * feat: implement Hook Chains runtime integration for self-healing agent mesh MVP - Add Hook Chains config loader, evaluator, and dispatcher in src/utils/hookChains.ts - Wire PostToolUseFailure hook dispatch in executePostToolUseFailureHooks() - Wire TaskCompleted hook dispatch in executeTaskCompletedHooks() - Integrate fallback-agent launcher with permission preservation (canUseTool threading) - Add safety hardening for config-read errors (try-catch protection) - Update docs with MVP runtime trigger explanation - Add 10 unit tests and 4 integration tests covering config, rules, guards, and actions This completes the self-healing agent mesh MVP by enabling declarative rule-based responses to tool failures and task completions, with fallback agent spawning, team notification, and capacity warming actions. * Update docs/hook-chains.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/utils/hookChains.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: address PR #711 review blockers for Hook Chains - Gate hook-chain dispatch behind feature('HOOK_CHAINS') and default env gate to off - Remove committed local artifact (agent.log) and ignore it in .gitignore - Revert hook dispatcher signature threading changes for canUseTool - Use ToolUseContext metadata hookChainsCanUseTool for fallback launch permissions - Make spawn_fallback_agent fail explicitly when launcher context is unavailable - Add config cache max age and guard map size limits to bound runtime memory - Update docs and tests for default-off gating and explicit fallback failure --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> | 5 个月前 | |
fix(openai-shim): strip store for Anthropic models on OpenAI-compatible routes (#2242) (#2247) | 13 小时前 | |
feat(config): add explicit provider env-file loading (#1668) * feat(config): add explicit provider env-file loading * fix(config): handle escaped quotes in provider env files * fix(config): polish env-file parser review feedback * fix(config): preserve provider env-file precedence * test(config): cover provider env-file precedence * fix(config): preserve provider env-file values * fix(config): allow documented env-file setup vars * fix(config): preserve provider flag precedence | 3 个月前 | |
fix(ollama): preserve chat history with native context (#1805) * fix(ollama): preserve chat history with native context Route Ollama chat requests through the native /api/chat endpoint so OpenClaude can send request-level options.num_ctx instead of relying on Ollama's OpenAI-compatible shim. Default the Ollama request context to 32768 tokens, support OPENCLAUDE_OLLAMA_NUM_CTX and OLLAMA_CONTEXT_LENGTH overrides, and map max tokens/temperature/top_p into native Ollama options. Adapt native Ollama streaming and non-streaming responses back into the existing OpenAI-shaped conversion pipeline, including usage, text, structured tool calls, and tool_use stop reasons. Normalize native Ollama request messages for images and historical tool calls, avoiding OpenAI-only image_url/id/type payload fields in /api/chat requests. Add Ollama context diagnostics, loopback-only ollama ps status checks, regression coverage, and documentation for verifying active context length. * fix(ollama): address native routing review feedback * fix(ollama): restrict loopback host matching * fix(ollama): exclude wildcard bind address * fix(ollama): keep https localhost proxies on chat completions --------- Co-authored-by: jatmn <12479882+jatmn@users.noreply.github.com> | 3 个月前 | |
fix(ollama): preserve chat history with native context (#1805) * fix(ollama): preserve chat history with native context Route Ollama chat requests through the native /api/chat endpoint so OpenClaude can send request-level options.num_ctx instead of relying on Ollama's OpenAI-compatible shim. Default the Ollama request context to 32768 tokens, support OPENCLAUDE_OLLAMA_NUM_CTX and OLLAMA_CONTEXT_LENGTH overrides, and map max tokens/temperature/top_p into native Ollama options. Adapt native Ollama streaming and non-streaming responses back into the existing OpenAI-shaped conversion pipeline, including usage, text, structured tool calls, and tool_use stop reasons. Normalize native Ollama request messages for images and historical tool calls, avoiding OpenAI-only image_url/id/type payload fields in /api/chat requests. Add Ollama context diagnostics, loopback-only ollama ps status checks, regression coverage, and documentation for verifying active context length. * fix(ollama): address native routing review feedback * fix(ollama): restrict loopback host matching * fix(ollama): exclude wildcard bind address * fix(ollama): keep https localhost proxies on chat completions --------- Co-authored-by: jatmn <12479882+jatmn@users.noreply.github.com> | 3 个月前 | |
feat: add repo map codebase intelligence (#1867) * feat: add Codebase Intelligence — repo map with PageRank-ranked structural summaries Adds a new module that builds a structural map of the repository by parsing source files with tree-sitter, building a cross-file reference graph weighted by IDF, ranking files with PageRank, and rendering a token-budgeted summary of the most important files and their signatures. Surface: - RepoMap tool the model can call on-demand, with focus_files / focus_symbols - /repomap slash command with --tokens, --focus, --stats, --invalidate - Auto-injection into session system context, gated by REPO_MAP=1 env var (compile-time feature('REPO_MAP') flag stays off in scripts/build.ts) How it works: git ls-files → tree-sitter WASM parse → extract defs/refs → IDF-weighted directed graph → PageRank → render top files until token budget Files imported by many others rank highest. Common symbol names (get, set, map, value) are down-weighted via IDF. Results cached to disk keyed by (path, mtime, size) — only changed files are re-parsed. Supported languages: TypeScript, JavaScript, Python. Tree-sitter tag queries are inlined as string constants in queries.ts so they ship inside dist/cli.mjs and work after npm install — the .scm source files are kept for readability/Aider attribution but are not required at runtime. A drift-guard test (queries.test.ts) asserts byte-equality between the inlined strings and the .scm source files. Dependencies added: web-tree-sitter, tree-sitter-wasms, graphology, graphology-pagerank, graphology-operators, js-tiktoken. * fix(repomap): invalidate rendered cache on file edits + Windows test fix - computeMapHash now folds per-file mtime+size into the cache key so a source edit (without changing the file list) no longer returns the prior rendered map. Adds a regression test that edits a file and confirms the second build reflects the new symbol without manual invalidateCache(). - queries.test.ts byte-for-byte drift guard normalizes CRLF -> LF when reading the .scm source so Windows checkouts pass. .gitattributes also pins *.scm to LF on future checkouts. - Externals: declare web-tree-sitter, tree-sitter-wasms, graphology*, and js-tiktoken in scripts/externals.ts so build validation passes. * fix(repomap): expand directory focus paths * fix(repomap): satisfy deadcode check * Fix repo map review findings * Resolve remaining repo map review findings * fix(repomap): address review findings * fix(repomap): address review findings * fix(repomap): resolve smoke and review follow-ups * fix(repomap): preserve cached tag order * fix(repomap): resolve review follow-ups * fix(repomap): satisfy query promise lint * Fix repo map context timeout cleanup * fix: address repo map review findings * fix: cancel timed-out repo map context builds * fix(repomap): preserve git file path whitespace * fix(repomap): handle graph and parsing edge cases * fix(repomap): preserve shell token positions * fix(repomap): respect configured cache home * fix(repomap): address review findings - Add explicit 10000ms timeout to the feature-flag-off context test to avoid cold-import flakes. - Add --focus-symbols flag to /repomap and forward it to buildRepoMap, matching the RepoMap tool. - Add parsing/command tests and docs coverage for --focus-symbols. --------- Co-authored-by: gnanam1990 <gnanasekaran.sekareee@gmail.com> | 2 个月前 | |
feat(skills): add verify for revocations and eyebrow drift checks (#2215) * feat(skills): add verify for revocations and eyebrow drift checks * apply code reviews fixes * fix(skills): read execute bits through the fs seam, skip symlink tests where unsupported * fix(skills): check X_OK through the fs seam, report symlink tests as skipped | 27 天前 | |
feat: smart auto-routing (per-turn simple-vs-strong model selection) (#1734) * feat(smart-routing): add smartRouting settings schema and reader * feat(smart-routing): resolve role keys to a SmartRoutingConfig * feat(smart-routing): wire per-user-turn routing into the query loop Classify once per user turn (transition===undefined), pin the decision in a loop-local, and apply the model-only route before the blocking-limit math. Enforce the org allowlist by calling isModelAllowed directly (coerce disallowed to strong; disable for the session if strong is also disallowed). Strip thinking history on a model change only under the provider gate (preserve-reasoning providers are left untouched). Export stripThinkingBlocksIfProviderAllows. * feat(smart-routing): add routed-error fallback to the strong model A simple-routed turn whose model call hits a retryable error retries once on the strong model, reusing the existing attemptWithFallback retry loop. Aborts and 4xx client errors propagate. Adds a session routing tally (simple/strong counts and simple->strong escalations) for the observability surface. * feat(smart-routing): add /smartroute command and env defaults /smartroute shows status and sets/toggles the simple and strong roles from agentModels keys, warning when the simple model is not first-party-cheaper than the strong one. OPENCLAUDE_SMART_ROUTING(_SIMPLE/_STRONG) provide startup defaults; an explicit settings block overrides env. * feat(smart-routing): show routing summary in /cost Appends a session routing summary (turns simple/strong, simple->strong escalations) to /cost, with an estimated-savings line gated on first-party pricing and annotated unavailable for unknown third-party pricing. Per-turn cost is already attributed to the routed model via the existing per-model breakdown. * fix(smart-routing): re-pin to strong after a routed-error fallback Without this, a turn's later continuation passes re-applied the pinned simple model after a fallback, re-triggering the same failure each pass. Re-pinning to strong keeps the rest of the turn on the recovered model. * fix(review): provider-swap guard, tally reset, notice-storm, env docs - Add the KTD6 provider-swap guard: drop the per-turn routing pin when a mid-turn provider-fallback swap changes the active provider, so the old provider's model id is not replayed at the new endpoint (adversarial P1). - Reset the routing tally in resetCostState() so /cost does not show stale cross-session counts. - Don't emit the disabled-for-session notice on every turn when no sessionId is available (suppress instead of storm). - Document OPENCLAUDE_SMART_ROUTING* in the openaiShim env-var header. - Add tests: provider-swap-safe pin, undefined-session silence, /smartroute strong arm and no-value guard. * docs(smart-routing): document /smartroute, settings, and env vars Register /smartroute in the web command catalog, add the smartRouting setting and OPENCLAUDE_SMART_ROUTING* env vars to the configuration reference, add a docs/smart-routing.md usage guide, and link it from the README. * fix(review): clear tally on /login, extract+test swap predicate, cap disabled set - /login used the raw bootstrap resetCostState, leaking the routing tally across an account switch; switch it to the cost-tracker wrapper. - Extract the provider-swap drop check as a pure, tested shouldDropPinForProviderSwap() and use it in the query loop. - Cap the disabledSessions set so a long-lived host can't grow it unbounded. - Document the 404/429 retry-by-design rationale; add tests for it. - Clarify the routedFallbackUsed per-turn scope and the apply-after-guard comment; document cross-provider role rejection and the re-enable path. * test(smart-routing): make allowlist tests robust to cross-file module mocks The decideTurnModel allowlist tests spied the global settings singleton, which let another file's leaked mock.module of modelAllowlist (agent.test.ts) flip isModelAllowed out from under them in the full suite. Spy isModelAllowed directly and restore it in afterEach so the tests are deterministic regardless of suite ordering. * fix(smart-routing): address CodeRabbit review and green CI - index.test.ts: pin the allowlist in the three happy-path decideTurnModel tests so they no longer inherit a leaked cross-file isModelAllowed mock (the CI test failure) - smartroute/index.test.ts: narrow the LocalCommandResult union via an expectText helper instead of reading .value off the union (the CI typecheck failure) - conversationRecovery.ts: route deserialize's thinking-strip gate through stripThinkingBlocksIfProviderAllows, removing the duplicated provider detection - conversationRecovery.test.ts: replace the two as-any fixtures with a shared typed factory * fix(smart-routing): scope cost claims to first-party reference pricing Smart routing's savings estimate and "simple isn't cheaper" warning were derived from the static first-party MODEL_COSTS table via getKnownInputCost, with no knowledge of the active provider, gateway, or account pricing. For a multi-provider user whose model ids happen to exist in that table but bill differently, the /cost summary and /smartroute warning stated a savings figure as if it reflected what they are actually charged. Narrow the copy instead of inventing provider-aware pricing the code cannot verify: the /cost line, the /smartroute warning, and docs/smart-routing.md now label the numbers as first-party reference pricing and note the active provider may bill differently. Tests assert the qualifier on every reworded branch so it cannot silently regress. No routing logic changed. * fix(smart-routing): clarify simple role wording * Fix smart routing review findings * fix(smart-routing): honor env roles and non-text turns * test(smart-routing): cover non-text skip path --------- Co-authored-by: jatmn <the@jat.mn> | 2 个月前 | |
fix: avoid file suggestion OOM on large repos (#1074) * fix: avoid file suggestion OOM on large repos * fix: handle ignore scope and abort semantics in file suggestions * test: stabilize rebased branch CI verification * test: make proxy env cleanup windows-safe * test: preload file suggestions module in setup * fix: keep file suggestions lazy on startup * chore: address final review nits | 3 个月前 |
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 4 个月前 | ||
| 1 个月前 | ||
| 14 天前 | ||
| 14 天前 | ||
| 14 天前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 5 个月前 | ||
| 13 小时前 | ||
| 3 个月前 | ||
| 3 个月前 | ||
| 3 个月前 | ||
| 2 个月前 | ||
| 27 天前 | ||
| 2 个月前 | ||
| 3 个月前 |