Repository navigation
v1.0.656
-
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
_runPromptoutside its guarded body and reach the root zone handler via
an unlistenedwhenCompletechain —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 dimsubmission cancelledreceipt (nevererror:), 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 interruptedreceipt 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 aSteeringInterruptFire
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 (liketask_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 deliverplus the
delivered ack. -
feat(1916): image-heavy submits no longer livelock — byte-based
images.maxBytesPerRequest(default 8MB, layered onto the count cap;
/imagesnames 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_createresponse into an actionable upgrade message instead of
exposingbad_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
agentInstancethrough 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
fanet1frames (sender X25519 key in the frame, channel-bound AAD) via
fa_hub_client0.2.11, so fa_network app members decrypt them without
a whois/senderKey round-trip. Inbound channel frames openfanet1
first and fall back to legacydap/1payloads. 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.startstops 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 reachingsendText, 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/waitForConnectionwith
rejoin-before-drain on reconnect, keyed channel decrypt
(hubDecryptPayloadWithKeyPair), public showcase payloads, and
HubBearerTransport(nativeAuthorizationheader 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.joinop), 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.changedinto the roster (additive displayName/class fields)
instead of dropping them, so the online counter no longer sticks at 0
when an agent comes online afterroster.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 / wholefa 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_membershipcustom record (leave tombstones, branch
resume); project scope is.fah/dap.json(committable,dapkey1
PBKDF2+ChaCha20-Poly1305 encrypted keys); global keeps the legacy
~/.dapfiles. 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-slotuseMainConnection: trueopt-in (YAML
models.slots.<slot>.useMainConnection/ app's "Same as main
connection" pick, preserved throughmedia_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_modalitiesgate; unsupported endpoints fail before any network
call); the OpenRouter video job API is a first-class video dialect
(submit → poll → mp4). Newdecidetool: 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 inprompts/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_ledgercarries 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.ymlbuild-extension leg) derives its tag as "latest+1 at its
own dispatch moment"; on 2026-10-24 thev1.0.635tag was cut by
auto_releasebefore those legs ran, so the extension (and the DMGs)
landed on an orphanedv1.0.636draft while the tag train published
v1.0.635 — every leg green, nothing asserted the published asset set.
Nowscripts/release_required_assets.txtis the single source of truth,
scripts/assert_release_assets.shgates therelease-provenancepublish
flip (a missing asset leaves the release a draft and fails the run
loudly) and a newrelease-asset-smokeci.yml job post-publish probes
every landing-page release link + the full required set (red on non-200).
Remediation: the exact-tagfa-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 onApp.framework/App+5.1%
(25,020,112 → 26,284,144, the accumulated Dart AOT of #1793/#1794/
#1796/#1805/#1806); the committedscripts/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--reseedoutput 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.dartgates the shape green
through the REALsize_manifest.py checkand pins the committed floor
byte-identical to a--reseedround-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.shnow ASSERTS the SPM lane instead:
SwiftPM state expected afterflutter 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'stextfor control bytes
(Ctrl+S →ctrl+swith texts), and the slash-menu edit cluster was
the one insert surface missing the_isCommandKeystrokeguard, so a
chord pressed mid-menu poisoned the typed prefix (/memorylanded 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
writesmodels.overridesAND 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.retentionin
~/.fah/config.yamlcaps 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
bashtool 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 ownsh -c;$0/$#/exit codes/set -e/stdin stay
identical), a PATH shim forpkill/killallplus akill()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 headlessSIGTERMaborts 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'sCore tests (shard 2/4)was
killed by its owntimeout-minutes: 12with 2535/2761 tests green — both
cache steps had stalled ~133 s on keys that existed (degraded service,
not eviction), coldpub 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 bothtest-corecache 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 inprompts/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). NewprojectContext:config
section:maxBytes(default 32768) —0disables 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'sbuild-androidleg died at
:jni:configureCMakeDebug[arm64-v8a]— the:jnimodule (transitive
jnipackage via path_provider_android) wires externalNativeBuild.cmake
with no version, so AGP auto-installed its defaultcmake;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-installscmake;3.22.1with 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.Modelgains an
optionalmaxImages(catalog-seeded:gemini-4-argon: 10, measured);
images.perModel.<modelId>config overrides it (strictimages:
section, user truth wins); the registry resolves
perModel > catalog > images.maxPerRequest(first present wins, the
issue-#1554 shape). A Google-shapedtoo 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: falsekeeps 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.userFractionof the live window budget minus the compaction
reserve, floored byspills.userThresholdbytes) 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_THRESHOLDenv knob (bytes) keep the legacy shapes. -
fix(gh-1743): a structured compaction that hits its total-budget
watchdog (the factory's 300stotalBudget) 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 newtui.showCompactionDetails
opt-in (default false), and the manual/compactstill 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… reasoningliveness 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_testsustained-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-providerpicker shows (presets,Customlast) —
and onlyCustomcontinues 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--checkreports credential health with
the SSO caveat./provider chatgpt import [path]adopts an existing
codex loginsession (~/.codex/auth.json) into the same slot — no
browser round-trip..github/actions/chatgpt-codex-authmints a fresh
access token from aCHATGPT_REFRESH_TOKENsecret in CI (direct curl,
no third-party action); invalid_grant fails fast and loudly.
providersQueue:entries speakchatgpt-codex: an omittedapiKeyEnv
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 inapiKeyEnvis 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: newexportAtifJsonwhole-snapshot exporter
(lib/src/trajectory/atif_export.dart, ATIF-v1.7 — the schema harbor
0.23.0's flagship adapters emit) behind a headlessfa 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.jsonreaches the hub with
the other agent logs (the leaderboard static analysis requires one per
rewarded trial) andresult.jsonmetadata carries an
atif_trajectorypresent/missing marker. The golden fixture
(bench/harbor_fa/fixtures/atif_sample.json) is pinned Dart-side and
cross-validated against harbor's pydanticTrajectory+
validate_trajectoryintest_atif_contract.py. Workflow: a
submission: truedispatch 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_registrysnapshots, compaction, anyappendCustomEntry,
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_repairare 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 degradedstderr 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 danglingmodel_request_summaryat 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.jsonrow and harbor'sagent_result.metadata, and the
post-mortem stampsusage_incompleteon 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 toStuckFollowUpMode.advisorywhen no explicit
agent.stuckTool:was configured, so the fa 1.0.580 incident session (a
deadgit fetchin the REPL) fired itstool_stuckflag 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;advisoryremains
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, soFA_PROVIDER_TYPE=anthropic+
bifrost's/anthropicpath +zai/glm-5.3exit-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 aProviderModelBinding.warningsentry, never exit 64. The warning
is rendered on EVERY resolve path (gh-1624 review): theFA_PROVIDER_*
boot notice, the provider queue (per entry at boot, on queue reload,
and in/provider queue test), and the--model/--base-urlflag
revalidation — a rider never boots silently.
Breaking-change note for 1.0.576 upgraders behind proxy
factories: the migration is thewire: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 undercurl | shon dash (Ubuntu sh) or busybox ash
(Alpine):scripts/gen_installers.dartnow 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 usesstty -echo+ a
by-nameread(dash has noread -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 ismktemp-created (no predictable, symlink-followable fixed
path). A new path-triggeredinstaller-portabilityCI leg proves the
script under dash + busybox ash (ubuntu) and bash (macOS) on every
site/**or generator change. -
fix(cli): gh-1572 —
faboot no longer rewrites~/.fah/config.yaml
from a lossy config.persistConfig(bin/fah_runapp.dart) built a
freshCliConfigcarrying only some fields andpersistConfig()ran
unconditionally at boot, so every launch silently dropped the user's
spills:,subagents:,jobs:,headless:,agent:,quota:,
tui:,trajectory:,waiting:,power.holdandauto_update
settings (first reported on 1.0.547). Three cooperating fixes: the
boot save is nowsaved.copyWith(<live overlays>)so every parsed
section rides along by construction;CliConfig.copyWithreplaces the
drifted hand-rolledwithCustomProvidersfield list (itself dropping
spills/subagents/waiting/jobs/agentMode/misuseBreaker/streamThinking
insidesaveCliConfig's merge step); the_diskPreservedSections
disk backstop covers every known top-level key.spills:renders in
toYaml(newSpillsConfig.toYaml) andagent.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, anauto_updatekey drop or a global compaction
pick is no longer resurrected/overwritten by the next save in the
same session);validateAgentSection(the strictfa config check/
config set/ settings-hub mirror) accepts the emitter's
agent.misuseBreakerandagent.stuckToolkeys sofa config check
never false-fails on fa's own output; clearing the context cap drops
only the cap key instead of the wholeagent:block (the sibling
mode:/misuseBreaker:keys survive); and theprovidersQueue:
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 —
_writeTtsrSectionand_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 insettings_flow_config_sync.dartis
now exhaustive). -
gh-1606: the
copilotprovider accepts fine-grained PATs
(github_pat_…) directly — PAT mode skips thecopilot_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. HeadlessCOPILOT_GITHUB_TOKENnow
works with a PAT. Connect flow: the paste hint explains PAT mode, the
dead-PAT case surfaces at the model fetch, and/provider exportcopies
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._readMacosprobes«class TIFF»after PNGf/JPEG
miss and converts viasips -s format png(a Preview/browser copy used
to dead-end onclipboard 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 injectableProcessRunner/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 WindowsGet-Clipboardread pins UTF-8
console output and normalizes embedded\r\n(OEM-code-page mojibake),
the Linux text backends pintext/plain;charset=utf-8(an image-only
clipboard can no longer leak bytes as "text"), the library doc no longer
overclaims theFA_FAKE_PASTEBOARDseam, andFaTuiCallbacksmoved to
afa_tui_callbacks.dartpart file keepingfa_tui.dartunder 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
_loadSessionmarathon 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 headlessfa trajectoryresolver 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(oneinsertAllper
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 afailledger 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
failledger 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 toAgentCliConfig.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_INCOMPLETEmarker 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 toemptyHiccupRetries(default 3)
re-streams with exponential backoff (emptyHiccupBaseDelay,
emptyHiccupMaxBackoff, both threaded through theAgentfacade,
AgentWiringSpec, and the CLI config), and exhaustion ends the run with
a loudStopReason.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 settlesStopReason.abortedfor
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>(thedart build clibundle layout), but every
POSIX installer copied the bundle libs beside the binary
(<install_dir>/lib), soread path=x.dbdied 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, andfa update
(bin/self_manage.dartcopyBundleLibs, now public and layout-pinned by
tests; also aligns Windowsfa updatewithinstall.ps1, which was
already correct) — plus clean degradation in the FFI engine:
Sqlite3Engine.openReadOnlyclassifies the dart:ffi resolver's
library-load failure (isSqliteDynamicLibraryLoadFailure) and throws a
cleanSQLite is unavailable — reinstall or run fa updatenote 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 onefa updatesuffices (the
successor binary repairs on its first SQLite read; a pre-fix binary cannot
repair itself), and the stale<bin>/libleftovers disappear as they
migrate. The POSIX installers also report a failed lib copy instead of
swallowing it (site/install.shwarns naming the lib dir; same in
install_local.sh), andsite/install.sh.templatecarries 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]abracket trick and is
wrapped so a kill failure can never replace theTimeoutError(the
pre-fix self-matchingpkillclassified 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-awareProgressWatchfor parity with the
terminal-bench adapter; the aggregate counts completeness by
result.jsonpresence (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//timerwrite one
session clock, the per-turn context carries atime-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 fixed0.0.0-devplaceholder (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-numberfrom the derived tag version
(pubspec.lockstops churning across releases), and
install_local.shstampsversion.txtfromgit 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_testgrowth-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.