Repository navigation
Conversation
- A turn whose process crashed right after saving its end now shows as finished: the next process writes the missing `turn_end` before any queued turn runs, and a retry of that prompt resolves at once instead of after 30 s. - A steer that a crashed turn had accepted now appears in the stream when the turn resumes. - On native, a steer that arrives as a step ends in a tool call joins the turn and the call runs, as on WebAssembly. The stream no longer says `end_turn` for a turn the journal records as cancelled. - A steer stored while a tool runs reaches the model in the request right after that tool's result, from any server. - A turn interrupted by a steer that carries an image now ends `cancelled`, matching its journal. - A reader in another process gets a running turn's lines in the same order and under the same cursors as a replay from cursor 0. - A turn that stops at its deadline between steps emits `session.deadline`. - `agent.session()` does no I/O. The durability starts on first use, and a new session's `id` is `null` until then. - Upgrading from a dev build that wrote `local()` sessions with world-local 5.0 keeps those sessions: libfx moves them into the new layout before the store opens, and rejects with `FX_LOCAL_STORE_LAYOUT` if it cannot. - Under `NODE_ENV=development`, creating an agent again with the name of one that already runs replaces it and emits `agent.replaced`, so `next dev` works. Outside development this still throws `FX_AGENT_NAME_IN_USE`. - The `FX_AGENT_NAME_IN_USE` error says that agents created without a name all share the name `default`, and to give each agent its own name. - Images within their documented limits are accepted beside the 8 MiB prompt frame, and the limits apply to `resizeImage`'s output, so large photos are resized instead of refused. - A prompt or steer that could never run is refused by `prompt()` and `steer()` before it is stored. - The missing-credentials error says no AI Gateway credentials were found and where libfx looks for them. - The model catalog is fetched at most once per agent, and a prompt with a known model id does not wait for it. - A `web_search` `tool_start` event carries the search query. - Cancelling a turn on WebAssembly ends it `cancelled` even when the request body fails after the abort. - After a cut-off `idempotent: false` call, the model is no longer told the call has a confirmed result. - The package no longer bundles undici; the bundled Worlds use `node:http`. - `next build --webpack` no longer warns about libfx's loader. - The npm package includes the libfx docs. - The README states that WebAssembly on Bun needs 1.3.10 or newer, except 1.3.14. - `fx -c` after a crash continues the interrupted turn once, with a notice. If fx crashes again while continuing it, the next `fx -c` asks. - The crash notice says whether fx quit before the response finished or again while continuing it. - After a crash, the next message, including a typed `continue`, reaches the model after the interrupted prompt instead of alone, in the TUI and in `fx ask --resume-id`. - The browser terminal draws approval prompts and keeps the page responsive while it waits for an answer.
- A steer that another server stores just before a reply ends continues that turn: before a durable turn ends, the engine reads the inputs its worker has not read yet. - A steer that a crashed turn accepted shows once after resume, also after a second crash. - A turn that ended without a record, such as a refused prompt or a cancel before any record, also gets its missing turn_end after a crash. - Durable sessions whose prompts carry images within the documented limits keep running later turns. Journal, checkpoint and snapshot loads allow 32 MiB, and the core bounds attached images at 4 MiB each and 8 MiB per prompt. - The model catalog cache keeps one entry per catalog URL, and a cancelled turn stops waiting for another engine's catalog fetch. - In development the newest definition of an agent name always wins, and only Node agents read NODE_ENV. - PR CI packages the docs from a local fixture; a release still requires the fx.sh docs.
… read - On native, the addon carries every model request the core sends. The core keeps each request within its 30 MiB budget by leaving out the oldest images, and the addon refused requests over 24 MiB, so image sessions stopped after about 17 MiB of images. They now run until the conversation outgrows the 32 MiB checkpoint limit, as on WebAssembly. - On WebAssembly, a cancel or deadline no longer waits for the read of stored inputs before a turn ends. - The README says that an image steer reaching a turn only as its reply ends leaves that turn `end_turn`.
- A request whose text alone is over the native addon's 32 MiB limit still fails with HostStreamBackpressure before it reaches fetch; the image framing test checks this again, and the comments no longer say every request fits. - The comment on the wasm turn-end read says a steer it finds after a cancel or deadline is placed like any steer stored after the turn ended.
The image framing test now requires a text-only request over the core's 30 MiB budget to reach fetch, and one over 32 MiB to fail with HostStreamBackpressure. Its comment says the transport carries each request the core trims, not every request.
fazxes
marked this pull request as ready for review
October 17, 2026 16:54
fazxes
marked this pull request as draft
October 17, 2026 18:03
Brings in fazxes/save-tool-images-once (with fazxes/save-session-images-once): durable sessions save prompt, tool and steer images once by name, outside their records and checkpoints. - A durable prompt is still refused before anything is stored when its frame is too large; the storage limit now counts the stored entry, with images named, and the images' bytes. - Checkpoints, journal loads and snapshots keep the 32 MiB limit for engines whose own store keeps images inline, and image staging uses the native attachment table's limits. - Durable prompts and steers with images name their saved images in the log, and the read of stored inputs before a turn ends keeps working with them.
- After fx ask is killed mid-turn, fx -c continues the turn it left, once, as it does after a crash in the app. A finished fx ask still leaves fx -c alone. - A continued turn's model request says the process crashed and the turn is resuming, in the app, in fx ask --continue-recovery and in libfx, from one shared function.
- libfx's bundled world-local records who holds a session's fence lock. A writer that finds it held by a process on this host that no longer runs takes it at once, instead of waiting 30 s for it to go stale. - The bundle fails to build if world-local's lock call changes.
- npm i libfx installs only the native addon for the machine. Each addon is its own package, @libfx/linux-x64, @libfx/linux-arm64, @libfx/darwin-x64 or @libfx/darwin-arm64, listed as an optional dependency, and the loader finds it; Next.js still needs no config. - The terminal is @libfx/term, and libfx no longer ships fx-term.wasm. - The release workflow publishes the platform packages and @libfx/term first, at libfx's version, with provenance, and stops before publishing anything if one of them doesn't exist yet.
- A turn's end record carries its stop reason, so a turn_end that recovery writes after a crash, and a retry answered from the journal, report how the turn ended instead of unknown. Ends recorded by an earlier libfx still read unknown, and earlier libfx versions load the new records. - A fenced turn rejects with the store's FxFencedError, and a fenced engine starts no further model request. - checkpoint() past its limits rejects with FX_CHECKPOINT_TOO_LARGE, naming the limit, and a checkpoint save the engine skips emits checkpoint.error. - A session that hasn't saved a checkpoint yet reports checkpoint.mismatch when it continues on another model. - A resume() that continued a turn writes no idle line after that turn ends. - FxStopReason lists "yielded", and the README says a checkpoint carries the model.
- DurableSteer.tla states that a steer stored after a turn's last read, while its end is landing, never joins that turn and runs once as the next turn, and drive-steer.mjs walks that window on native and WebAssembly. - On WebAssembly, a turn that is no longer current skips the read of stored inputs before it ends instead of waiting for it. - A delivery that read the log while the previous worker's run ended reads it again before judging that worker's lease, so a turn no longer starts early and sends a second model request.
- A durable session keeps the images one model request can carry, 100 images whose base64 fits in 30 MiB, from the same limits a request uses. An older image leaves the conversation with a note, as before. A session with 120 large photos opens in about a tenth of the time and memory it took. - An open reads up to 8 saved images at once, in history order, and still names the first one missing. - A tool result with more than 8 images fails with a message naming the limit. - A turn an image steer ended cancelled has the steer's messageId as endedBySteer, in its result and its turn_end line, so an app can tell it from a turn cancel() ended. - The README and NAPI.md agree on the 32 MiB checkpoint limit, images saved by name for durable sessions, the window, and that saved images are never deleted.
- When native runtimes run short, no agent holds more than its share of the 64: an agent may take one while it holds fewer than 64 divided by the agents holding any, and the last free one is kept for an agent that holds none. Turns past an agent's share run on WebAssembly and emit runtime.fallback; without JSPI they fail with LIBFX_NATIVE_LIMIT, naming the limit. Nothing waits for a slot. - An agent is a name on a store; engines created with createFxEngine() outside an agent count as one more. - steer() and cancel() reject after close(), and memory() no longer redelivers a message every 10 ms when no agent listens, so the process exits. - A non-function fetch or onEvent is refused, naming the option. - session(id) on a World durability throws at once for an id that isn't a run id, with FX_SESSION_ID_INVALID. - A file: URL string for the WebAssembly module is read from disk. - libfx/wasm exports only what its types declare. - With backend "wasm", the JSPI error no longer says no native addon was found. - vercel()'s authorization error says it needs a Vercel deployment or VERCEL_OIDC_TOKEN. - The native fetch bridge frees request and response buffers over 64 KiB after each request.
… errors - A provider's HTTP error body, the length-limit notice and an unconfirmed Ultrafast notice no longer stream as reply text: a refused turn has the provider's error in result.error, including the server's own message for a rejected custom-provider key, and other notices reach onEvent as turn.notice. A content-filtered turn's error says the content filter stopped it. - Every tool_start of a cancelled turn gets a tool_end, on native and WebAssembly. - A provider tool other than web_search is refused with an error naming it. - An agent turn's unsupported-effort error names the model and the capability. - backend "auto" doesn't retry on WebAssembly after the model refuses Ultrafast, so the catalog is requested once. - A custom model declared supportsToolUse: false is offered no tools. - credentials(), onCredentials and refreshLock name the session for the model-list request too. - A refresh refused because the token belongs to another account is not run again, and the turn fails with LIBFX_GROK_ACCOUNT_CHANGED.
- A durable session's end record carries the turn's reply. When recovery writes a turn's missing end, it first shows, as one text_delta, whatever of the reply the stream doesn't show yet, so a viewer and a replay read the whole reply once. - The record that ends a turn an image steer ended names the steer, so a repaired end, a retry answered from the log and a checkpoint keep endedBySteer.
- A model request after a tool call waits for the store's answer to the record holding the call's result, so an engine whose store refused that record sends nothing more, on native and WebAssembly, for every provider. - A worker another one replaced, that wakes after a stall, writes no more UI lines once its lease is past its end or a renewal is refused, so readers' lines and cursors stay as a replay gives them. - UiStream.tla models a takeover and finds the reordering a World that places a late line where it was queued allows; DurableSession.tla bounds the one line a frozen worker can still have in flight.
- A wasm URL that answers with an error status rejects with LIBFX_WASM_LOAD_FAILED, naming the URL without its query and the status, for the agent and the terminal. - When the browser terminal's session store refuses a save, the app gets session.persist_error with the store's error, and on a revision conflict fx says on screen that it stopped saving the conversation. - The terminal reports transport.start, transport.response and transport.error; a model or mode change no longer reports a failed write to a settings file the browser doesn't have; prompt history uses the workspace adapter's root; a refused workspace says why on screen and emits workspace.invalid; terminal.size fires only when the size changes. - In auto mode, a browser workspace with permission "prompt" asks the user for an action that would go to review, since the browser has no reviewer to clear it. Everywhere else a missing reviewer still holds the action.
- libfx's bundled world-local names a UI stream line only as it lands, the next name after the stream's newest line, so a line whose write was in flight when its writer stalled can't sort ahead of lines readers already have, and cursors stay as a replay gives them. A writer that stalled between naming and landing takes the next name. - The bundle fails to build if world-local's stream writes change. - UiStream.tla models where a World places a line and finds the reorder for a World that names lines early; drive-uistream.mjs walks its takeover graph against libfx's UI writer and reader on world-local.
Brings in main through b52a5c0, with the saved session images branches as merged and the resume size retry. - The native fetch bridge keeps its 32 MiB request limit, and each provider's own bound applies within it; a compile-time check keeps the bridge at least the core's 30 MiB request budget. - fx's crash notice is one constant, so steering never wraps it as a user update. - The README keeps the note that turn.result can resolve before a turn's last records are stored.
Brings in main through 8f89318, which keeps a finished reply when a crash cuts off its turn's end. - A turn's end record carries its stop reason, its reply and endedBySteer, and is a barrier that turn.result waits for; step records still run ahead of their commit. - A durable session shows a recovered turn's reply once, whichever worker ends the turn. - A delivered turn's history keeps no crash notice the model never got, and a steer accepted after the reply was saved reaches the model. - A deadline that arrives while a turn's end is being stored ends the turn as its stored record says.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
- A cancel or handoff that arrives once a turn's end record is being written doesn't apply: the turn ends as the record says, on native and WebAssembly, and cancel() returns normally. A cancel that arrives before the end is written is still recorded as cancelled. - On WebAssembly, the record that ends a cancelled turn says cancelled, not refused or error. - Recovery finds where a reply starts after fx's restart notice, so a restarted reply that a repair completes shows once.
The DUR-87 test dropped the stream as soon as the response started. On a slow runner fx had not read any of the reply yet, so the recovery it saved resent the request instead of restarting the reply, and the model got no restart note. The test now waits until fx shows the partial reply.
Resuming a turn whose reply was saved before its end ends the turn with that reply, unless the turn took a steer after the reply was saved. The README described only the first case.
The DUR-87 test killed fx once the trace showed any recovery checkpoint. fx saves one when it sends the request, before any of the reply arrives, so on a slow runner the kill landed before the checkpoint the dropped stream saves. The resume then resent the request, and the model got no note to restart the reply. The test now waits for the checkpoint that restarts the reply.
fazxes
marked this pull request as ready for review
October 18, 2026 18:37
fazxes
marked this pull request as draft
October 19, 2026 07:51
Each platform addon is published as a prerelease of libfx itself, such as 0.0.15-0.linux-x64, and libfx names it as the aliased optional dependency libfx-<platform>. No package besides libfx is published. fx-term.wasm stays inside libfx, because a terminal version installed under an alias resolves to the release on an app's next fresh install. The Vercel World bundle no longer carries the build machine's path.
main replaced the v1 session store. This branch's fx CLI crash continuation, where fx -c continued a crashed turn once and fx ask --continue-recovery told the model it was resuming, was built on that store, so it is dropped here and main's behavior stands: a crashed turn is kept, marked interrupted, and waits for the user. The browser terminal's revision-conflict handling and libfx's recovery are kept.
A delivery posted for another agent is forwarded to the session's owner. The forward used its own idempotency key, so forwarding a message the queue already held for the owner queued it twice. Each delivery is now keyed by its destination agent, so the forward and the queued message share one key. The tenants driver's queue now forgets a key once its message is delivered, as Vercel Queues and sdk/tests/queue-world.mjs do.
fazxes
marked this pull request as ready for review
October 20, 2026 01:08
main's durable worker can start from the log the request that took the prompt read and extended. It keeps doing so here, except when one of the worker's runs ended after that read, which may then lack the run's release; then the worker reads the log again, as it does for any read made while a run ended.
An ACP turn's stop reason is still the one its recorded end gives, and when that is cancelled, main's change finishes the turn's open tool calls.
main now finishes a turn's open ACP tool calls when the turn is finalized. That runs after the journaled end is written, and a journaled turn's calls read as cancelled only when its recorded end is cancelled, as the prompt's answer does.
This was referenced Nov 6, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Crash recovery
turn_endbefore any queued turn runs, and a retry of that prompt resolves at once instead of after 30 s.turn_endthat recovery writes, and a retry answered from the journal, report the turn's real stop reason. Only a turn an earlier libfx ended reportsunknown.turn_endafter a crash.cancelled, notrefusedorerror.session.deadline, and a deadline that arrives while the turn's end is being stored ends the turn as its record says.resume()that continued a turn writes no idle line after that turn ends.Steering
end_turnfor a turn the journal records as cancelled.cancelled, as its journal says, and has the steer'smessageIdasendedBySteerin its result andturn_endline, also after recovery, so an app can tell it from a turncancel()ended.Durable sessions and stores
FxFencedError, and a model request after a tool call waits for the store's answer to the record holding the call's result, so a fenced engine sends nothing more.agent.session()does no I/O. The durability starts on first use, and a new session'sidisnulluntil then.session(id)on a World durability throwsFX_SESSION_ID_INVALIDat once for an id that isn't a run id.steer()andcancel()reject afterclose(), andmemory()no longer redelivers a message every 10 ms when no agent listens, so the process exits.local()sessions with world-local 5.0 keeps those sessions: libfx moves them into the new layout before the store opens, and rejects withFX_LOCAL_STORE_LAYOUTif it cannot.NODE_ENV=developmentin Node, creating an agent again with the name of one that already runs replaces it and emitsagent.replaced, sonext devworks; the newest definition always wins. Elsewhere this still throwsFX_AGENT_NAME_IN_USE, whose message now says that agents created without a name all share the namedefault.checkpoint()past its limits rejects withFX_CHECKPOINT_TOO_LARGE, naming the limit, and a save the engine skips emitscheckpoint.error. A session that hasn't saved a checkpoint yet still reportscheckpoint.mismatchwhen it continues on another model.Images and input limits
resizeImage's output, so large photos are resized instead of refused.prompt()andsteer()before it is stored.Multiple tenants
runtime.fallback; without JSPI they fail withLIBFX_NATIVE_LIMIT, naming the limit.Errors, notices and events
result.error, including a custom provider's own message for a rejected key, and other notices reachonEventasturn.notice. A content-filtered turn's error says the filter stopped it.tool_startof a cancelled turn gets atool_end, on native and WebAssembly.fetchoronEventis refused, naming the option; a provider tool other thanweb_searchis refused, naming the tool; an agent turn's unsupported-effort error names the model and the capability.credentials(),onCredentialsandrefreshLockname the session for the model-list request too. A refresh refused because the token belongs to another account isn't run again, and the turn fails withLIBFX_GROK_ACCOUNT_CHANGED.supportsToolUse: falseis offered no tools, andbackend: "auto"requests the catalog once when the model refuses Ultrafast.file:URL string for the WebAssembly module is read from disk; awasmURL that answers with an error status rejects withLIBFX_WASM_LOAD_FAILED, naming the URL and status. Withbackend: "wasm", the JSPI error no longer says no native addon was found, andvercel()'s authorization error says it needs a Vercel deployment orVERCEL_OIDC_TOKEN.libfx/wasmexports only what its types declare, andFxStopReasonlists"yielded".web_searchtool_startevent carries the search query. After a cut-offidempotent: falsecall, the model is no longer told the call has a confirmed result. Cancelling a turn on WebAssembly ends itcancelledeven when the request body fails after the abort.Packages
npm i libfxinstalls only the native addon for the machine. Each addon is published as a version of libfx itself, such aslibfx@0.0.15-0.linux-x64, which libfx lists as the optional dependencylibfx-linux-x64, and likewise forlinux-arm64,darwin-x64anddarwin-arm64. No other package is published, and Next.js still needs no config. Whatnpm i libfxunpacks drops from about 46 MiB to about 18 MiB.fx-term.wasmstays inside libfx. A terminal published as its own version of libfx would resolve to the release on an app's next fresh install, so it ships with libfx until it can be a package of its own.addon(addon-devfor dev builds), solatestanddevonly ever name a release.node:http.next build --webpackno longer warns about libfx's loader. The package includes the libfx docs. The README states that WebAssembly on Bun needs 1.3.10 or newer, except 1.3.14.fx CLI
fx -ccontinued a crashed turn once, is not in it: it was built on the session store Remove the v1 session store #1328 removed. On the new store a crashed turn is kept, marked interrupted, and waits for the user. Continuing it on its own will come in a follow-up.Browser terminal
session.persist_error, and on a revision conflict fx says on screen that it stopped saving the conversation.transport.start,transport.responseandtransport.error; prompt history uses the workspace adapter's root; a refused workspace says why on screen and emitsworkspace.invalid;terminal.sizefires only when the size changes.permission: "prompt"asks the user for an action that would go to review, because the browser has no reviewer to clear it.AGENTS.md's permission section records this one exception; everywhere else a missing reviewer still holds the action.Decisions
idempotent: falsecall's outcome unknown, the model is told it may have partly run and the turn goes on.turn_resume, and the re-run starts after it.Before release
libfxthrough its existing trusted publisher.fx-term.wasmfrom libfx; fx-web'slibfx@0.0.14pins move when it upgrades.@workflow/world-localtoworld()gets the fence-lock and stream-placement fixes once world-local ships them ([world-local] Take over the in-band fence lock of a writer that was killed vercel/workflow#4908 and [world-local] Land a stream chunk after every chunk already in its stream vercel/workflow#4909);local()has them now.Docs: vercel-labs/fx-web#200