Skip to content

v1.0.668

Choose a tag to compare

@github-actions github-actions released this 30 Oct 01:30
· 51 commits to main since this release
a5a4c6f
  • feat(network): channel payload sender block — app members' channel
    messages carry {"text": …, "sender": {"name": …, "id": …}} (public
    base64 JSON and keyed fanet1 plaintext alike), so DAP agents can
    attribute the HUMAN author behind the fa_network relay's hub id. The
    harness unwraps it tolerantly (raw text passes through), rides the
    name as AgentMessage.fromName through the fabric, and the CLI
    steering line greets people again: from Uladzimir (<relay id>) in #channel: hello instead of from 3cfaee1aa054fe6e…. The app falls
    back to the payload sender for bubble names when the envelope carries
    none; DMs also name hub peers from the whois cache. Legacy payloads
    without the block decode exactly as before.

  • feat(gh-1956): reply auto-spill — oversized child reply / agent_message
    payloads spill to an agent:// artifact automatically; the parent receives a
    bounded excerpt + pointer + completeness marker with char AND byte counts.
    Manual chunking degrades to a documented fallback, restorable across parent
    resume via durable deliverable_artifact records, behind the
    subagents.replyAutoSpill kill switch (default on; honored on the CLI and
    the app host alike). Rework round: the salvage hook (output-truncated
    deliverable calls) restores the durable ids before allocating its own
    (no post-resume pointer shadowing), durability losses emit a
    [deliverables] diagnostic line, the app host honors the kill switch, the
    spill-reach gate is renamed spillAppliesTo (was spillApliesTo), byte
    counts are encoded once per spill, and DeliverableRecord.artifact(bytes)
    is optional again so external constructors keep compiling.

  • fix(#1947): schedule_message from the orchestrator no longer misroutes
    into a finished subagent's mailbox. A background shell job started by a
    CHILD registered its settle continuation inside the child's subagent
    zone; when the job settled after the child's run ended, the CLI's
    settle-wake (_onShellJobSettled → _startRun) started MAIN's reaction
    run still carrying the finished child's activeSubagentId(), and every
    identity consumer of that run misattributed it — the reported shape was
    a self-reminder scheduled "in /" that auto-resumed the
    completed child (duplicate-release risk). Three layers: the
    ShellJobRegistry now binds its settle continuation to the
    construction-time (boot) zone (_bootZone.run), both hosts'
    scheduleSenderMailbox wiring trusts the zone scope only while the
    child is genuinely in flight on the session executor
    (TaskExecutor.isInFlight), and the tool response names the RECIPIENT
    the record carries — an explicit to: used to be shadowed in the reply
    by the sender's mailbox, which read as "the override was ignored" even
    when it was honored. E2E repro drives the real AgentCli
    (test/cli/agent_cli_schedule_recipient_test.dart) plus a registry-level
    zone pin (test/tools/shell_job_settle_zone_test.dart).

  • fix(gh-1948): task_observe works on a RUNNING background subagent
    instead of crashing with an uncaught SessionException on the
    <parentSessionId>/<agentName> placeholder. Three legs: the executor
    flushes each child's transcript incrementally at every turn boundary
    (the JSONL exists and grows mid-run — also crash-resilience for
    aborted children), task_observe resolves a placeholder handle's real
    session file from the sessions tree (the same agent: subagent /
    parent: / id: header scan the boot adoption uses) and heals the
    handle onto the found path, and every read failure degrades to a
    structured "transcript unavailable" / "no transcript available yet"
    message instead of an uncaught tool error. The child-transcript reader
    rides the transient-ENOENT retry (issue #427) — it reads while the
    executor appends.

  • fix(#1941): the selected CodeMie project now rides EVERY model request.
    The header is baked into Model.headers only by the connect-time
    switches (gh-1809), so every other rebuild silently dropped
    x-codemie-project — and the backend bills a header-less request against
    the user's DEFAULT project (codemie-code header-injection.plugin.ts
    contract): the reported symptom is limit-exhausted from the exhausted
    default project despite picking a budgeted one. Rebuilds now re-resolve
    the project from the saved entry through one helper
    (projectHeaderForBaseUrl): the boot folder-state restore (JWT entries
    previously booted header-less — the SSO cookie patch was the only boot
    carrier), the session-resume re-apply (_buildRestoredModel), the
    roles-chain failover (ModelRef.project, mirroring the #964 authHeader
    leg), the roles-mode /model + settings-flow pins, and the generic
    /provider switch. The models list (/llm_models, SSO + JWT + guided
    setup + entry dispatch) and the quota adapter (dark until pinned) scope
    themselves to the same project, so display and billing can never diverge.
    Tests pin the header VALUE on the wire (real adapter over a fake
    endpoint) for the restored and failed-over models.

  • feat(network): private networks created outside the app surface after
    sign-in. GET /api/networks/mine (fa_network#16) is synced into the
    wallet after OAuth/dev sign-in and both restore paths — best-effort,
    silent on older backends (404) and any failure, and never destructive.
    Synced memberships keep the server's name/class, preserve local
    passwords/join facts, and land without a password (the endpoint never
    returns one) until the first join. Entering such a network — sidebar,
    ⌘K/⌘3-9, or a restored selection — opens the join sheet prefilled with
    the network id instead of a resume error or endless spinner; joining by
    name now keys the wallet/session by the echoed canonical id and migrates
    legacy name-keyed rows (password + channel keys) instead of duplicating
    them. A malformed /mine row is skipped, not the whole list.

  • fix(gh-1931): one render per tool call on the non-TUI console. Line mode
    and the headless -p log printed every executed call twice — a start row
    (• bash · cmd, no elapsed) followed by the end row (✓/✗ bash · cmd 0s). The duplicate start-row print is gone from _onToolExecutionStart;
    the end row (command, status glyph, elapsed, failure news) is the single
    render per call. The TUI keeps its running card + settled card, start
    bookkeeping stays (_toolStarts feeds the end row's detail/elapsed, the
    waiting coordinator still arms per-call liveness), and persisted
    artifacts (session JSONL, trajectory records) are untouched —
    rendering-only change. Long foreground calls keep the gh-1055 liveness
    reminders past waiting.toolLivenessSeconds (default 60s).

  • fix(gh-1929): esc during the pre-flight auto-compaction no longer crashes
    the CLI. The compaction layer's deliberate abort (#1085) used to escape
    _runPrompt outside its guarded body and reach the root zone handler via
    an unlistened whenComplete chain — fa crashed: CancelledException +
    crash.log + a lost session view. The pre-flight window (submit on an
    over-window transcript) now translates the cancel into a user-cancel
    outcome: neutral dim submission cancelled receipt (never error:), the
    TUI composer hands the typed text back (sendInputText) so a resubmit is
    one Enter away, headless exits 130 with the same receipt, and the next
    prompt works (the funnel's local-trim recovery or a retried compaction).
    Cancels DURING the run — in-flight relief, settle funnel — keep their
    pinned loud-abort surfaces, and the post-run window keeps its
    compaction interrupted receipt via the shared _renderUserCancel
    helper.

  • feat(1923): stall-proof steering. Steering arriving while the loop awaits
    an UNPRODUCTIVE provider submit (zero bytes, or silent past the steering
    stall window) now cancels the attempt via a SteeringInterruptFire
    cancel, injects the pending steering into the rebuilt request, and
    resubmits — the #1916-class submit stalls can no longer queue user or
    agent mail invisibly. Productive streams keep next-boundary semantics;
    the interrupt never consumes a retry attempt, and the run latch re-arms
    in place so watchdog replays and user aborts keep working. task_resume
    caps its foreground wait (like task_send) and continues in the
    background; video polling honors soft-yield checkpoints (remote render
    never restarted, honest hand-back text) and media generation honors run
    cancellation; SLO-breached undelivered child mail surfaces in
    task_status (UNDELIVERED … past the 2s delivery SLO); the TUI prints
    steering queued — provider stalled Ns, interrupting to deliver plus the
    delivered ack.

  • feat(1916): image-heavy submits no longer livelock — byte-based
    images.maxBytesPerRequest (default 8MB, layered onto the count cap;
    /images names both drop reasons), legacy text-embedded data-URL
    screenshots in history fold into the once-only registry instead of
    replaying raw forever, stall watchdog windows scale with the outbound
    payload (capped at 2x; explicit connect clamps untouched), abandoned
    connect attempts answering during the retry backoff are adopted, and
    repeated identical-payload stalls fire ONE actionable /compact +
    image-budget advice. TUI stall banner + docs/images.md carry the
    image-heavy playbook.

  • fix(gh-1912): Add an agent → DAP now turns a retired hub's unknown
    enroll_create response into an actionable upgrade message instead of
    exposing bad_frame. Independent enrollment never falls back to the
    legacy same-name credential rotation endpoint, and existing credentials
    remain untouched on failure.

  • fix(gh-1913): Add an agent is usable on narrow phone screens: scope and
    format choices wrap without per-character labels, enrollment controls
    stack at full width, and actions remain reachable by scrolling. Desktop
    keeps the segmented layout; phone/desktop light/dark goldens guard both.

  • fix(gh-1825): DAP agent enrollments carry a stable, independent
    agentInstance through app/CLI bundles, persistence, direct boot and
    reconnect. Independent same-name agents keep distinct identities and stay
    online together, while legacy bundles retain their project-scoped identity.

  • feat(gh-1825): DAP private-channel sends now use self-describing
    fanet1 frames (sender X25519 key in the frame, channel-bound AAD) via
    fa_hub_client 0.2.11, so fa_network app members decrypt them without
    a whois/senderKey round-trip. Inbound channel frames open fanet1
    first and fall back to legacy dap/1 payloads. Verified live in both
    directions between a headless DAP agent and the Flutter app.

  • feat(gh-1825): the network offline banner now has a Reconnect now
    action which restarts the socket and reloads the channel/member
    snapshot. Fresh sockets also clear a stale offline flag (the server
    sends no historical drain to a socket that was away during recovery).
    AgentNetworkController.start stops after disposal instead of
    notifying a dead controller.

  • feat(gh-1825): the roster sheet shows access-scope badges (whole
    network vs N channels, decoded additively from Member.access in
    snapshots and presence.changed) and owner/admins can drop an agent
    (DELETE /api/networks/{id}/agents/{agentId} with confirmation; the
    server tombstone keeps it out of future snapshots). Guests and store
    members are removable too via DELETE /api/networks/{id}/members/{id}
    (sessions revoked server-side); owner/admin/self rows stay protected.

  • feat(gh-1825): channel/network management in the app — rename a channel
    (PATCH), delete a channel or an owned network (REST DELETE with
    confirmation and state/navigation cleanup), and unnamed CLI/hub-created
    channels show a short friendly label instead of the raw 64-char id.

  • feat(gh-1825): pasting a DAP invite no longer demands a passphrase on
    macOS — importing NEW channel secrets auto-provisions a random
    passphrase into the per-channel Keychain item (decryption of existing
    envelopes never auto-generates; env/prompt/headless behavior unchanged),
    and passphrase warnings now name the concrete recovery (re-paste the
    invite to re-encrypt, or set DAP_KEY_PASSPHRASE).

  • feat(gh-1825): the network online counter opens a live-first roster
    sheet (names, member class, presence), and the channel composer offers
    roster-filtered @-mention autocomplete — the draft keeps the literal
    @DisplayName for DAP agents and the envelope carries the resolved
    member ids. On the CLI side the DAP channel notice names the agent's
    own identity (display name + agentId), and inbound channel mail that
    @-mentions that name (case-insensitive, word-boundary — no email or
    @name2 false positives) is marked "[mentions you — you are @name]".

  • fix(gh-1825): the app's network roster no longer reads as permanently
    empty ("0 online"): fa_network sends roster.snapshot with an OBJECT
    payload ({members: [...]}) and the app only accepted a bare list, so
    every snapshot silently decoded to zero members. Both shapes are now
    accepted, with a production-shape regression test. The DAP paste hint
    names web alongside CLI/macOS/iOS (plain-web join is supported).

  • fix(gh-1825): resumed CLI sessions rejoin their DAP channels and
    receive channel mail again — session replay now re-arms membership
    synchronization (plugin session-ready listeners on boot/resume and
    in-process switches) and idle channel probes complete deferred joins
    before inbox peek/drain. The post-replay sync also dials the
    membership's OWN hub with its enrolled credential before joining:
    a resumed session-scoped agent used to stay on the zero-config local
    URL forever, and because a disconnected fabric probe short-circuits
    before the sync, the remote dial never happened — the session-ready
    listener now drives it directly (throttled to one attempt per 10 s).
    App channel history no longer flashes raw
    payloads on first open: decoding waits for startup's channel listing
    instead of caching an undecoded guess until a manual switch.

  • feat(gh-1825): the fa app consumes the same DAP enrollment block as the
    CLI — paste it into the agent chat composer (macOS/iOS/Android; the
    smart-paste .txt chip path included) and the channel join happens
    without the block ever reaching sendText, analytics, or session
    records. Native joins ride the live agent-fabric hub repository:
    per-project scoped identity, invite pub/priv import, keyless public
    channels, reconnect-persisted membership in a dedicated secure
    enrollment store (Keychain/Keystore where available). Harness additions:
    HubMessagingRepository.joinChannel/waitForConnection with
    rejoin-before-drain on reconnect, keyed channel decrypt
    (hubDecryptPayloadWithKeyPair), public showcase payloads, and
    HubBearerTransport (native Authorization header or the browser
    ?token= equivalent; authed dials never stringify transport errors
    into logs). Plain web joins through the browser
    WebHubTransport (?token= bearer equivalent, IndexedDB identity);
    the browser extension rides the service worker's existing DAP client
    (hub.join op), never a panel-local second connection. Add agent now defaults to the
    DAP tab with a paste-into-chat hint; web gains ai-native provider
    sign-in through the existing popup/callback OAuth flow.

  • fix(fa_network#4): the app inserts hub agents arriving via
    presence.changed into the roster (additive displayName/class fields)
    instead of dropping them, so the online counter no longer sticks at 0
    when an agent comes online after roster.snapshot; legacy frames
    without those fields create no phantom members. roster.snapshot/REST
    contents are logged for decisive presence debugging.

  • feat(gh-1825): /network + scoped DAP channel membership — join by
    paste (invite url / url+keys / bare channel id / whole fa dap import
    command) with session/project/global scopes, live join without
    restart, per-scope leave + copy invite, and the composer-paste
    shortcut into the same wizard. Session scope rides a
    dap_channel_membership custom record (leave tombstones, branch
    resume); project scope is .fah/dap.json (committable, dapkey1
    PBKDF2+ChaCha20-Poly1305 encrypted keys); global keeps the legacy
    ~/.dap files. Headless CI: DAP_CHANNELS_JSON (+DAP_HUB_URL,
    DAP_CLIENT_SECRET, DAP_KEY_PASSPHRASE) joins at boot with zero
    prompts, plaintext or encrypted privs mixed. Passphrase chain:
    env → per-channel macOS Keychain item → one prompt with save default.

  • feat(gh-1844): media slots never fall back to the main connection
    implicitly — SDK/CLI, app store, and the JS bridge resolve a slot
    without an override as UNCONFIGURED with an actionable
    "configure the slot" error; the main connection serves a slot only via
    the explicit per-slot useMainConnection: true opt-in (YAML
    models.slots.<slot>.useMainConnection / app's "Same as main
    connection" pick, preserved through media_models.json). Image and
    video generation accept multimodal input (text + reference images:
    sandbox paths, http(s) URLs, base64) on capability-declaring
    OpenRouter endpoints (input_references + declared/probed
    input_modalities gate; unsupported endpoints fail before any network
    call); the OpenRouter video job API is a first-class video dialect
    (submit → poll → mp4). New decide tool: structured decisions via
    OpenRouter's Decisions API (/v1/systemone) — bool/choice/score
    questions with calibrated probabilities, structured error results, no
    streaming.

  • feat(gh-1834): the FinalizeGate contract counts a parity/conformance
    claim as verified only with negative + combination self-checks — new
    numbered step 2.6 in prompts/cli/finalize_gate.md (refused
    incompatible/mutually exclusive options, empty/corrupt inputs, boundary
    values, must-NOT-work cases, alongside happy paths; a blanket
    "everything matches" is not verified). Universality: abstract example
    only, no benchmark/task/flag names in the prompt; the step is
    conditional, so tasks with no conformance claim see no gate change.
    Ledger/verdict wire formats unchanged; validated end-to-end by the
    scripted IT-4 replay (real CLI + real bash) whose persisted
    task_ledger carries the rejection rows.

  • fix(gh-1832): release v1.0.635 published without fa-extension.zip — the
    landing-page download 404'd. The extension's only producer (the daily
    build-macos.yml build-extension leg) derives its tag as "latest+1 at its
    own dispatch moment"; on 2026-10-24 the v1.0.635 tag was cut by
    auto_release before those legs ran, so the extension (and the DMGs)
    landed on an orphaned v1.0.636 draft while the tag train published
    v1.0.635 — every leg green, nothing asserted the published asset set.
    Now scripts/release_required_assets.txt is the single source of truth,
    scripts/assert_release_assets.sh gates the release-provenance publish
    flip (a missing asset leaves the release a draft and fails the run
    loudly) and a new release-asset-smoke ci.yml job post-publish probes
    every landing-page release link + the full required set (red on non-200).
    Remediation: the exact-tag fa-extension.zip (built from the v1.0.635
    tree by run 40064876732) was uploaded to release v1.0.635 — the landing
    link serves 200 again.

  • fix(gh-1820): the arm64 DMG leg's stale size floor is reseeded onto the
    shipped v1.0.634 shape — run 4004758753 (post-#1815 SPM-lane adoption)
    passed the min-platform gate and the full build, then failed the
    size_manifest.py +5% budget on App.framework/App +5.1%
    (25,020,112 → 26,284,144, the accumulated Dart AOT of #1793/#1794/
    #1796/#1805/#1806); the committed scripts/size_budget/fa-macos-arm64.tsv
    was still the pre-#1096-diet floor (TOTAL 210.5 MB vs the artifact's
    116.4 MB), so the documented intentional-growth remediation applies:
    the floor is now the exact --reseed output for the failing run's
    artifact manifest (26 rows ≥ 64 KiB + TOTAL 116,386,369, from the run
    log's printed table — the zip never uploaded past the gate), locking in
    Fa 23.4→18.6 MB, quickjs_bridge 950→844 KB; new
    test/ci_1820_macos_arm64_size_floor_test.dart gates the shape green
    through the REAL size_manifest.py check and pins the committed floor
    byte-identical to a --reseed round-trip.

  • fix(gh-1814): the desktop DMG lane ADOPTS the SwiftPM lane — Flutter
    3.47.7 removed the FLUTTER_SWIFT_PACKAGE_MANAGER disable knob
    (flutter/flutter#193978), so the gh-180 pure-CocoaPods pin became a no-op
    and the min-platform gate (which forbade any SwiftPM state) failed both
    DMG legs on every pinned-3.47.7 run (39991663029, 40022900373); the
    #1813 escape (pin 3.47.4) is unreachable on the ~60kB/s runner link.
    scripts/assert_macos_min_platform.sh now ASSERTS the SPM lane instead:
    SwiftPM state expected after flutter build macos --config-only,
    generated package floor ≥ the app floor (the silent bump must not skip —
    the original gh-180 killer), no SwiftPM product above the app minimum,
    vendored inappwebview manifest floored at 14.0 if it ever reappears.
    Floors were audited and need no raising: Runner pbxproj 3×14.0, Podfile
    :osx, '14.0', generated floor 14.0 on 3.47.7, vendored podspec 14.0
    (its Package.swift stays deleted — the plugin rides the CocoaPods half of
    the hybrid lane via Podfile.lock). The dead env pin left the build-macos
    job env; the SDK pin reverts #1813 back to 3.47.7. The ci.yml macOS debug
    leg gets the same treatment (gh-1814 review): its
    FLUTTER_SWIFT_PACKAGE_MANAGER=0 pin — a silent no-op on the floating
    3.47.x stable it resolves past the knob's removal — is removed and the
    lane comment corrected; the leg rides SPM enabled as it already has been
    (run 399572012 green).

  • fix(gh-1805): modifier chords no longer type their base letter into the
    composer while the slash menu is open — the vendored dart_tui decoder
    puts the base letter into the keystroke's text for control bytes
    (Ctrl+S → ctrl+s with text s), and the slash-menu edit cluster was
    the one insert surface missing the _isCommandKeystroke guard, so a
    chord pressed mid-menu poisoned the typed prefix (/memory landed as
    /smemory). Plain letters and Shift+letters still type and filter.

  • feat(1515): ONE model-limits settings surface — the settings hub's
    standalone "Model parameters" row is retired and bare /model-edit
    (TUI) aliases into /settings → Model capabilities, pre-focused on the
    active model. The caps flow gains the model-edit preset ladders +
    Custom… free input for context window / max output (a preset pick
    writes models.overrides AND rebuilds the active model on the spot —
    the old flow's live-mutation semantics, now durable), every field row
    shows the current EFFECTIVE value with its source layer (pinned /
    role override / catalog), the thinking row is an omp-style selector
    (only levels the model supports, per-rung budget metadata, current
    level preselected), and the hub row describes the effective summary
    (200K ctx · 16K out · thinking off) instead of a pin count. The
    scripted /model-edit <field> <n> form and the line-mode limits print
    are unchanged.

  • feat(gh-1746): opt-in session disk retention — sessions.retention in
    ~/.fah/config.yaml caps the sessions root and sweeps whole session
    units (transcript + sidecars) oldest-last-used-first at boot, exempting
    live, named, young, and the current session; nothing is EVER
    auto-deleted without the section (byte-identical legacy boot). Adds
    fa session clean [--dry-run], the /sessions + fa session list
    usage footer under a cap, and header-cwd/path-escape guards so a
    planted session header can never steer a delete outside any root.

  • fix(gh-1763): a bash tool command running broad process kills can no
    longer signal its own shell or the harness. Scripts are delivered from a
    per-exec private temp file (sh -c '. "$1"' sh <script>) so the wrapper
    argv never carries the script body (pkill -f <pattern> cannot
    self-match its own sh -c; $0/$#/exit codes/set -e/stdin stay
    identical), a PATH shim for pkill/killall plus a kill() prelude
    guard exempt the harness pid chain + process group (exported as
    FA_PROTECTED_PIDS) and the invoking shell from the child's own kills
    while legitimate victims (stuck test runners, background servers) are
    still terminated, and headless SIGTERM aborts now log
    fa: SIGTERM received … so a child-issued kill never masquerades as a
    user Ctrl+C (exit code contract unchanged: 130 graceful, 143 escalate).

  • fix(gh-1778): the release gate no longer reds a fully-green shard when
    GitHub's cache service degrades. v1.0.621's Core tests (shard 2/4) was
    killed by its own timeout-minutes: 12 with 2535/2761 tests green — both
    cache steps had stalled ~133 s on keys that existed (degraded service,
    not eviction), cold pub get + native-asset hook builds delayed the
    first test to ~5 min, and the shard needed ~14 min against a budget
    sized for the warm ~8.8 min wall. The job budget is now 20 min (sibling
    test-leg band), and both test-core cache steps are best-effort
    (timeout-minutes: 2 + continue-on-error: true) — a failed restore is
    slower setup, never a red shard. test/ci_core_shard_timeout_guard_test.dart
    pins the budget and the best-effort cache contract (RED-first on the old
    YAML).

  • feat(#1761): the 32KB project-context budget never cuts silently again.
    A file truncated to its head excerpt now carries an extended
    <!-- From: path — N bytes, only the first M shown --> marker plus an
    explicit MUST-read-the-full-file instruction (offset/limit/ranges,
    prompt copy in prompts/context/project_context_read_full.md); a file
    dropped entirely leaves a one-line tombstone naming its path instead of
    vanishing; and the user gets a dim boot line per truncated/dropped file
    (CLI line mode prints directly, TUI re-prints after the banner, the app
    logs through its AppLog boot-notice seam). New projectContext: config
    section: maxBytes (default 32768) — 0 disables the section entirely
    and reads no files (fast boot); unknown keys are tolerated with a note.
    Leaf-first allocation is unchanged.

  • fix(gh-1757): the Android build no longer re-enters the sdkmanager
    download roulette for CMake. v1.0.619's build-android leg died at
    :jni:configureCMakeDebug[arm64-v8a] — the :jni module (transitive
    jni package via path_provider_android) wires externalNativeBuild.cmake
    with no version, so AGP auto-installed its default cmake;3.22.1
    mid-configuration and one corrupt archive ("Archive is not a ZIP
    archive") red'd the leg, the aggregate Quality gate, and the
    Advance-vpinned-marker job. The gh-1655 pre-install action now also
    pre-installs cmake;3.22.1 with bounded retry + idempotent skip (same
    hardening the NDK path already carries); the guard test pins the new
    AC6 assertions and the AGP-version coupling (AC7).

  • feat(#1750): per-model image caps — the outbound request never carries
    more unique images than the target model accepts. Model gains an
    optional maxImages (catalog-seeded: gemini-4-argon: 10, measured);
    images.perModel.<modelId> config overrides it (strict images:
    section, user truth wins); the registry resolves
    perModel > catalog > images.maxPerRequest (first present wins, the
    issue-#1554 shape). A Google-shaped too many images … maximum allowed for model <id> is <n> 400 is classified (image_overflow.dart, the
    overflow.ts twin), latches the learned cap for the (provider, model)
    pair for the run, and re-sends ONCE through the registry at the
    smaller cap — current-first-then-newest drops with the existing
    never-silent notices naming the learned cap; a second refusal
    surfaces exactly as today. JSONL is never rewritten;
    images.registry: false keeps the byte-identical legacy shape
    including the recovery-off behavior.

  • feat(gh-1760): oversized user-message intake spills to a file instead of
    blowing the context window. Any incoming user message — headless
    -p/positional, TUI first/mid-session, steering/follow-up drain,
    external inbox — whose text passes the new relative threshold (a
    spills.userFraction of the live window budget minus the compaction
    reserve, floored by spills.userThreshold bytes) is redacted and
    written verbatim to .fah/spills/<sessionId>/user-<n>.md (the #678
    layout) from one choke point (Agent.userMessageGuard); the model
    receives a bounded head+tail preview plus a read-in-parts instruction
    riding the replacement USER message (copy in
    prompts/spill/user_read_instruction.md). Images stay inline, a
    refused spill write degrades to today's inline behavior, and
    spills.enabled: false / userThreshold: 0 / the new
    FAH_USER_SPILL_THRESHOLD env knob (bytes) keep the legacy shapes.

  • fix(gh-1743): a structured compaction that hits its total-budget
    watchdog (the factory's 300s totalBudget) no longer aborts the
    live session. AutoCompactorFactory.run() now applies the
    mechanical local-trim fallback (keepRecentTokens, pairing-repaired,
    [context trimmed locally] marker — the same valve the classic
    pass ladder ends on) instead of failing fast; the honest
    both-roles-failed verdict is kept only when nothing is droppable.

  • fix(gh-1636): auto-compaction stops dumping the internal checkpoint
    summary into the console — the default report is ONE marker line
    (● auto-compacted · engine: N → M tokens (F freed · P%)); the records
    line + fenced summary moved behind the new tui.showCompactionDetails
    opt-in (default false), and the manual /compact still prints the full
    block. The headless log face no longer streams the summarizer deltas by
    default (the gh-1433 E4 live stream is part of the same opt-in; the
    gh-1430 … reasoning liveness line still covers the window). Nothing
    is lost — the summary stays in the session file, reachable via
    compact_expand.

  • fix(test): gh-1732 — the wrap_ansi_line_perf_test sustained-contention
    validation leg spikes 3 of the 5 doubled runs per attempt instead of 4
    (CI run 39546301132, core shard 2/4: the old shape left exactly ONE
    real-wall-clock-timed doubled sample per attempt, and real jitter on
    that single sample — the very shape being modeled — inflated all three
    attempts' min ratios to a 3.30x best against the 3.2 bound while the
    real 500k growth legs on the same runner stayed green). 3-of-5 is the
    documented live-red shape ("≥3 of the 5 timed runs"): median-of-5
    [c,c,s] is still a spiked value, so the leg still proves the
    pre-gh-1532 median harness fails, and the min harness now rides two
    clean doubled samples per attempt.

  • fix(#1705): CLI fresh-install boarding asks the provider choice FIRST —
    the same list the /add-provider picker shows (presets, Custom last) —
    and only Custom continues into the api-type wizard; a preset opens its
    own flow with the api type prefilled. Previously boarding opened the
    custom wizard directly, cold-asking "api type" before any provider was
    chosen.

  • feat(providers): gh-1704 — first-class Codex/ChatGPT credential export,
    CI refresh action, and Provider Queue integration (Copilot-export
    parity). fa provider export <name> [--check] resolves the saved
    ChatGPT login's OAuth blob from its secure-store slot, refreshes it when
    stale, and prints the shell lines (OPENAI_API_KEY + OPENAI_BASE_URL
    = the Codex backend — these tokens never serve api.openai.com); the
    refresh token never prints and --check reports credential health with
    the SSO caveat. /provider chatgpt import [path] adopts an existing
    codex login session (~/.codex/auth.json) into the same slot — no
    browser round-trip. .github/actions/chatgpt-codex-auth mints a fresh
    access token from a CHATGPT_REFRESH_TOKEN secret in CI (direct curl,
    no third-party action); invalid_grant fails fast and loudly.
    providersQueue: entries speak chatgpt-codex: an omitted apiKeyEnv
    resolves the Keychain slot automatically (in-memory refresh), a new
    refreshTokenEnv: field mints the initial access token on slot
    activation, and a raw Bearer token in apiKeyEnv is tolerated by
    chatgpt_codex.dart (no more FormatException — proactive/reactive
    refresh degrade, a 401 surfaces as the auth death the queue fails over
    on).

  • feat(bench): gh-1681 — leaderboard submission readiness. ATIF
    trajectories: new exportAtifJson whole-snapshot exporter
    (lib/src/trajectory/atif_export.dart, ATIF-v1.7 — the schema harbor
    0.23.0's flagship adapters emit) behind a headless fa trajectory export [--format atif] [--output path] verb; the harbor adapter runs
    it as a bounded best-effort exec after the agent phase on every
    terminal path, so <trial>/agent/trajectory.json reaches the hub with
    the other agent logs (the leaderboard static analysis requires one per
    rewarded trial) and result.json metadata carries an
    atif_trajectory present/missing marker. The golden fixture
    (bench/harbor_fa/fixtures/atif_sample.json) is pinned Dart-side and
    cross-validated against harbor's pydantic Trajectory +
    validate_trajectory in test_atif_contract.py. Workflow: a
    submission: true dispatch input turns the submission rules into hard
    setup preflights (full task coverage, ≥5 attempts/task, no
    agent-timeout override, pinned dataset) and makes the new per-shard
    ATIF presence gate fail hard; default runs gain only a warn-only scan
    (artifact flow unchanged). Runbook: docs/leaderboard-submission.md
    (pinned dataset + @latest content-hash recording, quota-window runner
    policy, lb CLI steps).

  • fix(headless): gh-1589 — the headless session becomes a SEALED artifact:
    no session-JSONL writes land after the run's agent phase ends. New
    Session.seal() (state in the writer, no file-format change): raised in
    runHeadless' finally and the REPL teardown as the LAST session action
    (close order: drain → usage fold → seal → exit, one implementation both
    paths — REG-2), it makes every writer (_persistMessages,
    shell_job_registry snapshots, compaction, any appendCustomEntry,
    leaf moves) drop late appends with exactly ONE stderr warning per
    writer class — never a throw into an unhandled zone; the exit code
    stays the turn's terminal outcome (issue #413 semantics untouched). The
    settle→reaction path is sealed with the file (I2): the drain wrap-up
    raises a fence (_reactionRunsSealed) so a background-shell settle or
    task completion arriving after the final answer's drain — including the
    registry's one-turn-deferred listener racing _finishHeadlessDrain's
    check-to-return window (the 65gGW7b 10:13:42–10:14:00 burst) — does
    board bookkeeping only: no _startRun, no model request, no record.
    An honest drain's records (final answer → seal) stay legal (E7); the
    seal is per-run state, so resume/replay/session_repair are unchanged.
    Adapter side (bench/harbor_fa, I3/AC4+AC7): run()/perform_task
    returns only after the fa process exited on EVERY path — the ladder
    kill became terminate-and-reap (SIGTERM → bounded exec await → SIGKILL
    escalation → pgrep confirm), and the external-cancel paths (harbor's
    own wait_for firing first, the no-knobs exec, external-cap-only mode)
    now fire the kill + bounded reap before propagating, with a loud
    agent-phase exit guarantee degraded stderr note when fa survives both
    signals (never a silent proceed, never an unbounded hang).

  • fix(bench): gh-1643 — a trial killed mid-stream no longer reports 0/None
    usage. The fold read only FINALIZED assistant messages, so a run killed
    before its first assistant message (the costliest rows) folded to silent
    zeros. Both folds now price a dangling model_request_summary at its
    KNOWN request-side volume (system prompt + outbound message chars at the
    shared chars/4 rate, estimated input; output unknown) and mark the usage
    incomplete instead: UsageChainScan.usageIncomplete → usage.json
    (usageIncomplete: true, written only when set so completed folds stay
    byte-identical), SessionUsage.usage_incomplete → the tb adapter's
    usage-guard.json row and harbor's agent_result.metadata, and the
    post-mortem stamps usage_incomplete on every results.json row it
    patches from a partial fold. Harbor also exports the run outcome class
    (agent_result.metadata.failure_mode: agent_timeout self-kill /
    external_kill / completed / crashed / never_started) so downstream
    classification stops relying on exception.txt archaeology; a summary
    superseded by another summary is flushed instead of silently dropped on
    both sides. Never-started trials keep the gh-1638/gh-1576 audit-only
    precedence (no fake usage).

  • fix(stuck-calls): gh-1632 — a stuck tool call in an interactive session now
    escalates like everywhere else. effectiveStuckTool() defaulted every
    non-headless host to StuckFollowUpMode.advisory when no explicit
    agent.stuckTool: was configured, so the fa 1.0.580 incident session (a
    dead git fetch in the REPL) fired its tool_stuck flag at the 300 s
    threshold and then hung for 27 more minutes — advisory mode never cancels,
    retries, or converts; only the owner's manual steering broke the deadlock.
    The autonomous default (cancel → retry once → background-convert, then a
    session-visible escalation) now applies in EVERY mode; advisory remains
    an explicit opt-out (agent.stuckTool.followUp: advisory), never an
    implicit default.

  • fix(bindings): gh-1624 — the gh-1570 binding preflight no longer kills
    translating-proxy factories. _validateModelEndpoint's vendor-prefix
    verdict was fatal on ANY endpoint, so FA_PROVIDER_TYPE=anthropic +
    bifrost's /anthropic path + zai/glm-5.3 exit-64'd every boot on
    1.0.576 (the prefix is a routing key on a translating proxy, not a
    dialect declaration). The vendor-prefix check now fires ONLY on the
    endpoints that carry a dialect verdict — the three family-exclusive
    canonical vendor endpoints (api.anthropic.com, api.openai.com,
    generativelanguage), mirroring the family verdict's scope: those keep
    the gh-1570 fail-fast, ROUTERS (OpenRouter/Copilot/DIAL/kimi — the
    prefix is a routing key there too; OpenRouter's canonical ids are
    anthropic/claude-*) and unknown/private endpoints carry the mismatch
    as a ProviderModelBinding.warnings entry, never exit 64. The warning
    is rendered on EVERY resolve path (gh-1624 review): the FA_PROVIDER_*
    boot notice, the provider queue (per entry at boot, on queue reload,
    and in /provider queue test), and the --model/--base-url flag
    revalidation — a rider never boots silently.
    Breaking-change note for 1.0.576 upgraders behind proxy
    factories: the migration is the wire: FA_PROVIDER_CONFIG key
    —
    documented now (README env-preconfig section,
    docs/agents/providers.md §Translating proxies, --help) with its
    caveat: the override engages only when the declared wire DIFFERS from
    the provider record's own kind (record anthropic + wire: anthropic
    still runs the checks). REG matrix (AC1/AC2/AC3) pinned in
    test/model_roles/provider_model_binding_test.dart; AC4 IT drives a
    bifrost-shaped loopback proxy through the production anthropic adapter
    (test/model_roles/bifrost_proxy_binding_test.dart).

  • gh-1596: the first-run wizard (site/setup.sh) no longer crashes or
    corrupts input under curl | sh on dash (Ubuntu sh) or busybox ash
    (Alpine): scripts/gen_installers.dart now renders the provider/mode/
    approval menus from the live provider catalog into plain POSIX sh — the
    bashisms that killed dash (printf -v, read -p) and silently corrupted
    busybox ash are gone; the hidden API-key prompt uses stty -echo + a
    by-name read (dash has no read -s) and the key never rides printf
    argv; EOF/Ctrl-C mid-wizard aborts non-zero with no half-written
    ~/.fah, and non-TTY runs print guidance and exit 1. The wizard's temp
    config is mktemp-created (no predictable, symlink-followable fixed
    path). A new path-triggered installer-portability CI leg proves the
    script under dash + busybox ash (ubuntu) and bash (macOS) on every
    site/** or generator change.

  • fix(cli): gh-1572 — fa boot no longer rewrites ~/.fah/config.yaml
    from a lossy config. persistConfig (bin/fah_runapp.dart) built a
    fresh CliConfig carrying only some fields and persistConfig() ran
    unconditionally at boot, so every launch silently dropped the user's
    spills:, subagents:, jobs:, headless:, agent:, quota:,
    tui:, trajectory:, waiting:, power.hold and auto_update
    settings (first reported on 1.0.547). Three cooperating fixes: the
    boot save is now saved.copyWith(<live overlays>) so every parsed
    section rides along by construction; CliConfig.copyWith replaces the
    drifted hand-rolled withCustomProviders field list (itself dropping
    spills/subagents/waiting/jobs/agentMode/misuseBreaker/streamThinking
    inside saveCliConfig's merge step); the _diskPreservedSections
    disk backstop covers every known top-level key. spills: renders in
    toYaml (new SpillsConfig.toYaml) and agent.misuseBreaker: false
    persists too; an unchanged save no longer rewrites the file at all.
    Rework round: the boot snapshot re-syncs from disk after every
    sanctioned mid-session settings write (onUserConfigFileChanged — a
    cleared context cap, an auto_update key drop or a global compaction
    pick is no longer resurrected/overwritten by the next save in the
    same session); validateAgentSection (the strict fa config check /
    config set / settings-hub mirror) accepts the emitter's
    agent.misuseBreaker and agent.stuckTool keys so fa config check
    never false-fails on fa's own output; clearing the context cap drops
    only the cap key instead of the whole agent: block (the sibling
    mode:/misuseBreaker: keys survive); and the providersQueue:
    block (no typed renderer) joins the disk backstop so a save can never
    silently delete the user's queue. Rework round 2 (merge + guards):
    validateAgentSection's per-key switch became the _agentKeyValidators
    table (the two gh-1572 keys pushed the switch form to complexity 13 —
    over the repo's CRAP ratchet; the table keeps the mirror complete at
    complexity 4), and the last two direct USER-config writers —
    _writeTtsrSection and _writeMcpSection — fire the re-sync hook, so
    a mid-session ttsr rule toggle or mcp server edit survives the next
    persist too (the writer roster in settings_flow_config_sync.dart is
    now exhaustive).

  • gh-1606: the copilot provider accepts fine-grained PATs
    (github_pat_…) directly — PAT mode skips the copilot_internal/v2/token
    exchange entirely (GitHub 404s every PAT exchange) and sends the raw PAT
    as the Bearer against the configured Copilot base URL with the official
    Copilot CLI's first-party identity headers
    (copilot-integration-id: copilot-developer-cli,
    user-agent: copilot/<fa-version> (<os>) term/unknown, installed by the
    host at startup). OAuth device-flow tokens keep the exchange +
    proactive-refresh flow unchanged; a PAT 401/403 surfaces the error
    instead of a doomed refresh retry. Headless COPILOT_GITHUB_TOKEN now
    works with a PAT. Connect flow: the paste hint explains PAT mode, the
    dead-PAT case surfaces at the model fetch, and /provider export copies
    the saved Copilot account's full token to the clipboard (account picker
    on several entries; the token never prints to the transcript).

  • gh-1575: macOS clipboard paste fixed for TIFF-only pasteboards + the
    Ctrl+V text fallback. _readMacos probes «class TIFF» after PNGf/JPEG
    miss and converts via sips -s format png (a Preview/browser copy used
    to dead-end on clipboard holds no image; a failed conversion keeps the
    unavailable fall-through and every scratch file is cleaned up); new
    readPasteboardText (pbpaste / xclip+wl-paste / PowerShell
    Get-Clipboard) behind the same injectable ProcessRunner/platform:
    seam, and Ctrl+V with a TEXT clipboard now inserts the clipboard text
    into the composer instead of printing the image probe's dead-end reason
    — bracketed-paste routing, gesture pairing and silent-failure semantics
    unchanged. Review pass: the Windows Get-Clipboard read pins UTF-8
    console output and normalizes embedded \r\n (OEM-code-page mojibake),
    the Linux text backends pin text/plain;charset=utf-8 (an image-only
    clipboard can no longer leak bytes as "text"), the library doc no longer
    overclaims the FA_FAKE_PASTEBOARD seam, and FaTuiCallbacks moved to
    a fa_tui_callbacks.dart part file keeping fa_tui.dart under the
    2800-line gate.

  • gh-1555 (W1, lazy/windowed-everywhere reads): the legacy full-open call
    sites migrate to windowed opens, so a session ledger past the gh-1073
    2 GiB full-open backstop stays openable on every user-facing read path.
    The TUI session picker, /resume, and the boot name-resolution scan
    open windowed and resolve the display name through paged reads; the
    subagent transcript preview opens the child windowed (tail only); the
    _loadSession marathon fallback no longer degenerates to a refused
    full open when the boundary walk exhausts its page cap (it proceeds
    with the tail-anchored windowed session; the boot cap owns over-window
    residue); and the headless fa trajectory resolver opens windowed,
    reading the whole ledger for the cost/inspect views through the new
    SessionTree.ensureWholeLedgerResident — a sequential forward scan in
    bounded strips (WindowedSessionStorage.loadAllOlder), not the
    boundary-walk's doubling strips (whose per-strip record cap measured
    ~13x read/parse amplification over the whole file). The same change
    block-prepends paged chunks in _indexChunk (one insertAll per
    list), removing an O(n²) the suspended-eviction walk paid per record.
    Trajectory export streaming (AC3a), split-and-retain rotation
    activation (W2), and the opt-in write-side caps (W3) remain follow-up
    work.

  • gh-1605: the FinalizeGate gains a "grader parity" named check. A
    produced entry point (script/CLI the verifier will run itself) is only
    verified when it survives the grader's own invocation: the checklist
    must re-run each produced entry point from a different working
    directory in a fresh subprocess with a clean env, capture stdout/
    stderr/exit code, and diff against the earlier in-session run — any
    divergence (different output, relative-path artifacts, locale/
    encoding-dependent output) is a fail ledger item, never a pass.
    Closes the modernize-scientific-stack__nwbgy7a near-miss (every
    in-session checklist item passed, the verifier's clean-cwd subprocess
    collapsed Station 102 to Station 101's mean, reward 0.0). REG coverage:
    the parity procedure catches cwd- and env-dependent entry points and
    does not false-positive cwd-independent ones
    (test/agent/finalize_gate_grader_parity_test.dart), and an IT replays
    the evidence shape end-to-end — divergence detected and persisted as a
    fail ledger item before finalize.

  • gh-1603: unattended runs can no longer end as a clean success on empty
    model responses. Loop: from the second empty-hiccup retry on, the request
    carries a continuation nudge user message ("your last reply was
    empty/truncated — continue the task"; exact-text constant
    emptyContinuationNudge, emitted with MessageStart/End so the transcript
    stays truthful) — a cap-truncated reasoning model re-streamed verbatim
    repeats the empty outcome, the nudge breaks the symmetry; the classifier
    treats that exact message as transparent, so the turn's partial-content
    window (gh-1577) survives a nudged retry. Headless: the empty-reply nudge
    budget rises to AgentCliConfig.headlessEmptyReplyNudges (default 2 — the
    session ends only after 3 consecutive unrecoverable empties; interactive
    keeps the #1085 M2b one-nudge semantics), and a run that still settles
    empty prints the explicit __FA_INCOMPLETE marker and exits 1 — a
    distinguishable failure for the bench adapter, never a silent exit 0.

  • gh-1577: an empty assistant completion after partial content (assistant
    text, tool activity, or a truncation signal earlier in the turn) is a
    bounded-retry provider hiccup — up to emptyHiccupRetries (default 3)
    re-streams with exponential backoff (emptyHiccupBaseDelay,
    emptyHiccupMaxBackoff, both threaded through the Agent facade,
    AgentWiringSpec, and the CLI config), and exhaustion ends the run with
    a loud StopReason.error (headless exit 1, interactive error + prompt)
    instead of the silent exit-0 settle that killed the gh-1577 trials.
    Clean empties keep the #1085 M2b one-nudge semantics. The abort always
    wins: a cancel racing the final retry settles StopReason.aborted for
    any empty shape (the exit-contract race is pinned by tests), and the
    backoff sleep is cancellation-aware.

  • gh-1574: SQLite reads work on installed binaries — bundle libs land where
    the binary looks. The AOT binary's native-assets resolver reads
    <exe_dir>/../lib/<name> (the dart build cli bundle layout), but every
    POSIX installer copied the bundle libs beside the binary
    (<install_dir>/lib), so read path=x.db died with a raw FFI dump
    (Couldn't resolve native function 'sqlite3_initialize' … Failed to load dynamic library '~/.local/lib/libsqlite3.dylib'); it only worked on machines
    with a manual symlink in ~/.local/lib. Fixed the layout in all three
    install paths — site/install.sh (incl. the quarantine-xattr pass and the
    source-build advice), install_local.sh, and fa update
    (bin/self_manage.dart copyBundleLibs, now public and layout-pinned by
    tests; also aligns Windows fa update with install.ps1, which was
    already correct) — plus clean degradation in the FFI engine:
    Sqlite3Engine.openReadOnly classifies the dart:ffi resolver's
    library-load failure (isSqliteDynamicLibraryLoadFailure) and throws a
    clean SQLite is unavailable — reinstall or run fa update note instead of
    the raw resolver dump; a Dart-level system-library fallback is impossible
    with package:sqlite3 native assets (build-time asset mapping, no loader
    override). Installs already broken by the old layout self-heal: on the
    bundled library's first failed load the binary moves the stale
    <exe_dir>/libsqlite3.* into <exe_dir>/../lib/ and retries the load
    (repairStaleSqliteBundleLayout) — so one fa update suffices (the
    successor binary repairs on its first SQLite read; a pre-fix binary cannot
    repair itself), and the stale <bin>/lib leftovers disappear as they
    migrate. The POSIX installers also report a failed lib copy instead of
    swallowing it (site/install.sh warns naming the lib dir; same in
    install_local.sh), and site/install.sh.template carries the same
    layout so regenerating the installers cannot resurrect the old one.

  • gh-1556: bench pipeline stops bleeding score + fa deadline awareness.
    Bench: the harbor ladder kill uses the [f]a bracket trick and is
    wrapped so a kill failure can never replace the TimeoutError (the
    pre-fix self-matching pkill classified TB 2.0 ladder kills as agent
    crashes — a SOLVED task scored 0); progress-watch (stall-gap 360s per
    the gh-1430 watchdog invariant, flat abs ceiling 3600s) is the
    bench-harbor.yml default on every shard, with the harbor adapter
    picking the gap-aware ProgressWatch for parity with the
    terminal-bench adapter; the aggregate counts completeness by
    result.json presence (errored trials report as their own breakdown
    line; exit 1 only on genuinely lost trials) and renders the full
    report — table, errored breakdown, ledger row — to
    $GITHUB_STEP_SUMMARY (TB1 parity). Both bench adapters export
    FA_DEADLINE_EPOCH_MS (effective cap − 120s wrap-up margin) into the
    agent session and audit the exact value. fa core: deadline awareness —
    FA_DEADLINE_EPOCH_MS / --timer/--deadline / /timer write one
    session clock, the per-turn context carries a time-left: line while
    armed, and one-shot system notices steer wrap-up at ~50%, T-3min
    (drain background jobs), and T-30s; absent deadline = byte-for-byte
    today's behavior.

  • gh-1522: tag-sourced versions — the git tag is the single source of
    truth. The release-bot file bumps are retired: cutting a release = a
    tag + GitHub Release (zero commits to main, zero file mutations).
    Both pubspecs carry the fixed 0.0.0-dev placeholder (CI guards
    hand-bumps), the publish job stamps the staged pubspec + generated
    changelog section from the tag (scripts/stamp_staged_release.sh,
    staged version == tag guarded pre-upload), store builds stamp
    --build-name/--build-number from the derived tag version
    (pubspec.lock stops churning across releases), and
    install_local.sh stamps version.txt from git describe.

  • feat(usage): gh-1460 — the fa-tokens: segment-close line carries the
    session's model id, so every token row the factory publishes is
    attributable to a model and therefore priceable. The fold now captures
    each segment's LAST-SEEN model id (the model that served the final
    request, the same string settings/UI display — mid-session fallback
    take-overs attribute the segment to the model that actually finished it;
    zero-filled fallbacks never clobber a seen model) and the pinned line
    shape grows an OPTIONAL "model":"<id>" key after "segment" — omitted
    when no model was observed, so legacy lines parse unchanged and the
    pinned regex (usageTokensLogPattern, gh-1241 AC7) keeps matching both
    shapes. usage.json's schema is untouched (the model is rebuildable from
    the chain, I6); old consumers using tolerant JSON ignore the new key,
    and the repo's single strict consumer (the pinned constant) is updated
    in the same change.

  • fix(test): gh-1460 rework — the wrap_ansi_line_perf_test growth-shape
    legs measure the doubling ratio with the MINIMUM over 9 runs instead of
    the median of 5 (CI run 38112524699, core shard 0/4: a scheduling pause
    inflated the doubled-size median — 12.09ms against a true ~7ms — and
    tripped the 3.2 bound at 3.65x on a genuinely linear wrap). Runner
    jitter only ever inflates a wall-clock sample, so the min converges on
    the true compute time; genuine superlinearity still trips the ratio
    (the gh-1496 quadratic candidates measure ~4x by min). The absolute-
    budget legs keep the median — their gh-1357 headroom already prices in
    jitter. A synthetic regression test pins the pause-proof property.