Plug-ins
6.11 Plug-ins — filmopen_plugins
Section titled “6.11 Plug-ins — filmopen_plugins”A manifest key slot’s auth is key by default, or oauth for the platform’s signed-in route. A renderer’s optional planKinds declares the catalogue tags its own signed-in platform slot renders, independently of row marks. Required film media still needs upload permission. The first OAuth slot on that platform offers a plan candidate, ready only while signed in; it never falls onto credit, and options retain an unknown quote. It uses the platform’s billing capacity without adding money to Usage. Ordinary key routes remain separate. A plug-in declaring no plan kinds keeps the text-only restriction on its OAuth slots (planOnly). A shared offer’s candidate is funded lent.
Plan calls carry funding naming the selected slot; program calls name the selected program. A program on a plug-in declaring plan render kinds ranks last and is never the first-ready default, while an explicit choice remains honoured. The dialog, Settings and Auto-generate name the plan route; its price says “through” that plan rather than promising free use. Render preparation records it as a plan route.
The optional sibling oauth block on the platform defines its authorization, token and revocation
endpoints, issuer, scopes and required scope, public client id, extra authorization parameters,
output-cap behavior and plan label. An incomplete block makes the platform unusable, with a diagnostic
at the field concerned; it never changes the platform’s existing auth.kind or its key route.
ctx.signIn({key}) and ctx.signOut({key}) are settings-action doors for the plug-in’s own
OAuth slots. Sign-in opens a fresh IPv4 loopback callback with PKCE, state, nonce and an
installation UUID, kept in preferences and reused across attempts. The exact redirect address is reused in the code exchange.
The optional redirect path defaults to /. With a declared callback origin, the listener answers an OPTIONS preflight with 204, that origin, GET and private-network permission; a GET on the declared path carries the same allowed origin. Other requests receive 404 while the listener keeps waiting.
The ID payload returned by the token endpoint must match the configured issuer, client
audience, expiry and nonce, and the granted scopes must include the required scope.
An existing client or account cannot change without sign-out. The credential and its
account/expiry key record are committed under the slot’s lifecycle lock; a refused store
never produces a signed-in record. Sign-out revokes once and clears both records only
on HTTP 200. The callback and exchange end on Stop or the action’s existing wall.
Codes, verifiers and tokens stay in Dart and never enter JavaScript or logs. Sign-in
logs its start, authorize URL, callback, exchange metadata, validation comparisons,
storage and refusal/failure stages with elapsed time. Credential and metadata writes are distinguished; a failed metadata write logs its error and stack, and a malformed token names its field without logging credentials. Sign-out logs its endpoint and client before sending, revocation status
and server text, record removal and completion, or the stage that failed. Endpoint
requests follow no redirects, and a browser closed by the person is not detected.
An OAuth slot’s Provider keys card reads account and expiry from the manifest view’s existing key record, without opening the credential store. Its Sign in and Sign out buttons call the host doors for that declared enabled slot directly, without a plug-in action or inferred name. A cleared record displays Sign in exactly as a slot never signed in. A refused refresh records its reason in refreshFailed/cleared log events and shows its terminal explanation in the dock. That explanation takes precedence over the record removal’s stale status; the session still becomes stale and sends nothing further. No card history is retained.
The process capability permits program doors. A plug-in without it is refused before any program action and logs plugins.capability.refused and plugins.process.refused with the door, arguments, reason and elapsed time. Consent names each program setting and says: “It runs a program on this computer that you choose on its page, as you, with your access.”
A program setting is an executable path chosen by the person and kept on this computer. It is refused in an entity’s settings or with any scope other than machine. Its optional names lists executable names without extensions. The plug-in page looks for those names on PATH, including PATHEXT on Windows, and offers Use this beside the found path; only accepting it or typing a path stores permission. A manifest default is reported as badEntry and dropped. A stored path that no longer names an executable file is shown inline. Discovery logs the names, found path and elapsed time; a missing executable and every stored setting change log the path as it is.
A program setting’s optional parallel names a machine-scoped number setting of the same manifest, declared in any order: the number of copies of the program that may run at once, the plug-in’s default and the person’s stored value, edited on the plug-in’s card as any number setting (not a string → wrongType and the program is dropped; naming no machine number setting → badEntry and the program keeps no lanes). The host reads it once per decision (copiesFor): the stored value, else the default, its integer part, below 0 read as 0; 0 is one at a time, and the resolution is logged (plugins.copies.resolved with the setting, the stored and default values, the number and its reason: person, default or none). Where the number is above 0, each operation names a separate host-owned process lane. At 0, a start that names an operation owns the single process for that operation at once — logged owned {reason: start} as a lane’s start is — so that a call which starts and only receives (a Check) has its copy stopped when the call ends (ownerEnded), and a start without an operation is claimed by the first send, as before. Start requires a nonempty operation and owns the lane immediately; starting the same live lane answers already: true. Send, receive and release select that lane by operation. Stop with an operation ends only that lane; stop without an operation, disable and exit end every lane of the setting, including starting processes. A program-path edit, and an edit of the number a program’s parallel names, publishes the accepted value before awaiting cleanup of that program’s old copies (settingChanged), so a lane started during cleanup uses the new program and number. Concurrent edits of other machine fields are preserved. At 0, or without parallel, the setting retains its single process and existing ownership refusals. The Codex program setting names its number (codexCopies, default 3); Claude’s has none, so its chats run one at a time; the text chats keep separate session operations and tool-loop state, while status uses its call frame. These lanes isolate process ownership. A root completion or render on a resolved program whose number is above 0 skips the per-plug-in slot, allowing chat and render processes to overlap. Key routes, status, options, lifecycle hooks and work on a program at 0 retain the serial slot; every root still takes one of the app’s five slots. No pool, idle reuse or cap at the door is introduced: the number bounds pictures through the render queue (below). Process logs carry the lane on every lifecycle and traffic event, the configured number on plugins.process.path, plugins.process.starting (copies) and plugins.process.start (configured, beside the live copies), and start records the parallel decision and reason. At 0 copies, a program render waits outside the plug-in and app runner slots while another operation owns the live single process. Process end or the job’s Stop wakes it; release does not. The wait and its outcome log the plug-in, setting, job, owner, pid, decision, reason, code and elapsed time. A stop naming an operation refuses busy when another operation owns the process, without ending it or its relay; unnamed or unowned stops retain their existing behavior.
ctx.process.start({setting, args, cwd?, operation?}) starts the executable currently stored by the person, never a default. Without parallel lanes, each plug-in and program setting has one host-owned process across JavaScript calls. An unset path is notSet, a path that is not executable is notExecutable, and starting an already running process answers already: true. Linux starts an owned process group; Windows runs through the shell so that a .cmd shim works. Arguments travel as a list. Without cwd, the host creates and uses the stable private plug-in data folder process/<setting>/; an explicit directory is used as given. A fresh start answers {already: false, pid, cwd} with that chosen directory, so a program protocol can pass it explicitly; plugins.process.done logs the answer whole.
An assistant complete may start with tools: true; its answer adds tools: {url}. If minting the relay fails after the program starts, the door logs the original failure and reason, stops that owned process tree, then rethrows. The host opens one IPv4 loopback listener on first use, with a URI-encoded /mcp/<operation> path and a random bearer per operation. The bearer never enters JavaScript: the plug-in sends Bearer <filmopen-tools-token> in its FilmOpen MCP server declaration, logged whole, and the process door substitutes only that server’s authorization header after logging. A line with no substituted header reaches the program unchanged. The relay logs request headers except Authorization, methods, ids, arguments, results, decisions, elapsed times and failures. It accepts POST only (405 otherwise); an unknown operation or wrong bearer is 401. It answers initialize, notifications/initialized (202), tools/list from the assistant policy’s table, and ping, with -32601 for other methods.
A relay tools/call is queued on the operation’s existing program stream as {filmopen: "tools/call", id, name, arguments}. Its FilmOpen id is a string minted within that operation; each HTTP response retains its own original JSON-RPC id, even when chats reuse a number. The plug-in yields the call to the assistant’s existing tool loop and sends its answer as {filmopen: "tools/result", id, success, content}; the process door answers the waiting HTTP response instead of stdin. An ended call is refused/unknownId. The relay checks the assistant turn’s control and binding, independently of a JavaScript frame ending at a yield. Turn end answers accepted calls once with an error reason, drops queued relay lines and voids the bearer before the protocol interrupt; process exit or stop does the same. Only the admitted invocation whose policy minted a relay ends it at turn end; an invocation refused before admission ends nothing. Process loss ends its operation’s relay as processGone; an explicit process stop uses its own reason, such as called, exit, ownerEnded or settingChanged. End is idempotent. There is no relay timeout, retry, body-size bound or second tool executor. A tools start outside an assistant complete is badArgs/tools.
The xAI plug-in’s Grok Build text program starts ACP with the returned directory and the system preamble and action prompt as its system context. A completion carrying tools requests this relay and a profile limited to searching and using MCP tools. A completion without tools, including an image-prompt writer, requests no relay and sends an empty MCP server list with an inline profile offering no tools. The tool decision is logged with the declared tools and its reason before the process starts; the session log records its profile and the relay URL or none. The film’s fenced blocks travel in the fresh turn’s prompt, as on the Codex route. Later turns load the saved session and send only the latest user message. Setup replay is logged without entering the answer, notes or usage. A tool yield keeps the process; its continuation answers the relay, while Stop cancels the original prompt and drains its terminal answer before stopping the owned process. The reported terminal input, output, cached and reasoning tokens belong to the Grok Build plan; USD ticks are logged without billing returned. Its status reads the existing program sign-in with models, without a model turn. On calls carrying tools, the person’s other MCP servers remain searchable.
send({setting, line, operation}) writes one line and the first send makes its operation the owner, unless the start named one, which owns the process from the start. Another operation’s send or receive is busy until release({setting}) frees the owner. receive({setting, operation}) takes the next complete stdout line, or answers exited: true with the exit code. It waits without a timeout and ends when the calling control is cancelled; cancellation leaves a process no operation owns alive; one owned by the cancelled call’s frame is stopped with it (ownerEnded). Output is queued whole, stderr is logged separately, and a crash is never restarted automatically. stop({setting}) closes stdin and sends TERM to the Linux group, then KILL after two seconds if it still lives; Windows ends the tree with taskkill /T /F. Disable, a hung call, a failed switch held off for the session, a change to the program setting, and application exit stop the owned processes, including children. Every process operation logs its inputs, path, arguments, working directory and whether it was defaulted, pid, whole input/output/error lines, ownership and release, refusals and reasons, exit code and elapsed time. No provider protocol or file-reading door is part of this transport. When a call ends done, failed or stopped, any process still owned by its own frame id is stopped and its queue discarded with reason ownerEnded, including a process in another plug-in started by a nested completion. The existing call slot stays held until that cleanup ends.
Every call receives its frame id in ctx.operation, assigned and logged before the context is built. Status and render use it for their program lane. A frame-owned lane stops at ownerEnded, including one never sent to. Text complete and interrupt retain their session lane in args.operation; their session handover is unchanged. stop releases ownership; release on that stopped pair answers notRunning. A normal reply still owning its process hands over as unreleased, rather than failed.
Every text complete receives an operation: the assistant session’s id, including finalization, or the calling frame’s id for another call. At an admitted turn’s end, an operation still owning a process is handed once to the text plug-in’s optional interrupt({operation}). Stop, a failed call, a third repeated tool refusal and a stale session take this same path; an operation already released takes none. The interrupt is a root call with a fresh uncancelled control and its own Usage ledger, waiting for the existing per-plug-in slot. It writes the program’s protocol interrupt, consumes its terminal line and returns {usage}; its ledger records that answer separately from every completed call’s ledger. The host then releases the owner. Ordinary Stop keeps the program alive when the interrupt succeeds. An absent or failed interrupt, or one reaching the ordinary call wall, stops its owned processes and discards their queues, with stop reason interruptFailed. No repeat-Stop control is added. If handover cleanup throws, that failure is logged and propagated while the existing turn control and policy are still cleared, leaving the backend free for another turn. assistant.turn.interrupt records session, plug-in, declaration, done, failed, absent or wall, the turn-end reason, returned usage and elapsed time.
VisualStyle holds the treatment’s five primary leaves as Observed text: camera, camera_mount, lens, depth_of_field_prompt and color_grading. A description may now propose them through the same treatment-leaf gate: text is trimmed, limited to 4,000 characters and refused if it contains a control character other than tab or a line ending. A null or empty text leaves the current value alone; an invalid leaf refuses the whole description. The existing depth-of-field scale, inheritance and prompt composition are unchanged, with treatmentPromptLeaves remaining the one ordered list of the five.
The assistant contract. An assistant declares its required catalog under assets/ and exposes no assistant functions. The host runs its catalog actions (§6.11a). The host subsequently owns microphone, playback, sockets, interruption and cleanup under the native conversation contract. Opening captures binding only; explicit Start admits a ready funded session. Finish draft stops capture and spoken output, drains already-admitted captions for at most five seconds, closes resources and freezes the visible transcript. End or Close tears down immediately without finalization or writes. Native sessions have no cap of the app’s on their length and require explicit restart.
Text start and turn use the frozen, verified text completion route, with transcript and session state owned by the host. The host voice finalizer consumes the visible transcript after cleanup; an incomplete transcript is labeled and requires an explicit choice to use it. Its proposal is bounded to one file object whose $id is the target’s id, changing at most the target’s field — a string, of any length — and removing nothing; an import that changes nothing proposes nothing. The person reviews and applies the proposal in FilmOpen.
The assistant’s limits are the model’s: the host keeps one in-flight text/finalization request and asks for the chosen model’s catalogue max_output_tokens through the provider’s verified output parameter, none where the catalogue names none; where the action names an effort (low, medium or high, the catalog’s) and the route’s catalogue row maps reasoning (OpenRouter’s and OpenAI’s reasoning.effort), each call of the turn and of Apply carries reasoning: {effort}, the call policy admitting that value and no other, and none otherwise, the model’s own default then standing — each call’s line logs effort and effortFrom (action, none, unmapped), and bytesBy the size by part — the opening’s (system, context, tools, fields) on assistant.open, and each model call’s (system, context, history, tools), measured as it is sent, on its assistant.turn.call and Apply‘s on its stage —; assistant.open.select names the transport (text, voice-openai, voice-grok) and where the fields’ guide went (tools or context); it counts no turns or model calls, measures no request, answer, partial or note against a bound of its own and keeps no clock on a turn. What ends a turn is the model’s answer in words, the person’s Stop, a stale binding, the third same refusal of one tool, an output cut off, a provider’s refusal (its context limit among them) or a network call’s own bound. Nothing is truncated: the activity block goes whole, and Apply’s answer is proposed whatever its length; roughly 300 words for a synopsis is prompt guidance. Cancellation closes only owned work, with no automatic paid retry or fallback. Finalization has its own Usage chain through the frozen nested completion operation; provider units, charged failures, estimates, confirmed charges, confirmed zero and unknown remain distinct. Usage is not a field of the finalizer’s answer. The schema does not enforce byte, turn, identity-equality or time bounds. Account/funding doors and the host’s preparation mode are separate runtime work; FilmOpen-credit conversation remains unavailable until an accepted authenticated metered relay exists.
The exchange doors. The contract settles three members of ctx.project, which the host answers (exchange_doors.dart): scopes(), the scopes export takes (§6.18’s registry, each with its argument); export(scope, arg?, options?), the film as the viewer has it in use, as one scope selects it, in exchange 1 — kept under its export_id for the call, at most four, or for the assistant’s operation; and import(document, options?), which plans a document against the versions in use and queues it for the person’s review, answering the plan (ImportAnswer: the state, the proposal’s id, and per patch its action, target stem, paths, removals, notes and problem — ids, paths and codes, never a document); options.base names a kept export and dryRun plans without proposing. The contract lists the codes and kinds the package raises (§6.18) and the rule that makes the lists safe to grow: a problem code a plug-in does not know is a refusal, a note kind it does not know is information. The outcomes of the person’s apply never cross the door. Exchange 1 is bound to these doors for as long as they exist. Their refusals are a code and a reason: scopes refused/noProject; export badArgs/scope and arg, refused/noProject and tooLarge; import badArgs/document and base, refused/noProject, otherFilm, noViewer, inCheck and tooLarge. An answer is bounded at 8 MiB of UTF-8 (exchangeAnswerMost) and the reason stays tooLarge whatever the size; a request is bounded by the engine’s 16 MiB. filmopen_js names the three doors in doorNames and the prelude.
The exchange binding and retained exports. The film and the viewer are the call’s binding (§6.11): the three doors read the bound film for the bound viewer and nothing of another. A door proposes and never writes: import plans with the bound film’s guards and holds the proposal through the state’s seam, ImportProposals.add, with its origin — the plug-in’s tag, the function and, under the assistant, the operation — and an id of the door’s own (p- and sixteen hex digits, never derived from the content); the person’s Apply is the only write. The answer is measured before the proposal is held, so an answer refused tooLarge leaves none behind, and the size goes to the log as plugins.exchangeTooLarge, never into the reason. Every running plug-in call may use the ordinary exchange doors; import remains refused inCheck in a check. Ordinary exports are kept on the chain’s top frame (CallFrame.keptExports): at most four distinct export_ids, the oldest dropped and an export repeated moved to newest, released when the top call ends. Under an assistant operation, exports are kept by ImportProposals.retainExport under its operationId and go with it (release). A document carries a file’s notes wherever its projection carries the whole content (whole, everything). Each dispatch logs plugins.exchange.start, then .done, .refused or .failed, with the plug-in, door, operation, arguments, answer or failure, and elapsed milliseconds.
A plug-in is one folder — plugins/<tag>/ in the shared library, or <tag>/ in a developer’s configured development folder — holding its manifest pg_<tag>_<author>_v<n>.json, its code .js beside it, l10n/<locale>.arb, and optionally README.md, platforms/<id>.json and assets/ (Project Specification §14.4). plugins/README.md is the reference a plug-in’s author and a coding agent work from — where a plug-in lives, the roles and the names, the manifest field by field, every member of ctx and every function a plug-in may expose, each marked built or planned, then the limits, keys, servers, calls, strings, trust and the developer page. plugins/REFERENCE.md holds the shapes — every function with its arguments, its answer, its example, who may call it and whether it is built — and plugins/filmopen-plugin.d.ts says the same in types, with what is planned in a namespace of its own so that nothing there reads as callable; both are generated from the interface definition (below), and the plug-ins repository’s template/ is the plug-in to copy. The generated documents are read against the definition by a test (§12, reference_test.dart) and the host against it too (contract_test.dart): every door filmopen_js’s doorNames lists is a member of ctx and none is missing, what a running call is given is what the definition says ctx carries, and the arguments the host builds for a function are valid against that function’s own args.
A manifest may provide docs, a guide path such as /docs/platforms/comfyui/. It starts with /docs/ and has nonempty segments of letters, digits, hyphens or underscores, with an optional trailing slash. A scheme, host, query, fragment, backslash, percent encoding, dot segment or repeated slash is refused: the field is reported and omitted, and the rest of the plug-in remains usable. FilmOpen supplies the documentation origin for the build, docs.dev.filmopen.ai in development builds and docs.filmopen.ai in release builds; a package cannot select a different host.
The contract, as data (plugin_api.dart; its checker JsonShape is the format package’s json_shape.dart, beside the records it also holds a model’s content to, re-exported by filmopen_plugins’ barrel). plugins/filmopen-plugin-api.json is the one place the interface between the app and a plug-in is written: $defs (the shapes that recur), everyPlugin (status), interfaces (each with what a manifest adds when it declares it, and its functions — required or not, who may call it, args, returns, an example, a state and the revision it arrived in), beyondInterfaces (actions and offers) and ctx. It is JSON Schema, draft 2020-12, in the keywords it uses and no others — type, properties, required, items, enum, const, oneOf, additionalProperties, minimum, maximum, description and $ref into $defs — so that a standard validator reads it as it is: JsonShape knows exactly those and anyOf, minItems and maxItems, which the format’s records use (§5), refuses a keyword it does not know where a schema is read (JsonShapeException, naming the keyword and where it is), and reports a wrong value by its path as a code and never a sentence (outputs[0].mime: missing), a oneOf answering with the problems of the shape that came closest — of the shapes the value’s own tags leave, the one with the fewest problems, a tag being a property that two or more of the shapes hold to fixed values in their own properties, such as state or kind, which rules out a shape whose values the value’s is not where another shape takes it; a oneOf inside a shape chooses within it — so that the path is one an author can go to. An integer is a number with no fraction, as a standard validator reads it. PluginApi.parse reads the file whole and refuses it whole: every $ref resolving, every function carrying its arguments, its answer, an example, its state, its since (never after the file’s own revision) and who may call it, and every example valid against its own arguments. The app reads it as an asset of its own — plugins/filmopen-plugin-api.json, bundled beside the stock plug-ins — at the host’s first load; a build that cannot read it runs no plug-in and says so in the log (plugins.apiMissing, plugins.apiUnreadable) rather than reading manifests against a contract it does not have, and a test of the root package pins the asset. ctx.api is the file’s two numbers, { api, revision }, so that a plug-in asks for a door only where the revision says it is there, and ctx.caller is { tag, stem } of the plug-in that asked, null where the app or the person started the call. The contract settles options with its per-stack readiness and quote, a job’s pending, collect and cancel under ctx.job, media sent by a descriptor’s grant (ctx.upload, an HttpRequest’s embed) and inline outputs kept behind ctx.stage‘s handles, a call’s Destination, Usage with its Billing basis, and four interfaces — reference-analysis, style-analysis, voice and server-provider; three members of ctx are built already: ctx.now, the host’s clock in milliseconds when the call started, ctx.job, the render job a call belongs to — its render, its collect, its cancel, and every call they make — and null for any other, and ctx.features, what the host bridges for a plug-in — nothing: it answers an empty object, a server provider implementing server-provider, and no inline output is bridged. ctx.stage and an output naming a staged handle are built, as The film’s media coming back says; ctx.upload and a request’s embed are built, as The film’s media going out says; an answer’s usage and its billing are recorded as §6.7 says, a failure’s too. The lifecycle interface holds onEnable, required of a plug-in that declares it, and onDisable, both called by the app alone, with LifecycleEvent and LifecycleResult and the manifest’s discovery routes. The app shows four members of Status.details (Turning a plug-in on and off, below). A function of the definition is never taken as an offer, so an offer of one of the capabilities’ names is refused (reservedOfferNames), and the server provider’s functions have names no platform’s offer uses. plugins/filmopen-plugin.d.ts and plugins/REFERENCE.md are generated from the definition by python3 scripts/gen_plugin_api.py, never written by hand, and its --check runs in scripts/test_all.sh and scripts/test_all.ps1; plugins/README.md stays the guide, written by hand, and states no shape the reference does not: every shape it writes outside its examples, a function’s arguments among them, is one of the definition’s objects — whole unless it says …, each field optional exactly where the definition says — and it names neither hooks nor provides, which reference_test.dart holds it to.
What a manifest declares it implements. implements is the one place a manifest says what the app may call it for — { "render": { "platform": "comfyui", "models": ["t2i-z-image-turbo"] } } — and hooks and provides are unknown fields, reported as any other (unknownField) and not read. Each declared interface is read against the definition: one it does not know is reported by name (unknownInterface) and left out with the rest working; one declared without what that interface must add — render with no platform — is reported and left out, since the app could neither route it nor say where it goes; a declared value of the wrong shape is reported by its own path, the definition’s declare schema deciding. ImplementedInterface carries the platform, the catalogue entries it speaks for (an id the catalogue lacks is kept and said on its page) and the functions the interface holds that this build can call — a function the definition marks planned is listed nowhere, the developer page’s list included — copied from the definition as the manifest is read, so that the host asks implementsFunction('render') without the definition in hand. What another plug-in may reach (§3.3) is plugInsMayCall’s, read from the definition at each call: the functions of those interfaces whose calledBy names plugins, and every plug-in’s status. minRevision is the least revision of the contract the plug-in needs; above this build’s, the registry drops its code, the plug-in is listed with needsNewerApp and does not run, and its page says which revision it asks for — a sentence drawn only where the revision is past this build’s, so that a plug-in which cannot run for another reason is never told it needs a newer FilmOpen. The contract is revision 1, and an absent minRevision is read as 1. A plug-in reaches its render and voice functions only when minRevision <= api.revision (admitsRevision), checked at the one guard every road to a call passes — a person’s call, a capability, a render job and the developer page each name the interface they resolved, and an offer, which can carry neither, names none — before anything is sent, and a contract not yet read admits none; every plug-in read is logged at info, plugins.revision.admitted or, listed needsNewerApp, plugins.revision.refused, and a call refused at the guard plugins.revision.refused, each with the revision needed and had, the decision, its reason and its code (and, at the listing, the read’s milliseconds), and the registry’s read line carries every plug-in’s. The floor below which a package that declares render or voice is listed with updateRequired (Update required: written for an earlier FilmOpen.), keeping its code and its other functions, is revision 1, so that no package is listed so today. capabilities.upload lets a plug-in send the film’s media a call of it is handed, through ctx.upload and a request’s embed (The film’s media going out, below): consent names it — only the pictures, clips and sounds FilmOpen hands to a call it takes part in, never another file of your film — and the plug-in’s page says the same; a plug-in without it is refused noCapability, by either door. x is anybody’s: kept as written, reported never, shown nowhere. An offer may carry args and returns, schemas of the kind the definition writes, which the developer page holds a call to; one this checker cannot read is reported and left out, the offer still readable. No offer may take the name of a function of the contract — every interface’s and every plug-in’s status — nor a name the language gives every object, nor a function one of the plug-in’s own actions calls: a function of an interface is reached because the manifest implements that interface and the definition says who may call it, never because it was offered.
The render container. Every render result, including voice design and speech, and each of its outputs, carries a metadata container: an object with an integer v, complete at every answer and never a delta, holding no credential, authorisation header or expiring address. Each output adds its provider id, its seed, role and mime, and width and height, integers where present. render, designVoice and speak take stage (prototype, draft or final) and, where they continue a take, from, the take’s complete descriptor, both optional in the definition; a plug-in that rebuilds its arguments keeps both. A failure answers error with a code and a message a person can read. KeyInfo adds usable, mode (own or funded) and reason, so that a plug-in asks the door whether it may call and never refuses on hasKey; the host answers them at the keys case from the person’s own key — usable a key stored and allowed, mode: own, reason absent where usable, else needsKey where none is stored and grantDenied where one is stored and not allowed — the funded route later. The manifest may declare sharing, the operations it could offer to others with their unit; PluginManifest.sharing reads and checks it, a wrong shape — scale without character among them — one wrongType at its path; lending (Lending, below) acts on its render operations. The current shapes are required: options answers stacks and params, and an output is an https address, a path on a server the person named or a ctx.stage handle, never data:. The describers (fo-openai and fo-fal) implement reference-analysis and style-analysis.
Exposed offers, and the text tool-turn shapes. An offer (OfferSpec) may carry expose, a public kebab name (^[a-z0-9][a-z0-9-]{0,47}$, declared by its author, since an offer’s own name is camelCase; a second offer of one plug-in exposing the same name is badEntry and reads unexposed, as does a name of the wrong grammar), description, a string key resolved as label is (at most 4,096 characters; label serves without it), effectful (default true: the call takes an operation key, not a consent) and the annotations readOnly and destructive; beside the parsed args and returns it keeps the raw maps (argsSchema, returnsSchema), only where they read. ExposedOffers (exposed_offers.dart) registers, on every change of the host, each enabled plug-in’s offers with expose and a raw args as commands of the state’s CommandRegistry (§7.3), replacing the plug-in’s commands whole, so that one turned off or removed has none: source the plug-in’s tag, name the expose value, exposed: true, the flags from the offer, the title from label; the input is the envelope {input, session, operation} with the offer’s raw args under input as it is, closed to anything else (a non-effectful offer’s has input alone), so that the offer’s own fields are never read as controls; an outputSchema only where the raw returns is an object schema. An offer without a readable args, one whose resolved description is too long, and every offer of a plug-in tagged builtin or app (the app’s own sources) are skipped (plugins.exposeSkipped {tag, name, reason: schema|description|reserved, description}), and a registration the registry refuses is logged with its tag, name, code and the registry’s words (plugins.exposed.refused); the plug-in loads and runs as before. The command’s handler re-reads at dispatch that the plug-in is on and the offer still exposed (not-enabled, not-exposed), then runs it through HostPluginRuntime.runExposed, which binds the run first, then waits its turn — its plug-in’s one call at a time, for at most 30 seconds, then one of the app’s slots, as every call does — then passes the registry’s effect gate (admit for an effectful offer; check, which admits nothing, for the others, so that a caller gone or a deadline passed runs nothing), then runs with that binding, nothing awaited between the gate and the start: an authority change before the gate is refused there and nothing runs; one after it cannot reach the binding, and the admitted effect runs to its end under the plug-in’s own wall time, the caller’s deadline being the gate’s. Two calls of one plug-in at once thus run one after the other; a call still waiting at the bound answers busy having admitted nothing, so its operation key runs on a later call (plugins.exposed.wait {ref, ms, gotTurn}, with the outcome’s code on every way out without the turn: busy, disabled when the plug-in is turned off while it waits, cancelled when the call is stopped while it waits, or a refusal before the wait). The run is otherwise an action’s: its own frame and usage chain, ctx.caller null, a nested ctx.call under its frame; it may spend through any door the host has, at any price, recorded by the usage chain as for the person’s button, and nothing is added to stop it. Only input reaches the function. A done answer is held to the offer’s returns by its JsonShape — a breach answered bad-answer and logged with each problem and the answer (plugins.exposed.badAnswer) — and answered as structured (an object as it is, any other JSON as {value}) with the JSON as text; a failure answers its code, and a refused one its fixed reason beside it ({error: "refused", reason}), which the done line logs too. Each registration logs plugins.exposed.register.start {plugins} with the stems, one line per offer — plugins.exposed.accepted {tag, name}, plugins.exposeSkipped {tag, name, reason, description} or plugins.exposed.refused {tag, name, reason, error} — and plugins.exposed.register.done {count, skipped, refused, ms}, or plugins.exposed.register.failed {type, error, ms} on an unexpected throw, the next change registering again. Each call logs plugins.exposed.call {ref, tag, name, input, enabled, exposed}, wait {ref, ms, gotTurn, outcome?}, frame {gated, admitted} and done {ref, outcome, reason?, answer?, message?, usage: done|unknown|none, ms}, never the session. The text tool-turn shapes are defined beside Message and CompleteResult, which do not change: ToolTurnMessage (an assistant’s tool calls, a tool’s answer by tool_call_id, or a message as Message has it), ToolCall, ToolSpec, ToolCompleteOptions (tools, tool_choice) and ToolCompleteResult (text, tool calls or both); nothing passes them yet, and their limits — an id of at most 64 characters — are written for the loop that will hold them; the loop bounds neither the count nor the size of its tools. A $defs entry carries no since, so each says its revision in its description.
The catalog assistant and tools in the text interface. An assistant declares its required catalog in implements.assistant.catalog, a path under assets/ (format 1, §6.11a), and exposes no assistant functions or catalog hooks. The host reads the declared catalog first and x.assistant (version: 1) only where the manifest declares none. A refused declared path or shape is not retried through x (assistant.catalog.refused {location: manifest.implements.assistant.catalog}). A refusal logs its code, its location and the value written there, a declared path as the manifest holds it, and, once the catalog file is read, its bytes and its text whole; a read logs the catalog’s path, its bytes and its text whole beside its counts. assistant.catalog.start carries source, declared or x. A text provider may declare tools: true; complete takes ToolTurnMessage messages and, for such a provider, tools and tool_choice, and answers a ToolCompleteResult — every Message is a ToolTurnMessage and every CompleteResult a ToolCompleteResult, so a plug-in written to Message reads and runs as before; ctx.ai.complete passes no tools.
The model picker and the prompt. The app asks the built model-picker interface for defaults by render kind and stage, and the built prompt interface for a character-reference or character-sheet prompt from the selected entity’s projected, escaped exchange object, including its open draft. A writer may declare catalogue entry ids in models and character targets in targets; absent declarations are generic (a prompt writer plug-in declares writers instead, below). Among enabled, runnable candidates, a matching declared plug-in wins over a generic one, then the one later in plugins.enabled (stems), then the lower tag (choosePlugin); the picker uses that ordering, and the prompt writers have their own (below). A writer with no text answers {prompt: null, reason: "no-prompt"}; unexpected failure remains a failure. A render plug-in naming no models is offered entries whose access row on its platform has render: true, including those needing film media where it declares capabilities.upload; output follows the entry’s kind. Before dispatch the host freezes the first frame and the first reference image with a bounded copy and digest; uploads verify those frozen bytes, so replacing the original cannot redirect the call. Picker and writer diagnostics record the selection, the values asked and answered — the writers, the source, the arguments handed, the words — the outcomes and elapsed time.
The prompt builder. The declared prompt-builder interface is the one with no function, which the parser admits where a declaration is required. The catalogue view reads an enabled plug-in’s file under assets/ (pluginFile, through assetInPackage) and an English-backed string (pluginString), without running it. File reads log plugins.file.start and a completion (plugins.file), refusal with its code (plugins.file.refused) or failure with its stage (plugins.file.failed), each path as it is, a failure with the error’s message, and each terminal event timed.
The prompt writers. The prompt interface’s targets are the eleven kinds a prompt is written for — character-reference, character-sheet, location-reference, location-sheet, outfit-reference, outfit-sheet, prop-reference, first-frame, last-frame, voice, treatment; no writer can list another, so another kind finds no writer, and the capability door the developer page shares refuses it badArgs/target (capabilityArgs). A model and its catalogue entry are sent only where one is chosen: entry is required with a model and absent without one, so a voice, a treatment and a prompt written before any model is chosen are asked with neither. For first-frame and last-frame the call names a block of the source scene by its id and the host passes that block, as the scene holds it, as block — refused badArgs/block where the source is no scene, its blocks are not a script, or the id names no block or two (§13.3). For treatment the host passes the effective visual_style.preset as preset — the folder’s own, else its nearest ancestor’s, else the project’s, as the bound viewer reads the film (treatmentFor) — absent where none is chosen; a writer may use it as it sees fit. An absent argument is left out, never sent as null. The writers. A writer plug-in declares writers in its prompt interface — each {id, label, target, kind, asset, models?, llms?, estimate?}: id unique among its writers of one target, label a key of its strings, kind template (it fills its asset’s words in alone) or llm (it asks ctx.ai.complete), asset the file under assets/ it reads, models the render models it is for where it is for some alone, llms an LLM writer’s preferred text models, catalogue ids in order — and no longer models or targets of the interface; a manifest declaring those alone is refused its prompt interface (unknownField implements.prompt.targets and .models, missingField implements.prompt.writers). Every issue the reader records is logged at each reload — plugins.manifest.refused with the reload’s generation, the plug-in’s tag (and stem where a manifest runs), the file, the field, the code, its arguments, the app’s sentence for it where it has one, and leftOut: true where no manifest of the plug-in was kept and the plug-in is left out whole, false where it loads without what was refused — and a plug-in left out because its manifest or its folder could not be read again is plugins.registry.readFailed with the stage and the error; so that what is missing is explained by the log, not only by the plug-ins page. The writers for a kind (PluginInvocation.promptWriters, promptWriterOrder, promptWritersOf) are every enabled, runnable plug-in’s writers of the target — one that names models left out only where another model is asked, so the Prompt card, which asks none, offers it — fo-prompts‘s first, then the other plug-ins’ by tag, each plug-in’s in its manifest’s order. A writer is named by its label in the person’s language, alone where one plug-in answered, <plug-in name> / <label> where two or more did, decided once in the host. An LLM writer carries the text model decided for it from its llms (decideLlm: Use the LLM: where set, else the first reachable of its preference, else the first reachable text model) as (id, name, rate, quote): the rate from the chosen platform’s access row, per unit its first input and output price per million tokens that holds unconditionally (no when), else — where every price of that unit says when, as OpenRouter’s do — the unit’s lowest, as the provider’s own price reads it, written <in>/1M in, <out>/1M out (promptLlmRate); unknown where the row has no price at all for one of the two units; prompt.writers logs where it came from, rateFrom: plain | lowest | none; none where no model is reachable. Its quote is (input_tokens × inUsd + output_tokens × outUsd) / 1,000,000 USD as a decimal string, null without an estimate or either rate; prompt.writers records the whole estimate, rate and quote. The writer’s own price (estimateCost of the prompt interface) is gone. buildPrompt runs the pair the person named — plugin and writer, the writer’s id; a plug-in alone its first writer of the target — failed notFound where it is not among those offered, else the first of that list, refused/noWriter where there is none, so the commands’ automatic writing and a person’s choice share one default; it hands the writer its declaration as the manifest declares it, as writer, since a plug-in does not read its own manifest, and the LLM decided for it from its llms by the rule the list used, decided once for the call and frozen as llm; an optional accepted record is the LLM record the caller received, checked against the live decision before execution: a changed id, rate or quote refuses refused/decisionChanged; the name and route are not compared, and no accepted record preserves the current behavior; its answer is the writer’s, negative included. Log: prompt.writers (the target, the model asked, each writer offered — its pair, name, kind and label key, and for an LLM writer its llms, the model decided, its name, its estimate, rate and quote, the platform, why it was chosen and why none was —, the plug-ins that answered and whether names are prefixed, the order’s reason: stock, only, lowerTag or noWriter, elapsed); render.prompt.start (the target, the model, the pair asked, the accepted record, choice — named or default —, the block, the covered blocks, the tokens and the source’s stem and $id); between it and the end plugins.choice (the pair chosen, the frozen llm, the executed record, live platform and funding, the pairs chosen from and because: the order’s reason, or named or notFound for a named writer) and render.prompt.args (the pair and the arguments handed to the writer, whole, as sent); render.prompt.done (the pair, choice, the prompt’s words and its negative, a failure’s code, reason and message, and both accepted and executed records) and, for an unexpected failure, render.prompt.failed with its stage, the error’s type and its message. A change of Use the LLM: is told as the host’s change (plugins.llm.setting, the models before and after), as a key and a credit answer are, so that what lists the writers lists them again.
A writer’s turn. buildPrompt takes wait and admitted, handed to the call runner as every call’s: with wait a call that finds the writer’s plug-in running one already waits its turn instead of being refused busy, and admitted, asked once it has its turn and slot, refuses it disabled before it starts where it says no (Auto-generate pictures, §6.15). Such a wait has no bound of its own: a writer waits as long as the calls before it run. render.prompt.start logs wait as given and admitted as whether a check was handed.
The tokens a writer is handed (their selection below). buildPrompt takes tokens — the media the source’s prompt may name for its target, in promptTokenCandidates’ order, each {token, name} as written in a prompt and with the person’s name for it (the chip’s): on an entity’s *-reference prompt its live inspirations (@inspiration-n), on a *-sheet prompt those and then its live reference images (@reference-n), on a frame’s the film’s characters’, locations’, outfits’ and props’ reference images and sheets as the viewer reads them (@<stem>:reference-n, @<stem>:character_sheet-n, @<stem>:location_sheet-n, @<stem>:outfit_sheet-n, @<stem>:prop_sheet-n); none for voice and treatment. The Prompt card asks its owner for them as Generate starts (§9.3); capabilityArgs passes them where given and not empty, absent otherwise, and refuses badArgs/tokens tokens handed for a voice or a treatment and any token the codec does not read whole as one media token (ReferenceCodec). A writer may write any of them into its prompt, and only these; the host does not check what it wrote. render.prompt.start carries tokens, each handed as {token, name}. The revision also corrects the prompt-builder’s summary: a pick is stored and passed to the writers, never added to a render’s words by the application.
The planned parse-script interface. Beside the planned import, which stays as it is, parse-script holds one function, parseScript({film}): film the export of a film the app has just imported from a screenplay, the answer {state: "succeeded", document} in the import dialect of ctx.project.import or {state: "failed", error}, called by the person. Planned: no feature calls it, and the host filters it from calls as it does every planned function; the stock fo-parse-script is its stub.
Two more targets, a clip’s arguments, and no character-transform. PromptTarget holds prop-sheet and clip too. For clip, buildPrompt is handed the block (block), the later blocks the clip covers, in order, as the scene holds them (blocks), the entities the block and the covered blocks name, each projected and escaped as source is and resolved by the scene’s own rules, each once (context), and the block’s effective language, BCP 47 (language); every other target is handed context too, below. A writer’s answer is its prompt and its negative: inputs, suggestedParams and usedSources, which nothing read, are struck. character-transform is removed with the shapes only it used (LeafChange, AfterSave), with no backwards compatibility: the name transformCharacter is no longer reserved, so a package that still offers it loads with an ordinary offer that nothing calls. The host sends them (capabilityArgs): the targets it accepts (promptTargets) are the thirteen, prop-sheet among them (a prop’s sheets are rendered from its sheet prompt). block is found by its id as a frame’s is; blocks are the blocks the call names as covered (buildPrompt‘s covered, a list of block ids), each found in the scene the same way and sent in the order named — one that names no block, or two, refused badArgs/covered — and absent where none is named. context.entities holds, in this order and each once, the speakers of the block and the covered blocks, the scene’s cast, the scene’s location, and every character, location or prop an @ mention names in those blocks’ dialogue, direction, action and beats’ descriptions (ReferenceCodec): a character at the epoch its cast entry gives, explicitly, else the scene’s (as speakerOf reads it), the location at location_epoch, explicitly, else the scene’s epoch, a mentioned location or prop at the scene’s epoch, read from the scene’s open draft, each resolved for the bound viewer through the scene (FilmProject.resolve) and exported from the film as loaded, in source‘s shape (exportEntityDocument); one that resolves to none is left out. language is the block’s own lang, else the project file’s, absent where neither has one; a language the call gives is not used. The hand-over to frames and other targets is below. Log: render.prompt.context on the call’s ref: resolved, each entity asked with its type, tag and epoch and the stem it resolved to; unresolved, each one left out with the resolver’s own cause (noEntity, noEpoch, noEpochOrDefault, noFile, ambiguous); covered, the covered blocks’ ids; language and languageFrom (block, project or none); and elapsed. The entities’ documents themselves travel in render.prompt.args.
One prompt context and pictures at its epochs. Every buildPrompt receives context: {entities: [...]}. A first or last frame resolves its owning block alone; a clip resolves that block and its covered blocks. The shared promptContext resolver (filmopen_format) reads speakers, cast, location and mentions by the scene’s existing rules above, adding each resolved character’s cast-selected outfit, else its default_outfit, implicitly at the character’s resolved epoch; type and tag are kept once, at first use. A character’s own reference, sheet and voice targets receive its default outfit. An outfit’s reference and sheet targets receive its wearer (the tag before the last dot), implicitly at the outfit’s epoch; an imported unassigned outfit has empty context. Other targets have empty context. Unresolved entities are omitted, with the reason logged. blocks and the covered refusals remain clip-only; language now applies to all three block targets. Block targets receive the scene’s effective treatment, and the treatment target the source’s: treatmentFor’s flat, dotted leaves composed from project, enclosing folders and named Style, even where empty. The treatment target’s existing preset remains.
For block targets the page’s picture candidates first resolve every block in the scene through that same resolver, offering those characters’, outfits’, locations’ and props’ usable media at the resolved versions, then the remaining film entities as before. Own reference and sheet candidates are unchanged. The host validates the offered tokens and selects only from them: inspirations for a reference target; references where any, else inspirations, for a sheet; one picture per block-context character, outfit, location and prop, in context order: its first sheet (character_sheet, outfit_sheet, location_sheet, prop_sheet), else its first reference, recorded first-sheet or first-reference. The same rule hands every block target, last frame and clip included. Voice and treatment have no tokens; nonempty tokens for either remain refused. render.prompt.context runs for every target, on the call’s existing ref and timing: references records each type, tag, requested epoch, explicitness, why and outcome (resolved stem and fellBack, or problem); an unassigned wearer records {type: "ch", why: "wearer", outcome: "none", reason: "unassigned"}. references replaces the former resolved and unresolved lists. The line retains covered blocks and language/origin, and adds treatment leaves/sources, treatmentAmbiguous (whether an ambiguous script position skipped folder profiles; null where no treatment is composed), and selected token/name/rule with the overall selection rule. Existing start, whole outgoing arguments, completion and failure events record the operation around that decision; the payload is logged before credentials are injected, with no mask.
The scriptwriting workflow. A plug-in declares implements["script-writer"].workflow, a required path under assets/, with minRevision: 1; this interface has no functions. Its Workflow file has workflow: 1 and ordered sections for project, folder (optionally by kind) and scene, each with a heading, optional help, level 1–3 and steps. A step names an offer, arguments and fields (text, note, number, choice, flag), optional model ids, a call/token estimate, wall minutes (default ten, at most thirty) and a project-derived done condition. A choice declares its strings; required defaults to false. Labels, headings and help use a matching plug-in string key or their text as written. WorkflowSpec.read checks at most 1 MiB of UTF-8, JSON object, contract shape, known offers, unique file-wide step ids, choices and the reserved field name focus, in that order, refusing the whole file at its first problem and retaining the accepted JSON for consumers. HostPluginRuntime.workflowOf reads through the enabled package’s existing asset door without running code or caching; an unknown tag or absent declaration returns null, otherwise WorkflowLoad carries the spec or a code and location. plugins.workflow.start, .read, .refused and .failed log tag, version and path; the terminal event records elapsed time, full text where decoded, headings and ids on success, the refusal’s code/location, or an unexpected failure’s stage, type and message. Invalid UTF-8 is refused json with its original bytes logged. ctx.ai.complete accepts ordered options.llms below an assistant’s frozen route, with Use the LLM: still overriding; a per-call choice leaves the step default alone, and the answer names model and provider. params may carry reasoning: {effort} and response_format through fo-openrouter. The scopes include scene_dialogue and focus ({folders: [tags], scenes: [tags]}): focus returns the project frame, character/location summaries, folder/scene synopses and the named folders/scenes whole with their descendants, blocks and cast. Import children replaces the project’s or folder’s reference list whole; append to the exported list using its export id as base, with created children before the parent. A project’s $declare: [{label, year?}] allocates eras inside its one content write; a scene cannot name a newly declared era in the same import.
The prompt writer’s estimate. A writer may declare estimate: {input_tokens, output_tokens} for one LLM call; both are required nonnegative integers, and the estimate admits no other key.
A step’s call. The catalogue role’s scriptWriters lists enabled scriptwriting plug-ins by tag, with their translated labels and checked workflows as plain JSON or their refusal code and location. The invocation role’s decideStep resolves a workflow step’s model, name, rate and selection reason without running it, and its quote: the step’s declared estimate at that model’s rate, calls × (input_tokens × in + output_tokens × out) / 1,000,000 USD as a decimal string — null without an estimate or a rate —, which runOffer’s answer carries too; it is what the run dialog says before a run, never a bound (the ceiling is). runOffer addresses that step by id, requires an accepted workflow and a focus {type: pj|fo|sc, tag} with a nonempty tag, and overlays the caller’s filled arguments on the action’s declared arguments. It decides the model from the step’s llms, with Use the LLM: winning, and grants only the checked minutes: a grant replaces the runner’s wall up to thirty minutes, while no grant keeps its default. Nested calls keep their own wall except complete, which has no wall-clock cap. The answer carries the outcome, decided model, optional text and decimal USD spend summed from each ended ledger chain’s latest saved record; a missing cost or another currency makes the spend unknown, and no chains means known zero. An answer whose state is refused or failed is that outcome, never done, its spend still reported: its reason the error.code, else the answer’s own top-level code, and its words preserved as an action’s: its top-level message where it is a key of its strings, else its error.message, else its message as written. The plugins.workflow.writers, plugins.workflow.decide and plugins.workflow.run.start, .refused, .done and .failed events record the whole arguments, decisions, answer, the answer’s state (answerState), text, spend and final record lines, with elapsed times and failure stages and causes, and plugins.workflow.run.answer a refused or failed answer’s state, code, error and words; plugins.call.wall records the granted, base and effective milliseconds. The developer page runs an offer with the runner’s default wall and the frame’s own LLM decision.
The spend ceiling. runOffer accepts the dialog’s positive finite USD ceiling, null for none; an invalid or unparsable amount is badArgs/ceiling. Before each ctx.ai.complete, inside its slot, one synchronous ledger check adds ended spend, the worst cases still in flight across the run’s frames, and this completion’s bound. The bound counts the whole JSON of messages and params, including schemas and tools, in UTF-8 bytes divided by four and rounded up for input tokens, plus a positive whole max_tokens for output. Where a writer names no max_tokens, the door fills it before the check, synchronously in the same slot, so the same params are priced and sent: the catalogue entry’s capabilities.max_output_tokens, and under a ceiling the most what is left of it buys at the bound rates — the ended spend, the reservations and the input estimate taken off, divided by the output rate in exact decimals and floored, at least 1 and at most that maximum (affordableOutput) — the ceiling’s number alone where the entry states none, and the catalogue maximum where the rates are unknown (the admission then refuses rate); with neither, none is sent. The assistant’s params and a writer’s own number, under max_tokens, max_output_tokens or max_completion_tokens, are untouched. plugins.ai.complete.start logs the params, maxTokens and maxTokensFrom (assistant, plugin, catalogue, ceiling, none); usage.ceiling.fill logs the decision’s amounts. The decided model’s catalogue row on the provider’s platform supplies each unit’s highest price with neither when nor match, or its highest conditional price where all prices are conditional. OpenRouter can therefore bound Claude Opus 5 at the fast endpoint’s USD 10 / 50 per million tokens while the chip shows 5 / 25. Exact decimals stay unrounded and equality passes. A request is refused refused/ceiling with ceilingWhy of over, rate or outputLimit for an excessive total, missing input/output rate or missing/invalid output limit. The run’s ledger keeps the first refusal’s reason, worst case (null where a rate or the limit is unknown) and ceiling (usage.ceiling.firstRefused), and runOffer answers them as PluginStepRun.ceilingWhy and ceilingWorstCase, logged on plugins.workflow.run.done, so the row can say what the step would have cost. An ended completion counts its known USD cost on its single provider chain, otherwise its reserved worst case, including a failure or another currency; its reservation is always released. The input count is an estimate without a tokenizer; provider-added framing is unseen, reasoning is treated as bounded by max_tokens, and cached-input discounts are ignored. Only ctx.ai.complete is checked: a plug-in’s own-key ctx.http, ctx.call to a platform’s run/request, and renders are outside this ceiling. usage.ceiling.admitted, usage.ceiling.ended, the existing plugins.ai.complete.failed and workflow-run events record whole inputs/answers, reasons, exact amounts and elapsed times; the run’s completion distinguishes actual spent from conservative ceilingCounted.
Prompt-builder catalogs. filmopen_picker resolves schema 1 into categories and entries in file order, refusing the whole catalog above 1 MiB or on invalid JSON, version, category/entry shape or ids, duplicate entry ids, missing English-backed strings or invalid picture addresses. Pictures are deferred: package thumbnails and HTTPS full pictures are checked before decoding against 512 KiB and 8 MiB bounds and JPEG, PNG or WebP signatures; HTTPS reads require status 200 within 10 seconds, with the body buffered before the bound is checked and no request cancellation on timeout. Source discovery skips disabled or other-target builders and keeps the rest after a catalog refusal or failure. promptBuilderPrompt returns null for agreed missing references, while a refused catalog or unexpected read failure is logged and thrown. The picker.sources.*, picker.catalog.*, picker.picture* and picker.prompt* events record starts, outcomes, safe reasons, stages and elapsed time without words, addresses or paths; picker.choice.missing logs a stored choice’s source and item as they are, its reason noSource, noEntry, or malformed where either is no catalog token, nothing cut from them, and picker.choice.malformed a stored choice that does not parse as its text, raw.
The typed chat’s tools: a loop in the text turn. assistant.tools.offered logs the actual filtered tool definitions; the external session command remains registered. A catalog action’s chat whose chosen text provider declares tools: true freezes at its opening the app’s commands with the voice session’s table and aliases, every command exposed or not, except the built-in session command for field and form-section writing actions — given to the provider as ToolSpecs; a provider that declares none, or a table with no command, gives neither argument, as before, and the preamble says whether the tools offered act in the app or the chat cannot act. The tool descriptions explain changing an entity, a scene’s blocks by id, and moving an entity to the recycle bin with "deleted": true at the top of update-entity’s content, where the person can restore it (Project Specification §7) (assistant.open logs tools: the count, or unsupported, and toolsBytes). Each turn runs under one AssistantCallPolicy with tools admitting the table whatever its size: each model call is admitted by the policy — the turn’s liveness read there — runs the provider’s complete at the root with tools and tool_choice auto on every call, on a ledger of its own, settled once before any of its tools runs. An answer in words ends the turn; otherwise every call it asks for runs, in order: an alias the table does not hold answers {"error":"bad-call"}; a call whose arguments were not a JSON object — the provider’s ToolCall.argumentsError, its text exactly as the model sent it and the parse error’s words, beside empty arguments — runs nothing and answers bad-call with {"error":"bad-call","message":"the arguments were not valid JSON: <error>"} (assistant.tool.output’s reason argumentsError), the call keeping argumentsError in the exchange so that the provider sends its text back as it came, the answer’s other calls running, and the model asked again with the error; no repair and no resend of the app’s (a cut answer, cutOff, still runs nothing); the turn’s liveness — its session neither stale nor closed, its control not cancelled — is read immediately before the dispatch and again after the result, either stopping the remaining calls and any further model call; an effectful command gets session and operation = text- and a digest of the session, the turn and a turn-wide ordinal counting every call of the turn, never the model’s call id; each runs through CommandRegistry.call as the text caller under the turn’s guard and no deadline, its output the result’s structured as JSON, else its text, whole, sent back as a tool message. One refusal the model was told of twice, made a third time, ends the turn: every call of an answer runs and is answered, its refusals included; then the turn counts each alias with each refusal’s detail once for that answer — the detail read from the output the model is sent, its problems joined (content.prompts.first_frame:notAllowed), else the code (bad-call, the code its command refused with, error) — logged assistant.tool.refusalCounted {session, turn, tool, detail, code, told, answer}, and at the third answer that carries one pair, the model told of it after each of the two before, no further model call runs and the turn ends toolStuck (The assistant kept asking for something it could not get, and stopped. Try asking in other words.), logged assistant.turn.stuck {session, turn, tool, code, detail, args, answers, told, calls, toolCalls, ms} with the last arguments that carried it; several calls of one answer refused alike count once, so the model is always told before it is stopped, and a model that sends a different wrong key in every answer is not stopped by this rule; Stop, a cancellation or a stale session still ends the turn at once; a success between does not reset the count, a new turn starts afresh, and the calls answered stay in the turn’s kept exchange. The host hands each call to the registry with the chat’s own id and the selected assistant’s (CommandContext.assistantSession, assistant), set by the host and never read from the model’s arguments, so the writes the call’s command authors are the chat’s (§7.4), a declared epoch’s excepted (§7.3). A turn’s tool exchange — the calls that were answered and their outputs, as far as the turn went, whatever its outcome — stays in the backend’s memory, keyed by the person’s message it answered (the start’s by the opening’s own message), resent right after that message on later turns, and cleared on close (assistant.turn.kept); the dock keeps its words alone. The policy admits one POST per admitted model call with an object body; provider wire grammar is the provider’s responsibility. Refusals are logged plugins.refused with their reason and ms where they happen, even where the provider catches them; a POST outside the call’s binding — no call admitted or one already posted, another key, method, output bound, model or path, an address, a server or an embed — also assistant.policy.refused {reason, clause, value, provider, platform}, the clause it broke and the value that broke it, key being the key’s id. The fixed key, model, path and output bound and stream admission remain in force. An assistant session’s ctx.log and l10n.missing take the ordinary doors: a provider’s text.tools and openai.responses.incomplete lines are written as it gives them, through the log door below. Each model call logs assistant.turn.call {session, turn, call, started, route: key|credit|none, requestBytes, messages, replyBytes, hasText, reply, toolCalls, outcome, finish, units, ms} — finish the model’s own reason for stopping as its answer gave it (the provider’s own word: stop, length, tool_calls on a chat-completions route, the response’s status, such as completed, on OpenAI’s Responses; null where it gave none), so that an answer cut at its length shows (a provider refuses one as cutOff, logged length) — the session id as it is, the messages sent and the reply whole, and the call’s start time, so that it sits beside the HTTP door’s plugins.http.stream.open or .whole; each tool assistant.tool.call {turn, call, ordinal, id, asked, arguments, argumentsError?, tool, source, name, effectful} and assistant.tool.output {outcome, bytes, reason?, output?, ms} (outcome ok, bad-call, not-run, the code the command refused with, declared or not, as the registry’s command.call.done records it — error only where it gave none —, as the voice road’s; output what the model is sent, whole; absent where the tool did not run) beside the registry’s own lines; the turn assistant.turn.start {sessionId, input, messages}, its messages stage with requestBytes, maxOutput and maxOutputFrom (catalogue or none), and assistant.turn with turn, calls, toolCalls, requestBytes, reply, display and ms; a failure, assistant.turn.failed {stage, type, error, ms}, the error’s message in error; why a turn stopped, written where it fires before the cancel, assistant.turn.cancel {reason}: stop from the person’s Stop, stale with staleReason, the session’s lines with session and turns, stop and stale with busy. Not guarded, by design: no retry, no second model, no tool shown in the dock, no bound on the calls one answer asks for, a model that keeps calling tools that succeed, or is refused in ever different ways (the person’s Stop ends it); Apply keeps its one call with no tools. A close asks no model (§7.4), and a request of any phase but start, turn and finish, summary included, is refused request. A film left or the quit during a close logs assistant.turn.cancel {reason: leaving, running}, cancelling a turn begun since the close began where running says one is (a close’s own cancel of the turn under way is close); while a tool runs, the backend’s inFlight is that registry call, which a close lets settle.
status, required by the contract and never a reason to refuse a plug-in. The seam’s checkStatus(stem) runs the plug-in’s status with ctx alone and holds its answer to the definition before the app reads it: PluginChecked with one of the four states (ready, unconfigured, unavailable, degraded), the plug-in’s own message and the four facts of its details the app shows (PluginStatusDetails, below); PluginSaysNothing where the code has none — this plug-in does not say whether it is ready, which api 1 asks of it — after which its other functions still run, since no app can know a function is missing before it has run the code; PluginCheckFailed with the call’s own outcome, a shape the contract does not hold being badResult with its path. It starts no render, spends nothing and installs nothing, and runs only on a person’s gesture, or an agent’s, and for this computer’s render engines — each enabled plug-in implementing platform that renders on a platform of kind local itself or that a plug-in rendering on one calls (localPlatformsOf, which logs why each platform plug-in is one or not) — once at start, in the background when the plug-ins have loaded, and on each arrival on the GPU page (refreshLocalStatus, reason start or refresh, §9.7a), logged plugins.status.refresh.start and .done with the state, the plug-in’s words and details, a failure’s code, reason and path, and the time, never on a timer: the app draws Check (plugin:<tag>.check, the answer plugin:<tag>.status) on every plug-in’s page, and a list of plug-ins calls nothing. A check may not reach ctx.ai.complete or a platform’s run, nor may any plug-in a check calls, and a status is a check however it is reached — an action’s ctx.call too — the chain’s frame carrying it (CallFrame.inCheck) and the door answering refused/status (run, and the road through an action too) — since the app presents Check as an inspection and that is the one door that always costs money; its own key, its own server and another plug-in’s status stay open, which is how it answers is my key there, does my server answer, and what those cost is recorded as any call’s is. Check is disabled with its reason while the plug-in is off, and while a check or turning it on or off is under way; what it finds is the runtime’s record (Turning a plug-in on and off, below), drawn on the page and on the plug-in’s card in one wording — ready in green, degraded in amber, unconfigured and unavailable in red, always with words and a mark — and the plug-in’s words are left out once the language changes, half the line being the app’s and half the plug-in’s, an answer that lands after the change included. The log carries plugins.checked with the state, the message and the details as the answer holds them, or a failure’s code, reason and message. Every route a plug-in runs on is in its summary (PluginSummary.routes), drawn under Check on its page from what the host holds, never probed: for each platform of its text and render interfaces, on a platform of kind local the row of its server setting — or, where it declares none, of the local platform plug-in it calls, as a render resolves it, that plug-in named (via) and its stored server or default read; with neither, a row not ready whose cause says which was missing (noPlatformPlugin, noServerSetting) —, else each program setting, each sign-in slot and each copied key it may send there — with the keys of the platform plug-in it calls where it holds no copied key there, as a render resolves it — then FilmOpen credit, in that order (Automatic keeps its own, above); each row says what it makes (text where the text interface is on that platform, image where the render interface is, a program only where the render names it, a sign-in never) and whether it is ready, with why not: a server or program not set (notConfigured), a sign-in not made (notSignedIn), a key not stored (notStored), credit by the reason kept with its last answer — notSignedIn and noCredit from the funding’s own reason or, where a stored key answers first, from the account read with it, else that reason’s name or unknown. A remote platform’s credit not yet known is asked as the list is read, and a changed reason publishes as a changed answer does. The list is logged plugins.routes.listed {plugin, routes, credit} where it changed, and the credit answer assistant.funding.creditReady with ready, wasReady, why, whyBefore, the account as read (account), kept and ms.
Turning a plug-in on and off (plugin_lifecycle.dart, host_runtime.dart, ctx_doors.dart). A plug-in that declares the definition’s lifecycle interface is told when the person turns it on or off, each time and only then: setEnabled and Allow (allow) are the two roads, and the app’s start, a reading of the plug-ins again, a stock install, a change of files and a redraw call nothing. A call of the same value is no operation — nothing written, nothing read again, no hook — measured against the value last accepted or, with none pending, the switch as the registry has it. An accepted Off-to-On shows Checking at once (PluginReadiness.phase enabling), writes the switch, reads the plug-ins again, and then runs onEnable once, told { operationId, reason } — an opaque random token and toggle, consent or retry — only on what the registry published on: where the reading did not turn it on (its new files ask Allow, or the reading failed), nothing runs and the problem notPublished is kept. A reading another switch or a grant let go is read again first: that switch starts its own reading only once its write has landed, so nothing may have been published since this On was written. The plug-ins are read again — twice at most — only while no reading begun after the On was written has been published, so that a plug-in that came on is never said not to have, while one a published reading left off (its new files ask Allow, it cannot run, its switch was not saved) is said at once; a reading that failed published nothing, and is tried again with them. An accepted On-to-Off is at once: the registry’s view of the plug-in is off before anything is awaited (RegisteredPlugin.switchedOff), so a call asked for is refused disabled and one waiting for its slot is refused when it gets it, while an ordinary call under way runs to its end; owned program lanes are stopped at once and their live calls fail honestly with the returned transport or caller error, without replacing a received terminal success; a probe or a Check of that plug-in under way is stopped; and one bounded clean-up is put in line, run by the version that was on — its consented code and grants, captured as Off was accepted — the one call the runner makes without asking whether the plug-in is still on (stillOn), which only that clean-up skips. A plug-in that was never on, as the registry had it — an On whose reading had not published it yet when the Off came — has nothing to tidy: no clean-up runs, and no problem is made of it. The switch’s two preference lists are written one switch after another, in the order the switches were accepted. A switch is held over every reading until its write lands (_unwritten): the switches being written one at a time, a plug-in whose Off or On has been accepted and not yet written is published off by every reading, one already under way when it was accepted included, whatever the preferences it reads say meanwhile — an Off refuses ordinary calls and runs nothing from the moment it is accepted, and an On is on only once its own reading, begun after its write, has published it, before its onEnable runs. A switch that cannot be written holds only while it is the latest operation: one accepted while it was written decides, and an Allow of a plug-in already on changes nothing. An Off that cannot be written holds for the session all the same, as a hung plug-in’s does, since its clean-up has run and a reading would otherwise publish it on again with nothing having told it, and it is said on the plug-in’s card (PluginReadiness.switchNotSaved) beside whatever its clean-up leaves unconfirmed, until its next accepted operation. An On or an Allow that cannot be written holds the plug-in off for the session too — the preferences keep a value in memory before the disk takes it, so a reading would otherwise publish it on whatever the disk kept — and is said as that, its switch not saved (PluginSwitchNotSaved.on), never notPublished, since nothing failed to be read. Per plug-in, everything runs in order (PluginLifecycles): a clean-up always ends before a later onEnable begins, neither is dropped for the other, and an onEnable that an Off overtook before it began never runs; an onEnable waits for the plug-ins to be read again outside that line, so that a clean-up an Off puts in line meanwhile never waits behind a reading. Off holds whatever the clean-up answers.
- Each lifecycle call keeps its mode (
CallMode,CallFrame.mode), whoever calls it — the app or the developer page — as astatusis a check whoever calls it. Both lifecycle functions use the ordinaryctxAPI with its authorization, input bounds andinCheckspending guard. HTTP uses the ordinary transport, bounded by the operation’s remaining time and closed when the operation stops (PluginHttpDoor.send’sabort).implements.lifecycle.discoveryis optional metadata, not a separate HTTP gate. - Bounded in all (
enableBound, 10 s;disableBound, 5 s), from the hook’s own start in the plug-in’s line — behind what was asked of the plug-in before it, each bounded in turn, a probe an Off stopped ending within its grace: the wait for the plug-in’s turn behind a call under way and for one of the five slots, the run, and the grace a stopped call has to end (lifecycleGrace, 1 s), held inside the bound rather than added after it — the call is stopped a second before the bound, its JavaScript wall set to the same, its requests closed with it and each request’s own timeout set a quarter of a second past the stop, so that the stop decides. The host waits for the runner’s own answer — a call that does not stop answershung, at the bound — until a grace and a quarter of a second past the bound, and takes the call as timed out only where the runner has not answered by then, so that a hang is never said as a timeout. Whatever comes after the stop is the stop’s — a refusal of the request it closed, an answer the plug-in made of that refusal — so a server that never answers is said one way every time:activationTimedOutorcleanupTimedOut, a clean-up not confirmed, never a success. One that does not stop when stopped is turned off for the session as any hung call is (activationHung). A clean-up does not promise an immediate hook or cancel an ordinary call; switching off does stop owned program lanes. Process start re-reads live enabled state after its Usage await and refuses a disabled start before dispatch, logged with stageafter-usage. - What a hook answers is held to the definition’s
LifecycleResult—succeededwith theStatusit may carry, orfailedwith itsProblem— and then read. A function the code leaves out is the neutral outcome, never Ready. A throw, a refusal or a shape the definition does not hold isactivationFailed(orcleanupFailed) with the call’s code, its path forbadResult, and the plug-in’s words for a throw; afailedanswer keeps its problem’s code as it is, its words, and the status it carried. A failure keeps the person’s On, says what went wrong, and offers Try again (retry), which runsonEnableagain, toldretry— or Check again, where it was a check that failed; a plug-in that hung or did not come on is off, and its switch, or Allow, is the way back. - What is known (
PluginReadiness, the seam’sreadiness(stem)): the phase, whether the plug-in declares the interface, its last fact — the state, its words and the four facts ofdetailsthe app shows (PluginStatusDetails: the service’s version, the graphics card, its memory and the computer’s, each copied by its type, one absent unknown and never zero) — and its last problem. A plug-in’s words reach a screen cleaned (pluginWords), a status’s message, a failed answer’s and a throw’s alike: at most 300 characters — code points, so that a cut never parts a surrogate pair — cut with an ellipsis, with control characters and the marks that embed, override or isolate a direction made spaces, runs of space made one, and half a pair standing alone, which no text can draw, made U+FFFD — only as much of the text read as the characters kept need; a detail’s text over 200 characters is left out, as unknown. It lives in memory and is unknown at every start. A fact said under another identity no longer holds — how many times the plug-in’s configuration changed in this session (a setting saved, a grant, its code read anew:PluginLifecycles.reconfigured, which drops what was known at the change itself), its files (its consent hash), its grants, its settings on this computer, and the records of its keys that are saved, so that a change undone is a change still and a key saved again, checked again or removed and saved is another — and is dropped for good, never asked again, as is a failure of turning it on or of a check (activationFailed,checkFailed, a check that ran out of time among them), which was what the configuration before came to; another language leaves out the plug-in’s words and keeps the state; an answer of an operation a later one overtook, or of one whose identity or language changed while it ran, is never kept; and a timeout or a hang of turning it on, a clean-up not confirmed and a reading that did not publish it (notPublished) stay until the next operation that succeeds. Check publishes into the same record (phasechecking), so the card says what the page found; a check that finds nostatuskeeps what turning the plug-in on found under the same identity, since a provider’s guidance is not lost to a check its code cannot answer. - The host fixtures (
packages/filmopen_plugin_host/test/fixtures/lifecycle/) are the contract’s examples, not shipping plug-ins:fixture-comfydeclares/system_statson itsserversetting and answers what a ComfyUI measured there — its graphics card (trimmed of the service’s device index and allocator), that card’s memory, the computer’s and the version — or where to look when nothing, or something else, answers;fixture-saladasks only whether its key is saved — in itsonEnableand in itsstatusalike, so that a Check says it again in the reader’s language — and sends the person to Provider keys, with no request at all. Both haveonDisable, both sayminRevision3. - The log says
plugins.lifecyclewith the stem, function, operation token, reason, outcome, code or state and failure message, andplugins.lifecycleRetry. The plug-in’s words and addresses are logged as given; credentials never enter it.
The manifest (plugin_manifest.dart). PluginManifest.parse(json, api:, knownPlatforms:) never throws. It answers a PluginManifestParse: the manifest, or null when the plug-in cannot run on this host, and every PluginIssue found — a code (PluginProblem), the dotted field it concerns and the values a sentence needs. Refused (no manifest): a file that is not pg, a tag that is not a segment or is app (the owner of the app’s own keys), an author that is not a handle, a missing or mistyped v, name or api, an api other than the definition’s (apiUnsupported, carrying the manifest’s version and the host’s), and an entry that does not read (badCodeFile, carrying the path it concerns). entry is a file name or an ordered list of them (the format’s §14.1): each a .js file of the plug-in’s own folder named by a relative path with /, no .., no absolute path, no drive letter and no segment beginning with a dot, none twice, at most PluginManifest.mostCodeFiles (32) of them and mostCodeBytes (2 MB) in all — the last checked where the code is read, since a manifest knows no sizes. Absent, it is <stem>.js; manifest.entry is the first, for what reads one. Reported and left out, the rest read: a field the manifest does not know (the common header’s fields and plugins are known), at its top or inside a key slot, a setting or an action — a mistyped defualt among them, a key slot’s or a setting’s label or help that is not text, a number’s min above its max (both bounds left out), a hook (unknownHook, entity or platform), a setting type (unknownSettingType), a setting, action or key slot that cannot be used, a malformed key id in uses, an entity type the format does not know, entitySettings for a type applies does not name, a host in capabilities.network that is not a host name, a default of the wrong type (the setting kept without it). A key slot naming a platform no platform file defines is reported and kept. kind is platform, model or tool — a word for lists, which decides nothing, as a folder’s kind decides nothing; the three are said in the reader’s language on the plug-in’s page, and another token is kept and shown as written. The models of an interface it implements (implements.render.models, implements.text.models) are the catalogue entry ids the plug-in speaks for (§14.7), since an entry’s id does not always fit a tag: one that is not an entry id is reported and left out, and one the catalogue does not define is reported on the plug-in’s page and kept, since a catalogue grows and a plug-in that names tomorrow’s entry is not broken today. offers and calls are read as the architecture document’s §4.2 has them: an offer is a plain name on the plug-in’s object with a label — a locale key of the offering plug-in’s strings, since it is what the caller’s consent shows — and optionally help and an example of its arguments; one without a label is reported and left out, because consent would then say nothing about it. calls is <tag>:<name>, a list apart from uses, since a person reads the keys it may use and the plug-ins it may use as two things; a plug-in’s own tag is allowed, which is how the template shows the door with nothing else installed. A hook may never be offered, nor a name the language gives every object (reservedOfferNames: every PlatformHook and EntityHook, constructor, toString and their kin), nor a function one of the plug-in’s own actions calls — a platform hook is the app’s to call with its consent and its usage records, verifyKey receives a candidate key the person just typed, an entity hook writes versions, constructor is not the plug-in’s function at all, and an action is a person’s button, with their gesture behind it, which a caller must not press for them (reference said so and nothing had enforced it). It is refused where the manifest is read and again at the door, since one of the two alone would be a rule with a hole. Settings (setting_spec.dart) are string, text, number (min, max), boolean, enum (values), model (category, platform) or, server, each with a label and help that are locale keys and a scope of project (the default) or machine; an entity setting has no scope. A server holds a ServerAddress’s normalised text and is always machine-scoped, whatever scope says — scope: project is reported (badEntry) and the setting is kept on this computer anyway, so its author learns it — and is refused under entitySettings (unknownSettingType, the field left out), since an entity’s file travels as the project file does. Its default may only be this computer: an address elsewhere is reported (badHost) and dropped, so the setting starts empty and the box asks; one that is no address at all is wrongType, as any default is. accepts takes what ServerAddress.parse reads. Setting keys and action calls are identifiers, action ids and slots segments.
Keys (key_id.dart). A key’s id is <owner>:<slot>, both segments: the tag of the plug-in whose manifest declares it in keys. The tag app is reserved for the application, which owns no key (§6.7): a request for app:<slot> names nobody’s key. Its store name is filmopen/<owner>/<slot> and its preference providerKeys.<owner>/<slot> — the status, the date and the last four characters, never the key (§6.7).
Strings (plugin_strings.dart). A locale file is ARB-shaped and read by the format’s own reader, so a repeated key refuses the file (notLocaleFile, as §18.1 refuses one anywhere): @@locale must be its file’s name (localeMismatch otherwise), @key metadata is skipped, a key holding : is refused (colonInKey). PluginStrings.t(key, locale:, args:) reads an unprefixed key, or one prefixed with the plug-in’s own tag, as the plug-in’s; app:<key> through the application’s lookup; any other namespace as nobody’s. It falls back from the locale (pt_BR read as pt-BR) to its language to en to the key itself, reporting each missing key (plugins.missingString: once a session for each plug-in and key, the key as it is, since a manifest’s label is looked up by its own text, which may be any sentence), and fills {name} placeholders, leaving one without a value as written. textOr shows a manifest’s name or description as a key where the strings have it and as written where they do not.
The package (plugin_package.dart). PluginPackage — tag, list, text(path), bytes(path) (for the consent hash) — is the one interface the loader reads a plug-in through; SourcePluginPackage is a folder inside any ProjectSource, and SourcePluginPackage.allIn(source) every plug-in folder under plugins/, sorted by tag, from one listing of the source. PluginFolder.read(package, catalogueIds:) reads every manifest at the folder’s top (a stock plug-in and a person’s fork share a folder), the locale files of the first manifest’s l10n, and the plug-in’s own platform files, refusing one for an id the catalogue defines (platformConflict); a folder without a manifest (noManifest), a manifest whose tag is not its folder’s or whose stem is not its file name (folderMismatch), a manifest whose entry is not in the folder (noCode, the manifest still listed so that its page can say why it cannot run), a folder with no en locale file (noEnglish), a file that cannot be read as text — bytes that are not UTF-8 — (unreadable, with the failure’s type, the rest of the folder still read), and a document that does not parse are reported by the path they concern. The code is not read. PluginManifest.isByStockAuthor says only that the author is filmopen, the stock plug-ins’ handle; it grants nothing, since anyone can write under it and the template does.
The engine (filmopen_js). runJs(JsCall, doors:, cancel:, note:, onDoorFailed:) runs one call in an isolate made for it (Isolate.spawn), in a QuickJS-NG runtime made for it and freed after it — unless its isolate dies on an uncaught error, which leaves the runtime unfreed —: each of the call’s files is evaluated in order — JsCall.files, a JsSource of a name and its text — as plain scripts of one scope in the one context, so that what an earlier file declares at its top is there for a later one and the last file’s value is the plug-in (the format’s §14.1); the function of that value is called with ctx and the JSON arguments (or ctx alone, for an action), its promise awaited, and the answer is JsValue or JsFailure with a code — failed (the script’s own message, its author’s words for the person; or, where the call’s isolate dies on an uncaught error — its native library not loading, say —, isolate: <error>; function <name>, which the host’s plugins.failed line carries), limit (time, memory, stack, result, wall), cancelled, hung, noFunction, notJson, stalled (a promise nothing can settle), engine; a function the plug-in does not have is noFunction: one the definition lets it leave out (required: false in every interface it implements that has it) — a plug-in’s options, which it answers by having none — logged once a run as plugins.absent, never as a failed call, and one it must have — render, status — a failed call as before. An action’s answer of state: 'failed' is its failure (runAction): a PluginCallFailed whose message is the plug-in’s own — its message key in the person’s language where its locale files hold it, else its problem’s words — and whose reason is the problem’s code as it is, logged as plugins.actionFailed with that code, else failed, the problem and the words; a message without that state is an answer, never a failure. A file that fails stops the load where it is, so the failure’s place names that file. The C shim (src/filmopen_js.c) keeps every JSValue, the budget and the cancel flag, so Dart passes only UTF-8 strings and integer handles and no Dart callback runs inside JavaScript; fjs_load is called once per file, the prelude running before the first. Before the script, a prelude builds ctx on one host function and removes eval, Function — the global, and the constructor of every kind of function — and Math.random; the runtime has QuickJS-NG’s base objects — §14.2’s list and the language’s other plain built-ins (Reflect, Symbol, BigInt, the weak collections, queueMicrotask, …), none of which reaches outside the runtime — and no Date, module loader, timer or I/O (Project Specification §14.2). The prelude captures JSON’s functions, Reflect.apply and the error constructors before the plug-in’s file runs, locks Error.prepareStackTrace — whose call sites would otherwise hand a script the functions on the stack, the host’s among them — and evaluates to the runner, which the shim holds: nothing of the host is on the global or reachable from the script. The runner reports through a host function, and the shim writes the outcome’s JSON — the value by the engine’s own serialiser, the message as a JSON string — so a script decides only its own value and its own failed message; a thrown error’s code is kept only as refused or badArgs, which doors give, or cutOff, which a text provider gives for an answer cut off at its output length (kept from any operation’s script, as the other two are), and cancelled, limit, hung, stalled and engine are the shim’s and the runner’s alone. A request is written to JSON by the engine’s own serialiser in C, never by the script’s JSON, so its name and id are the shim’s, and the runner answers only the door names ctx uses; its bytes count against the memory limit until its door answers, a request is at most 16 MB and at most 10,000 wait at once. A microtask that throws outside the call’s promise is dropped, not the call’s outcome; a log or progress made just before a call ends still reaches its door, and no other request queued then is started; a cancel that comes before the call’s isolate has started is delivered when it starts. A failure says where it is: the runner hands done the error’s stack where typeof says string, the shim cuts it at 2,000 bytes and makes it JSON-safe — a quote, a backslash and every control character becoming a space, so the frames run together and stay readable — and a load-time failure, a syntax error among them, reads the exception’s own stack the same way; Dart then takes the first frame naming one of the call’s own files as JsFailure.file, .line and .column, so a <prelude> frame never counts and a failure with no such frame — a door’s refusal, made where the door is answered — has no place. Only the text crosses over: Error.prepareStackTrace stays locked, since call sites would hand a script the functions on the stack, the host’s among them. A script can throw an error that reads as a stack overflow, or write its own stack, which only mislabels its own failure, as it can mislabel its own message; the capped stack protects the process either way. Every C string is read as UTF-8 with malformed bytes replaced, and a result is measured in bytes before it is copied. ctx’s asynchronous members are requests queued in C and answered by the caller’s JsDoor on the caller’s isolate — where plugin channels and providers are — and a JsDoorRefusal reaches the script as an error with its code and reason — and, for ctx.http, its stage: admission where the host refused it before anything left the machine (no-key, no-grant, no-platform, insecure, host, store, badArgs, a usage, credit or assistant-policy refusal, and a media request’s refusals before its request — its grants, its bytes, its key’s road, its upload to the media store — logged plugins.media.refused), transport where it refused it once it was being sent (timeout, connection, redirect, tooLarge, a portal’s refusal, cancelled); an answered status, a 401 or a 403 among them, is an answer, not a refusal. A plug-in passes an admission refusal on as the host gave it, never as an outcome nobody knows; plugins.refused logs its code, reason and stage. Where it ends the call, the call’s failure keeps the stage (PluginCallFailed.stage), and ctx.ai.complete hands it on to the plug-in that asked; a direct describer (fo-fal’s describe, fo-openai’s responses called directly) answers it as its failure’s code, so a reference analysis says the key is missing; and the assistant’s turn reads it as not sent: notSentNoKey (Not sent: <provider> has no key on this computer, with Open Settings to its key card) for no-key, else notSent, logged assistant.notSent {code, reason, stage, message} —, while anything else a door throws, or an answer that cannot be encoded, arrives as failed with the message door: <stage>: <error>; door <name>, args <arguments> (call or encode) — uncaught, it is the call’s failure, whose message the host’s plugins.failed line carries, and at either stage, caught or not, onDoorFailed tells the host before the rejection is built, whose plugins.door.failed warning carries the plug-in, the function, the door, the stage, the error’s type and message, the door’s arguments and the milliseconds since the call began, since the engine has no log of its own; a call whose isolate cannot start is engine, its message engine: spawn: <error> with the function and the files; sleep is answered in the call’s isolate, when it ends and never sooner — where the call has a wall, one longer than it has left is ended by that clock, whose cancel wakes it, and the call ends as the wall limit; settings, plugin, selection and l10n.t read the call’s data synchronously, a missing string reported through a door. Limits: memory is counted by the runtime’s own allocator, so running out is known for certain and ends the call even when the script catches it; the JavaScript stack is capped at every entry by what the current thread really has below it, less 256 KB (a 1 MB limit crashed a Linux isolate thread whose stack was smaller); engine time is counted only while JavaScript runs, never while it waits on a door, and the interrupt handler ends a call past its budget or cancelled — uncatchably; the wall clock and the result’s size are the Dart side’s. A cancel sets the C flag at once, from any thread, and wakes a call waiting on a door or asleep; a cancelled call that has not ended within 5 s answers hung and is left, since an isolate inside C cannot be killed. The host may grant one call a longer wall, up to JsLimits.longestWall (thirty minutes); the default remains ten minutes except for root and nested complete, which have no wall-clock cap. A null JsLimits.wallTime creates no wall timer; the runner’s noWall takes precedence over a requested wall or a grant. interrupt and every other function keep their ordinary wall. Cumulative JavaScript time (thirty seconds), memory, stack, result size and cancellation grace remain unchanged, and Stop still cancels a completion. Each complete logs plugins.call.wall once with its plug-in, hook, null wall, reason: complete, ms: 0 and base, requested and granted walls.
The host (filmopen_plugin_host). lib/main.dart installs createPluginRuntime into the state’s PluginRuntime seam (§7.1) — the host where there is dart:io and dart:ffi, the seam’s NoPluginRuntime on the web — and attaches it to the container, as the automation host is. Nothing runs at start: the runtime reads manifests and code as text and installs the stock plug-ins’ files; JavaScript runs on a gesture. The host reads the app through a HostEnvironment (the container’s, or a test’s in memory).
- The stock install. On its first load the runtime runs
installStockPlugins(filmopen_plugins/stock_install.dart) over the library with the bundledplugins/stock.json(JsonAssetSource), the plug-ins repository’s packages staged at a ref (§3): a file the library lacks is copied, a stock file whose bundled content changed is replaced, a stock file the bundle dropped is removed unless the person changed it, and nothingplugins/.stock.jsondoes not list is touched; a deleted stock folder comes back unless the person disabled that plug-in. The project is re-read when anything changed, since the library’s index was read before the files were there. The stock set is every package of the plug-ins repository at the staged ref, installed on and trusted by their origin, so that their key cards stand on their pages before a key exists, except the template, installed off (stockPluginsOffByDefault). A plug-in may ship files that are not text: a file of a plug-in is text when its bytes are valid UTF-8 (isPluginText, the one rule, whichscripts/stage_plugins.pyholds asis_plugin_text, both checked againstscripts/tests/plugin_text/); the staging carries any other file, such as a picture, as base64 instock.json‘sbinarymap (present only when a package has one), and the install writes it as its bytes and records its hash over them, so that an installed stock plug-in with pictures verifies as stock as one of text alone does;ctx.assetstill reads text. The log says the start once with the ref and the commit (plugins.stock.start), each decision with the plug-in, the path, the outcome and its reason (plugins.stock.decision:installed/new,updated/bundleChanged,removed/bundleDropped,left/personalormodified, and per plug-inkept/unchangedwith a count,left/disabledGoneandleft/bundleUnreadablewith a count), each failure with its stage and the error’s type (plugins.stock.failed; a staged binary is decoded only when it is read, so one that does not decode fails its own file at stageread; every file of a plug-in is read before any is written, so a plug-in one of whose files does not read is left as the library holds it, none of its files written or removed and its record kept, and the other plug-ins install — a plug-in installed without a file would match its shortened record and run trusted; a bundle that cannot be read as a whole ends the trail in place of the start, at stagebundle, with the error’s type and message,outcome: notStartedand the time, before the reload fails,plugins.loadFailedcarrying the message too), and the totals with the pictures’ count and bytes and the time (plugins.stock). - The registry (
plugin_registry.dart) reads plug-ins from the development folder, then the shared library (§14.5). It never scans a project for executable code:plugins/files in a project are Other files, and apg_document indata/is an entity that runs nothing. The development folder is a folder on this computer whose sub-folders are plug-ins —<tag>/pg_<tag>_…json, with noplugins/inside it — named by the preferencefilmopen.plugins.devFolderor, where that is empty, by the environment variableFILMOPEN_PLUGIN_DEV_FOLDER, so that an agent’s launched build, whose preferences are new each time, needs no typing; the preference wins where both are set. It is found first, before the library’s and under a stock plug-in’s tag too, so that a working copy is the one that runs; it is never written by the app, a path that is not there lists nothing, and it is read one level deep, one plug-in at a time (dev_folder.dart’sDevelopmentFolder, through aDevFolderReadera test replaces to count what it was asked): its immediate sub-folders alone are looked at, one whose name is a tag and whose own files at its top include a manifest of that tag is a plug-in, read on its own, and any other —docs,research,tests— is never read and nothing below it is listed, the names at its top being all that is asked of it once, while a folder whose name is no tag —.git,node_modules— is not asked about at all; so the folder may be a repository. A plug-in folder that cannot be read is a problem of that plug-in, said with its path on the developer page (devFolderProblems) and in the log once (plugins.devFolderUnreadable), and every other plug-in of the folder is there as before; one that cannot even be looked into is passed over, since nothing says it is a plug-in, and said in the log once with its path (plugins.devFolderPassedOver), as is a development folder that cannot be listed at all, so that a folder that lost its permissions does not vanish without a word. A plug-in found there is never trusted by origin: it asks Allow like any other in ordinary use, once for each state of its files. One installed version of each plug-in runs, whatever the film: the stock author’s highest, else the folder’s highest (plugins.registry.chosen, at debug: the tag, the version andstockorhighest). The film is no input to the reading: a pick or an official pointer on apgstays a legal file, read as a document and choosing nothing; another version runs once the person removes the package and installs it. The runtime still installs stock plug-ins from the app’s bundle into the library; registry discovery needs no bundle tag list or project-tag guard. A plug-in is trusted when every file of its folder is one the stock record hashed, unchanged, and the folder holds no file the record lacks (widened): it is on unless the person turned it off, and itsusesare granted as installed. Any other is off until the person allows it: Allow storespluginFolderDigest’s hash (plugins.consent.<stem>), **the SHA-256 of every file of the folder as the source lists it — the manifest, every code file,assets/,platforms/,models/,l10n/— sorted by path, each line the path, its length in bytes and its own SHA-256, one file held at a time; a change to any of them turns the plug-in off and asks again. What the listing leaves out does not run: a source reports no name beginning with a dot, nothing underbuild/ornode_modules/and no.tmp, so a code file it does not report isnoCode— the plug-in listed and not runnable —ctx.assetrefuses a file it does not report, andentrymay not name a segment beginning with a dot; reading one would be running what nobody allowed. A folder of more thanmostFolderFiles(2000) files ormostFolderBytes(64 MB), a zip’s own limits, is not read at all: unreadable, so not trusted and asking again. A workflow inassets/decides what runs on the person’s own computer as surely as the code does, and a plug-in’s ownplatforms/*.jsonis inside the hash. Files are hashed by their bytes, so a picture inassets/counts; one that cannot be read hashes as a line no readable file can write — never as the empty file somebody may have allowed — and the registry lists it as a problem (unreadable) on the plug-in’s page; it also breaks trust, since a file outsidefileswould otherwise slip past the record’s count. The preferences areplugins.disabled,plugins.enabled,plugins.grants.<stem>(key ids),plugins.consent.<stem>andplugins.prefer.text/.media(a tag). A key’s platform is its owner’s declaration, and only an enabled owner’s: a disabled owner’s key cards are gone from its page, so its keys are nobody’s to send. The keys a plug-in may send are its own slots and each granteduseswhose owner is enabled and still declares it. A plug-in’s ownplatforms/*.jsonnames a platform for the app only while that plug-in is on, and never decides where another’s key goes: a key is sent by the catalogue’s file, else its owner’s own — a folder nobody had allowed, read first, used to decide where every key on a platform the catalogue lacks was sent. A plug-in one of whose calls hung (it did not end within the grace after a cancel; its isolate cannot be killed) is off, without touching the preferences, until the app starts again or the person turns it on or allows it again — their choice, which may leave a second hung isolate beside the first. Consent to other files than the ones allowed before takes back the keys granted then. - What the seam answers.
plugins(summaries in the app’s language: name, description, origin, enabled, consent, grants, hosts, servers — eachserversetting’s words and the default it starts with, which consent says in one line — types, hooks, machine fields with their values, actions, problems);keyOwners(the key cards’ owners: each enabled plug-in’s slots; the app owns none, §6.7);sectionsFor(type)(each enabled plug-in’sentitySettingsfor an entity type, or its project-scoped settings for the project file);providers(hook); and the changes —setEnabled,allow,setGrant,setMachineSetting,setPreferred,installArchive(a zip’s text files, by the same rule, written as text and anything else, a picture inassets/, as its bytes) — each logged (plugins.enabled,plugins.disabled,plugins.granted,plugins.settingChanged,plugins.preferred,plugins.installed). An install from a file says its start with the file’s size (plugins.install.start), the plug-in and each file’s size it read (plugins.install.read) and each file it wrote, with its target (plugins.install.wrote), and its end with the stem and its time (plugins.install.done); a refusal isplugins.install.refusedwith the step, the code, the field, the problem’sargs(a reader’s error text), the reason — the problem’s sentence,no-library,library-read-onlyortag-present, with the tags present — and the time; a failure isplugins.installFailedwith the step, the target being written, the error’s type and message and the time, and the person is answered that the install failed —writeFailedfor a failed write,failedfor any other step. Where every file was written and reading the plug-ins again failed,plugins.loadFailedsays why,plugins.installFailedfollows at thereloadstep with the reasonregistry-reread-failedand the files written, noplugins.install.doneis logged, and the answer isreloadFailed. A machine setting accepted during a reload survives that publication: its exact tag/key value or removal overlays each fresh loaded-tag map, latest accepted edit per key winning and unrelated disk fields retained, until a winning reload begun after the successful write publishes; a known failed write forgets its own edit. - Calls between plug-ins.
ctx.call('<tag>:<name>', args)runs a function another plug-in offers, or one of an interface it implements that the definition says plug-ins may call (platform’srunandrequest, and every plug-in’sstatus) — by name and never by a handle. NeververifyKey,transform,renderorcomplete, which go through the app so that they are routed, recorded and paid for as the person’s, and never a function one of the plug-in’s own actions calls: the manifest’s reader drops such an offer and the door refuses it again, reading the contract itself (plugInsMayCall(api, manifest, name), the one rule both use), so that neither half alone is a rule with a hole. The function runs as its owner — its settings, its server, its storage, its keys, its own engine time and memory — andctx.calleris{ tag, stem }of the plug-in that asked, null where the app or the person started the call; the caller receives the JSON it answers and nothing else. Refused with their reasons:no-grant(not in the caller’scalls), and which cause it was (the code staysrefused):notInstalled(no plug-in of that tag),notEnabled(off — the cause a person can act on),cannotRun(on, and unable to run: a code file missing, a newer FilmOpen needed — never told to turn on what is on) andnotOffered(it answers no such function: not an offer, not a function of the contract plug-ins may call, an action’s own, or a name every object has — so an action, a hook that is not offered andverifyKeycan never be called), each with the app’s sentence as the refusal’s message,cycle,depth,keyCheckandcallerEnded; andserverSession, in the app’s words, for a server provider’sdeployandwake— refused on every road to a call, an action and a hook as well, since only a server’s own screen — Start server on the GPU page’s Servers tab — may start one (§9.4a). A declared call is allowed by Allow itself: there is no tick and no store of grants — what takes a call back is what takes the plug-in back, its switch or files that changed — and consent names every call grouped by the plug-in called and said in that plug-in’s own words, saying so where it is not installed.ctx.ai.completefollows the same frames and is no longer refused merely for being nested; what refuses a provider whosecompletefalls back onai.completeis now the cycle rule, which still allows the assistant’s Ask (ask→ai.complete→ its owncomplete). A nested call that did not do its work is loggedplugin.call.refused— the caller’s tag, the callee’s and the function as asked (the name as given where it is not<tag>:<name>), the arguments,answered(refusedwhere a gate of the call refused it — no grant, not installed, not on, cannot run, not offered, a cycle, the depth, the caller ended, a key check — or its callee threw;failedwhere it answeredstate: failed), its code and reason as they are, the refusal’s or the answer’s message and the milliseconds; the answer itself goes back unchanged. - A frame per call (
CallFrame,plugin_call_runner.dart). A frame knows its parent, so a chain is a list:depthis 1 for the call a person started and at mostmostFrames(4) — the expected case is three, the app calling a model plug-in’srender, which calls the platform plug-in’ssubmit, which may call a base plug-in;onChainrefuses that function of that plug-in where it is already on the chain — another function of a plug-in already on it runs, sinceA:f → B:g → A:his not a loop. The turns are per frame and never per chain: oneTurnsfor a chain would deadlock the moment a nested call made a call of its own, the parent holding the turn its grandchild waits for. A frame’s model completions take up tomostCompletions(4) slots at once, the fifth waiting in order; its other calls keepturns. A frame’s end cancels its children, which cancel theirs, so no call outlives its caller; one usage ledger serves the chain, the top call’s, so a key any frame sends is recorded against the plug-in the person ran, and the hook a call records is<tag>:<function>. Only the top call takes one of the five slots and the one-call-per-plug-in turn. - A call (
_run) runs a plug-in’s function throughrunJs(the engine, above) — one call at a time per plug-in except a completion or render on a resolved program whose number of copies is above 0 (plugins.call.slotlogs it ascopies), and at most five roots in the app — a call stopped before or while it waits for one of the five leaves the queue at once and never starts, and a run cancelled before its isolate began spawns none — a call made throughctx.ai.complete— whose answer’s usage is recorded against the key it spent, an action’s own answer being a message — running inside its caller’s slot, so a plug-in asking the text model it itself provides never waits for itself, and bounded by its caller instead: a call’s model completions run up to four at once, the fifth waiting its turn, its calls throughctx.callone at a time, and a caller’s end — an answer, a limit, a cancel — cancels the calls it made, which cancel theirs, and closes its usage ledger, so no request on a key starts after it. — withctxbuilt from its settings (each default, then the project file’splugins.<tag>for the project-scoped, then this machine’s for the machine-scoped), its strings resolved for the app’s language (its own keys, and everyapp:<key>its code names, fromfilmopen_l10n’s generatedappStringsByKey) and the selection. The outcome’s code is logged (plugins.limit,plugins.failedwith the message,plugins.cancelled,plugins.hung— which also turns the plug-in off); the plug-in’s own message goes to the page and to the log. - What a call spent (
usage_chain.dart, §6.7). The estimate instartedis not the ceiling’s bound. A call’s ledger writesstartedbefore its first request on each key, with the catalogue’s price for the call as asked (the format’susageEstimate): a render’s quote for the whole call where it was handed one — never a quoteperan hour, which prices a server’s lease; else, by what the entry makes, a text call’s tokens, a picture’s images, megapixels or requests, a clip’s seconds, a piece of music’s minutes, a voice line’s characters — on the row of the variant the call names — a render’s chosen stack first —, else the platform’s first, at the least of the prices whosematchthe call’s settings all equal (the format’spricePer), else the least of those without one whosewhennames its resolution, else — only where no price names amatch— the least of all; a picture none of whose prices applies — nomatchit meets, nowhennaming its resolution — where some price names amatchhas no estimate, never the least of them — so that the estimate is the least the call can cost, and none where nothing prices it or the request cannot be read:startedthen carries no estimate, a cost nobody can foresee, never one of nothing (§6.7). Each call frame carries its own pricing — the model and the request it was given, a nestedcomplete’s own and an offered function’s its caller’s — so that calls that overlap never price by each other’s. A nested answer uses the pricing of the call that asked it, so models on one key each cost their own rate, summed on the key’s one chain, whose record names the model of the request that opened it. Each chain captures the answer as it came, before anything holds it to a shape:usage.billing‘s cost with its basis, aprovidercost of more than twelve fractional digits being none; the top-levelcost; units that the catalogue prices one way — a price’smatchchoosing among them, aspricePerdoes — or tokens (usagePrice, basisprice); else a cost nobody knows with the units, or the requests the chain sent — never a cost of nothing the answer did not say. The call’s own answer speaks where the call spent on one chain; a nested call’s answer — a platform’srun, a text provider’scomplete— for the one chain the plug-in’s own requests used, a receipt answered twice counting once. A receipt counts for its own key: arequestId, the usage’s or the billing’s, that no response on that key held, or that is a word of the call’s prompt, makes the cost unknown and is not written. A response holds what its head’s headers and its body say — a streamed answer’s head and each read’s events alike, so that the id a plug-in cites from a stream is seen as a whole answer’s is, a stream’s event strings counting as a text answer’s words do; each response’s new receipts are logged as they came (usage.receipts{plugin, platform, added, kept}), and an unseen one names the ids cited and unseen and why (usage.receiptUnseen). A request the service accepted is never free: the HTTP door tells the ledger what became of each request on a key the moment it is known — its status as soon as the status line arrives, before a body it may yet refuse; that it never left, where its host’s name did not resolve, its connection was refused or its TLS handshake failed; or that it was lost, sent and never answered. Where nothing says what a chain cost, a call that succeeded ends it with a cost nobody knows and the requests accepted, or sent; on a call that did not, the chain ends once no request on it waits for its answer — with a cost nobody knows where the service accepted one, whatever the call did after (a plug-in that threw, an answer the definition refused, a stop), with no cost where it accepted none, and not at all where a request was lost, so that it stays open with its estimate pending (§6.7). A call stopped while its request is out has its chain ended when the answer comes. An ordinary call’s total built from nested answers is unknown when some spend was said but fewer answers said spend than requests were sent: a refused answer or a 2xx answer without spend cannot leave a previous known part standing as the whole total. The call’s own answer or a funded receipt remains authoritative, including an aggregate cost covering several HTTP requests. Lost or waiting requests take their open or unknown path before the partial check. At an ordinary close,usage.endedlogs the latest record, its provider request id (request) and its operations’ ids on credit (operationIds), the known subtotal and currency, the sent, spend-bearing-answer, accepted, lost and waiting counts, the operations by frame, the decision reason and elapsed milliseconds. The subtotal stays in the log alone; an unknown record carries no cost. Two first requests on one key open one chain. Cached input tokens are counted within the input tokens, and priced at the catalogue’s cached price where the row names one. A usage’scacheWriteTokens(cache_write_tokens) are counted within them too, and priced at the row’s cache writes price where it names one;usage.unitsPricedlogs the price’s source, the units, the band chosen and why, andusage.priceda cost the plug-in’s billing gave. A paid call that then failed, or whose answer the definition refused, keeps what it said it spent on itsfailedrecord. A pending answer spent nothing yet: a cost a job’s answer says while it sayspending— itsusage, the top-levelcost, or its nested calls’ — is no cost and writes no record, so polls saying one are never summed and the job’s usage ends on its terminal answer; afailedanswer that confirms no provider end keeps its reported cost at once, and a receipt cited by two answers still counts once. - The doors. The callable
httphas an awaitedreadmember;partialandnoteare awaited text-delivery doors.httpisPluginHttpDoor(http_door.dart): with a key, the key’s own platform file — the catalogue’s, else its owner’s own, never another plug-in’s that names the same id — gives the base URL forpath, or aurlmust behttpson itshosts— and, on a host the file namespathsfor, on one of those paths (Project Specification §14.9:PlatformFile.refuseCredentials). A plug-in allowed to use a key holds the key’s whole authority on every path it may reach, and a platform’s hosts can serve more than models — fal’sapi.fal.ai/v1/keysmakes keys — so the path is matched by whole segments (/api/v1/keyis not/api/v1/keys), and judged as the plug-in wrote it: theurl‘s own text from the end of its authority, a backslash ending that too, or the base’s path withpathjoined, beforeUri.parseresolved..and%2e%2eaway. A.or..segment however escaped or whatever;parameter follows it, an escaped slash or backslash, any backslash, an empty segment (a trailing/included), a control character, and a%that is not a printable ASCII escape or that escapes%itself (%252e%252eis..to a server that decodes twice) are refusedbad-pathbefore anything is matched, and a plain path outside the prefixespath; both are refused before the credential is read, and nothing is sent. FilmOpen credit (§6.6): the person’s own key first — a key stored is used as it always is; where none is, a request to its platform’s own origin (the origin of the platform file’sbase_url:https://queue.fal.run,https://api.openai.com,https://api.elevenlabs.io) on a platform the funding resolver answers credit for goes to the website’s pass-through,https://<kFilmOpenDomain>/v1/portal/pass/<platform><path>— the target’s path and query as_targetresolved them, apathjoined to the base or a wholeurl, so that fal’s queue URLs, OpenAI’s/v1/images/generationsand ElevenLabs’ query all keep theirs — with its method and body untouched, of its headerscontent-typeandacceptalone, and the bearer and the operation’s portal credential (ProviderFunding.portalHeaders) andX-FilmOpen-Operation. A non-job frame holds its own operation and reuses it during its life; a render job’s frames share the chain’s operation across submit, status, result and cancel, saved in the receipt across a restart. The platform’s own credential header is never added. Its usage chain is opened with no key, on the routefilmopen.ai, holding the operation (§6.7), and a frame that holds one stays on credit. Entering a key between non-job frames can produce a mixed-funded chain whose recorded route is its first request’s. Two completions inside one job still share an operation; a non-job submit and poll split across two calls do not share one. A body over 16 MiB (maxBodyBytes) is refusedbody-too-largebefore the chain opens, nothing sent; an operation with no credential is refusednot-funded. Then the ordinary transport: the plug-in’s timeout, no redirect followed, the answer’s size bound and its shaping as on a key; the provider’s answer comes back as it would on a key, a refusal of the website’s —409 insufficient-fundsamong them — as its status, which the plug-in’s own error handling reads. The website marks its own refusals withX-FilmOpen-Refusal: <code>, and that header alone tells them from the provider’s answer, whatever a body says. A401whose refusal header is the portal credential’s lets the credential go (portalRefused) and is refused with its code, once, never resent.X-FilmOpen-Operation-Stateand the echoedX-FilmOpen-Operationare logged as they came and never acted on. Each request logsplugin.http.creditbefore the send — the plug-in, the platform, the operation, the method, the path after the base without its query, the bytes — thenplugin.http.credit.answeredwith the status, the state, the echo and, on the website’s refusal, the header’s code as it came,plugin.http.credit.refusedwith the stage (local;portal, with the code;provider, the status alone, for any answer of 400 or more without the header), orplugin.http.credit.failedwith the stage, the failure’s type and its message, each with the sending frame and milliseconds; never the bearer or the portal token. For ordinary model completions, the whole request body onplugins.http.startcarries that child frame;usage.operationOpenedjoins it to its operation and parent, and the credit lines carry the same child frame and operation. The AI completion’s start, done and failed lines carry the caller frame, joined by that parent. The operation-opened line also gives the function, model and scope decision’s reason.api.fal.aiand every other host stayno-keywithout a key, and so does a key check, which reads the person’s own key alone.ctx.keys()says a key not stored but granted is usable on credit where its platform is —usable: true,mode: 'funded'. A job’s collect and cancel read the route’s key stored when they run, as every other call does. A job on credit ends with the website’s receipt: the operation is read in the job’s ending call, throughProviderFunding.receiptwith the kindhttp, before its one terminal record is written — once, and once more after the cost collector’s bound (PluginUsageLedger.receiptWait, 30 s) where the result was eligible for a charge — and itschargedCentsis the record’s cost (charged, with its operation and receipt id); without one the cost is unknown, never zero. A cancel the provider confirmed (cancelled) first ends each of the job’s operations on credit once (ProviderFunding.end, loggedusage.endwith the operation, the reasoncancelledand the milliseconds,usage.endFailedwith the error’s type and message), so that the website cancels and settles it and the receipt read after it is the terminal one; no other call or state ends one, since its result is still wanted and itsGETsettles it. Apathcannot leave the base’s host: one that starts//or holds://isbadArgs/path, so fal’spathreachesqueue.fal.run, which the file names no paths for and which takes any. The same door decides for a function reached throughctx.call, which runs as its owner: a platform plug-in’srequestbuilds the address, and the door refuses it. The app’s own requests with a key — Provider keys’ check (http_key_verifier.dart) and dictation’s session (dictation_setup.dart) — go through the samemayReceiveCredentials, and a test reads every platform file’sverify.urland every access row’sendpointagainst its own file’spaths. The credential is read from the store underfilmopen/<owner>/<slot>and put whereauth.headersays, a header of the plug-in’s own by that name dropped — and the transport’s own headers,Host,Content-Length,Transfer-Encoding,Connectionand their kind, are never a plug-in’s to set on any request, sinceHostwould name another site behind the address the door judged; a redirect is followed only to another of the platform’s hosts and paths, overhttps, five at most; refused, with the reason the script receives, are a key not the plug-in’s own nor granted (no-grant), a key whose platform has no file (no-platform), no key stored (no-key),http(insecure), a host not listed (host) — a*.entry ofhostslists no address a plug-in names, only a bound server’s endpoint — a path outside the file’spaths(path), a path that cannot be made plain (bad-path), a redirect off them (redirect), a store that cannot be read (store), an answer over 16 MB (tooLarge), a timeout and a failed connection. A model completion’s answer is waited for as long as the provider takes (the owner’s word of 5 November 2026): a request made in a text provider’scomplete, whole or streamed, has no head bound of the host’s own — ended by Stop, by the provider closing, or by atimeoutMsthe plug-in asks, honoured as asked —, and the gap between its body’s bytes or its events is 240 s or the plug-in’stimeoutMs. Every other request — a job’s poll, a cancel, a server check — keeps a 60 s head and a 60 s gap, or the plug-in’stimeoutMs. Each request’s wait is logged with its source onplugins.http.start(wait{head, headFrom:provider|host|plugin|bound, idle, idleFrom:host|plugin|bound}). A model’s provider that never answers and never closes is waited for until Stop. The plug-in names the key, and the door adds the credential after the request is logged; an answer is searched for nothing (the owner, 29 October 2026): it is handed over as it came —{status, headers, json | text}, the JSON decoded once where the content type says JSON — so a platform that echoes the key hands it back, and only the transport’s headers,set-cookie,authorization,proxy-authorizationandwww-authenticate, are left out of the plug-in’s copy, by name. Without a key, onlycapabilities.networkhosts overhttps, with no credential. A request may instead name aserver:ctx.http({ server: '<setting>', path })names one of the calling plug-in’s ownserversettings — any other name isbadArgs/server, andkeyorurlbeside itbadArgs/keyor/url— and the host sends it to the address the person gave for that setting on this computer, over that address’s own scheme, with no credential, ever.pathis required and is joined byServerAddress.resolve(§5.14), one it refuses beingbadArgs/path; the value is parsed again at the door, whatever is on disk: what this machine holds, else the manifest’s default, with a held value that does not parse refusedno-serverrather than quietly replaced by the default — the person means the address they typed, whatever became of it — and nothing at allno-serverlikewise. The bare name is the ordinary form, and the plug-in’s own tag may qualify it as an output’s does, so one spelling works in both places. A redirect is followed only within that very address (sameOrigin), anywhere else beingredirect. A plug-in never supplies, parses or builds on an address, which is what lets the host reach the same server by another route one day with no plug-in changing; anAuthorizationheader a plug-in sets itself is sent, since it is the person’s server and no credential of FilmOpen’s. Every request logsplugins.http.startas the plug-in built it, before the door adds a credential (the owner’s word of 21 October 2026): the plug-in, the method, the URL, the plug-in’s own headers and the body — anembed‘s body as the plug-in gave it, before the host writes any medium in, an upload as its field, name, type, size and other fields; the key’s header, or the credit route’s target and headers, are put in after it and are in no line. A URL is logged as it was before the host put a credential into it, in every line of the app’s log and the developer’s alike: as the plug-in gave it, whole, its query included — aserverrequest’s route too — and the error’s message whole; the same holds for an output’sserverroute refused or failed. An exception’s text, or any URL a line carries, is written whole byAppLogitself (§6.2), wherever it is logged. Off credit it then logs one terminal line (the credit route’s failures are its ownplugin.http.credit.failed).plugins.http.donecarries the status, the bytes,onCredit, the milliseconds and the answer as it came, every header included;onAnsweredhears the plug-in’s copy, once.plugins.http.failedcarries the phase (sendfrom the start of each hop until the answer’s headers came,readafter), the reason the script receives (connection,timeout,cancelled,redirect,tooLarge, or a refusal before the send), the error’s type and message, and, for a socket’s failure, the operating system’s code (errorCode, 111 for a refused connection on Linux), beside the URL of the hop it failed on and the milliseconds. A streamed answer’s error event is logged as it came, its event name, code, whole message and data, and its transport failure with its whole message. A provider’s refused reply is logged (the owner, 18 October 2026): on any status that is not 2xx,plugins.http.refusedcarries the plug-in, the platform, the hop’s URL, the status,onCredit, the milliseconds and the reply’s body whole, as it came. The credit route keeps its ownplugin.http.creditlines. On every route, on credit or not, the answer’s three moments are logged with the milliseconds from the call’s start:plugins.http.response(the plug-in, the status, the hop,onCreditand every header as it came) as each hop’s head arrives, a redirect’s included, before the redirect is judged,plugins.http.firstChunkwith its first body chunk, andplugins.http.body(the bytes and the chunks) at its end.callruns a function another plug-in offers (above), andctx.callernames who asked;keysanswers each key the plug-in owns or asks for, whether it is allowed and stored and when it was verified, from the Provider keys states.project.manifest,.get,.entities,.resolveand,.scriptread the open project as the viewer resolves it —scriptanswering the rows of the format’sscriptOutline(§5.12) as plain data,{ type, tag, stem?, name?, kind?, depth, problem?, children }, withstemandnamethose of the version in use andproblemthe walk’s own code, so a plug-in reads the same script a person sees, flags and all; withget,resolveandentitiesthat is the whole project tree, read-only, and the eye colour of the character a scene casts is oneresolveaway;storage.get/.seta JSON file of at most 256 KB, and machine settingssettings.json, in<app support>/plugins/<tag>/(pluginDataDirectory, §6.4);asseta text file of the plug-in’sassets/;loga line written as the plug-in gives it:plugin.<tag>.<event>where the event is an event name (letters, digits and_.-, at most 64 characters, not an address), elseplugin.<tag>.linewith the words astext, its data whole — nested maps and lists as they are; the level is the plug-in’s (none isinfo,debugunder Verbose logs), and a line whose level is none of the four is not written as the plug-in’s line (a debugplugins.log.unwrittensays it) and, outside a developer-page run, counted (linesNotWritten); a lifecycle hook’s lines likewise, an assistant hook’s still dropped; the developer page’s own log of a watched run keeps every line as the plug-in wrote it, since that log is the page’s and the developer’s, taken before the door;progressthe call’sPluginCallControl;ai.completethe preferred text provider’scomplete, the model the options’ or that plug-in’stextModelsetting, logged whole —plugins.ai.complete.startwith the messages,.donewith the answer and its usage,.failedwith the code, reason and message of a refusal, before the call or the provider’s, or with the stage, type and message of an exception thrown on the way, which goes on unchanged — only for a plug-in that declarescapabilities.ai(shown on its consent) or is itself the preferred text provider; any other is refused (noCapability). Storage reads, writes and receipt retirement share one serial turn per owner, including nested callers. Genuine readable absence is empty. An unavailable folder, corrupt or non-object JSON, access failure or failed atomic write refuses asrefusedwith reasonstore, never empty receipt storage or a successful save. The size refusal remainsstorageFull; storing null retains its ordinary JSON meaning. A plug-in’s data files have one read: an unreadablestorage.jsonstays refused, and an unreadablesettings.jsonis read as no settings; either emitsplugins.storageUnreadableonce per plug-in and file in that run, with the plug-in tag, the file, and the error’s type and message; genuine absence is empty and not diagnosed as unreadable. A line’s level (pluginLogLevel): none orinfowrites at info;debugreaches the log only while the threshold is debug — the verbose logs setting, or a recording that runs (§6.8) —, so a plug-in’s diagnostics cost nothing in ordinary use;warninganderrorsay what they say; anything else writes nothing and is counted among the lines that went nowhere. For event lines reaching the application’s log through the ordinary log door, the host addsmsas milliseconds since that call began, replacing anymssupplied by the plug-in.
The first frame’s bytes. Before a render’s plug-in runs, the host copies its first frame and first reference from the bound film’s store into staging with a streamed 16 MB bound and a check of its MIME signature. A lookup by media reference prefers the frozen copy even when another input was granted the same reference first. The call’s grant serves that copy, retaining the film’s filename, and its RunBinding freezes the SHA-256 measured while copying. Both upload and embedding verify the exact bytes before the HTTP door sends them; a mismatch refuses mediaChanged in the application’s words and sends nothing. A replacement after preparation cannot retarget the call. A program route freezes every reference and receives the frozen descriptors with absolute path values in its call inputs. Key routes receive no paths. Every staged bytes file carries its known MIME extension (.jpg, .png, .webp or .mp4), with none for other types; reads also find earlier extensionless files using their metadata, and sweep keeps either naming by handle. The freeze log names the input, index, handle and path. The copy belongs to the call’s staged set and is released with its unkept handles, including a pending or failed answer. Neither the digest nor the grant is journaled. The dialog currently freezes the input reference at submission; a replacement during the queued wait can still reach preparation, and a restart does not preserve an input copy for a new call. No data URI or media bytes enter the log — a refused freeze’s cause carries the error’s message as it is —, and the media’s bytes never enter JavaScript.
Program output files. ctx.stage accepts exactly one of base64 and path. Path staging is available only to an output-capable call on a host-selected program route, inherited by nested calls. The path must be absolute and readable; its bytes must match its image MIME or video/mp4 with role video. The host streams it into staging without an application output length cap. Inline image/audio retains its 16 MB bound. Invalid forms, paths, types and roles refuse before staging, a missing source refuses unreadable, and a call ended during copying releases its file. Stage logs retain the plug-in, frame, program, arguments without inline bytes, path, MIME, role, bytes, handle, code, reason and elapsed time; an unexpected failure retains its type and words.
ctx.storage. A plug-in’s reads and writes, and the host’s removal of a settled job’s receipt, take one turn at a time per plug-in. A store that cannot be read is refused refused/store, never read as empty, and a write that would make it larger than 256 KiB is refused/storageFull. Each operation logs plugins.storage.start with the plug-in, the door, the key and a written value, then plugins.storage.done with a read value and the milliseconds, or plugins.storage.refused with its reason.
- The film’s media going out (
media_grants.dart,ctx_doors.dart,http_door.dart). The application reads and sends the media, never JavaScript, and a plug-in sends only what the call was granted: each descriptor a render is handed carries agrant(MediaGrants), an id issued to that call alone, and the grants ride on the call’s frame to every call it makes — a model plug-in hands its first frame to its platform plug-in’s offer, and the platform plug-in sends it. An estimate is granted nothing, nor is any call not handed descriptors, and a batch item records its inputs without their grants, which were the call’s alone.ctx.upload— declared ascapabilities.uploadand not from astatusor a key check — names its media bygrantor by the descriptor’smedia, exactly one of the two (badArgs/grantelse), a form part’sfieldand the form’s otherfieldsas text; the application reads the granted file from the film’s media store or its frozen first-frame copy, refuses it where it is not there (mediaMissing), over 16 MB (mediaTooLarge) or not the type its descriptor says by its first bytes (mediaNotWhatItSays), and sends it as onemultipart/form-dataPOST, the file’s name its name in the store, through the same door asctx.http— to its key’s platform with the credential, and a request of its usage chain, or to aserverof the plug-in’s with none, the form’s own type set after any header of the platform’s — answering what the service said, as it came; media the call was not granted isnotGranted, and nothing is sent. A descriptor’s type is a media type in its own grammar alone (a recorded one with anything beside it is not believed), since a form’s header and a data URL are written from it. A request’sembed— withcapabilities.uploadtoo, since media leaving the film is what consent names however it goes, and only with akey, never aserver(badArgs/embed) — writes each granted file into a copy of the request’s JSON body at its JSON pointer, as a data URL or base64, 16 MB in all, a pointer whose parent is not there beingbadArgs/embed; on FilmOpen credit (§6.6) a data URL piece is uploaded through the media client instead and its short-lived download URL written there, abase64piece staying inline, and nothing is uploaded until every refusal of the request is decided — the door’s own checks for its key and its road (PluginHttpDoor.keyRoad, whichsendasks too), each piece’s grant, type and pointer, and the 16 MB in all, and on credit the body’s bound with what stays inline; a refusal of the media client isrefused/media:<code>with nothing sent. The request’splugins.http.startlogs the plug-in’s own body, before any piece is written in, so no medium is logged as sent; an answer that echoes a file’s base64 or a download URL is handed to the plug-in and logged as it came. Each refusal is in the application’s words, since a plug-in may hand it on. - A key granted, or taken back, is in force for the next call at once (
setGrant): the registry’s view of that plug-in is changed before anything is awaited, and the plug-ins are then read again, which with a development folder of many packages takes seconds, so that a check or a dialog opened at once never says the key is still missing; the plug-in’s page is drawn from that view at once too, so that its box shows what was just done and a tap on it to take the grant back never gives it again. - Render jobs (
render_backend.dart,host_runtime.dart). The render manager’s backend in the application isPluginRenderBackend(§6.15), which runs each of a job’s calls —render,collectwith the receipt the plug-in kept,cancel, and the route’soptions(renderJobCalls) — through the plug-in the request’s route names, gated as a call to it would be (notEnabled,cannotRun) and never chosen again, bound as any call is and givenctx.job: the job’s id, its destination and the id of its key, which every call it makes inherits. A render is prepared from the request as a person’s is — its inputs granted, its seed the one asked for or a new one — and handed its stack, its destination and its quote in the contract’sQuote(quoteForPlugin); a render given media of a film is refused where another film is open when its call begins (refused/inputsOtherFilm, Another film was opened before this render began…): its descriptors and their grants are made from the film open then, so a picture of one film is never sent for a render asked in another, and nothing of either is sent; the answer is held to the definition. A job’s usage is one ledger across its calls: its render opens the chains and collection continues them. Every raw cost-bearing answer is captured with its basis and safe failure reason, including an HTTP503 or an otherwise unverified failure code and a voice failure before translation. The ledger closes only on an understood provider end or the manager’s local terminal settlement, including Dismiss; a poll or failed collection never discards its reported cost or resubmits work. Immutable answer lines and receipt-aware cumulative accounting retain each answer while counting the chain once (§6.7); a job its service still holds keeps its chains with its receipt (RenderJobCall.usage,PluginUsageLedger.saved), so that a collect after a restart continues them (resume) rather than opening others; and a render’s chains name the version it is of and the group its takes go to (target,field), its job running on no entity. What a render succeeded with is fetched and staged before anything takes it in (PluginOutputs.stageOutputs): anhttpsaddress or a server’s path downloaded without credentials and without following a redirect, checked by its first bytes as it arrives, with no cap of the app’s on its length; a handle the call staged kept; every other handle it staged let go; and the job’s stop heard between two outputs and during a download, which it ends — the render thencancelledat the stagedownloading, its possible charge kept on the job (a stop’s tile never saying it: the producer’s rule), and what it staged let go, a download that lands afterwards included. What a render’s takes are recorded with is made as it is sent: the run,{ tag, stem, hook: "render" }— the plug-in that ran and its stem then — the record of what it was asked (§12.2: the model, the platform, the prompt, the params with the seed, the inputs and, by name, their descriptors without their grants, and the treatment) and the seed. It goes with the job’s receipt while a service holds the work — the receipt is the plug-in’sJobunderjoband that record undermade, and the plug-in is handed itsJobalone — and with each output once staged, so that a render collected after a restart, or an output taken in after one, is recorded exactly as it would have been without it. The manager’sRenderOutputnames the staged file, with the seed the application sent where the plug-in applied it — never one an output names — the stack the answer named, and that provenance, the answer adding only the stack and, where it did not apply the seed,seed_applied: falsewith the seed out ofparams; outputs whose record cannot be made are not taken in (shape).itemopens an output for the insertion with the provenance it was staged with, andreleaselets it go.optionsanswers a plug-in’s stacks, their readiness and issues, what each needs of the machine, its choices and its quote (quoteFromPlugin); one with nooptions— a single way, the plug-in’s own default, as ready as the platform it runs on says and then as its ownstatussays, each asked where the render would run (statusWhere) — the plug-in’s own speaking for that default alone, so no other way of it is taken to be ready; and one whoseoptionsfailed, or that is off, a way that is not ready, with why. An options operation has one ten-second bound, covering its tag turn, a global slot, server resolution and both optional-function status fallbacks together. At the bound, or a busy admission answer, it returnsRenderOptions.busywith the last completed choices for that model, route and destination across parameter edits, and a finite UTCretryAt; busy keeps a remembered stack’s readiness only for equal frozen parameter values. A new ask is a fresh bounded operation. Late results are consumed without replacing remembered choices, and an older overlapping ask cannot replace a newer one. The scoped stop ends only that inspection; the runner keeps its tag until the call actually ends, and the bound never cancels an ordinary render. This door adds no dialog cancellation parameter; the consumer retains its generation guards;optionsgives a plug-in’s own words — a way’s label, a choice’s label, help and values’ labels — in the person’s language, and where a route that bills names no price, the catalogue’s estimate, the one its usage record starts at (§6.7), or none where the catalogue prices nothing there;candidateslists each plug-in rendering the model on the platforms its entry names — one that names no model only for a model that requires none of the film’s media, since a model given a first frame is handed descriptors a plug-in serving its whole platform was not written for, and would be sent without its picture — a ready server the person started first, then the person’s own computer, each with its readiness and, where it is not ready, why: never allowed, turned off, needing a newer FilmOpen, no key saved — as Provider keys records it, never the key — or none at all on a platform that bills, or, a key being saved, one of another’s that the person has not allowed the plug-in to use: the key first, in the order a person gives them, the key’s card and then the grant; a model plug-in that holds no key on the platform sends on the key there of the platform plug-in it calls (platformOf) — that plug-in’s own, no grant asked, or one it uses, as a model plug-in usesfo-openai‘s OpenAI key (fo-openai:openai), which the person must have allowed it — and is as ready as that plug-in: allowed, on and able to run, elsethroughConsent,throughOfforthroughNewerApp, the candidate naming it (RenderCandidate.through); its key saved, a key not saved naming the plug-in whose own it is (RenderCandidate.keyOf); and allowed to send on it, the grant it lacks naming it too (noGrantwiththrough); a route that callsrunorrequest— a platform’s own functions — of a plug-in that is not installed, where no platform plug-in serves it, isthroughMissing, naming that plug-in by its tag, a helper it calls otherwise being its calls’ to refuse; what Provider keys records of a key is read from the preferences, or, where a build keeps its records elsewhere, fromKeyRecordSource.readeralone (keyRecordOf); a resource is its platform and a stable digest of what stands behind it — the destination, else the key’s id, else the unique local provider’s tag and its declaredserversetting resolved at submission — so that model tags using that provider share its budget and no address or key id names a queue. A queued local request with a unique provider keeps that submission’s machine settings and resolved provider server, including an unset value, by its job id until its render begins, so a setting edit or a reloaded default cannot send it to a different resource, and two submissions of one request are two jobs of their own; the server resolved and the servers a render’s call takes are logged (render.server.resolved,render.server.taken) with the job. The call still binds the film, viewer and media when it begins, preserving the cross-film input refusal, while live grants and credential reads keep their own rules. A queued local renderer whose provider identity changes between unique and ambiguous, or between unique providers, is refusedbinding-changedbefore submission and releases its resource: no service receipt exists. An unchanged ambiguous provider remains on the shared resource. A non-null invalid stored server remains refused by the HTTP/output doors, never replaced with a default during capture. A local route whose provider is ambiguous joins one shared local resource of capacity one. For a route with a unique provider, its capacity is the one that provider declares (implements.platform.capacity) — the route’s own plug-in or one it declares a call on, where exactly one of them serves the route’s platform, by a key there, its own or one it uses, or, for a platform of kindlocal, by having none of its own — else 1 locally and 2 on a key. The person’s Stop of a job on FilmOpen credit ends its operations at once through the ledger (endOperations,usage.endwith the reasonstopped), one End per operation shared by the job’s later cancel, which otherwise ends them itself whatever its plug-in answers (cancelled); the job’s next request on credit waits for that End and is refusedoperation-endedwith nothing sent, and a first request whose record was being written as the Stop came is refused too, unsent, once that End is done — its operation, never submitted, needs no End, since the website opens one only at its first submission; the receipts read after it decide (plugins.render.stopped,plugins.render.endedOnCreditwithconfirmedand the milliseconds): every operation settled is the job’s confirmed end,RenderJobCall.endedOnCredit, which the backend answers ascancelled, confirmed (§6.6, §6.15). A managed job prepares its terminal Usage when a call ends the job and appends it after the render journal is saved; an unconfirmed cancel keeps its chain open. A direct one-call job’s usage ends with the call that ends the job, but not with a collect a stop cut short, which the cancel then ends, nor a cancel whose work had finished, which the collect after it ends; a job whose plug-in went away ends there, its usage with it.startRendersinstalls the application’s manager, its journal a file beside the jobs’ (FileRenderJournal, which sets a file that is not JSON in UTF-8 aside as.unread) namedrenders.journal.json— as the server sessions’ isservers.journal.json: names no job’s can be, so that the jobs’ own journal passes over both — taking over an earlier build’srenders.jsonorservers.jsonthe first time it is read, and restores it beside the first frame: after the plug-ins and the catalogue have loaded and settled (settled, a load another followed waited for with it), sweeping the staged files no render keeps and this session did not stage before any render runs — nothing where the journal was not read; closing the application closes the manager, so that calls in flight may end. Each job’s call logsrender.call {job, plugin, function, args}— the arguments as the render plug-in is handed them — andrender.call.answer {job, plugin, function, answer | code, reason, message, path, file, line, ms}, the answer as it came, before it is held to the definition or a voice’s answer is read as a render’s; the backend logs the outcome it read asrender.answer {job, submitting, charged, endedOnCredit, state, phase, error | answer | code, reason, message, path, file, line}(answerwhere it is not a map). - The film’s media coming back (
staged_media.dart,ctx_doors.dart,host_runtime.dart,plugin_outputs.dart). A service that answers an image or a sound inline, in base64, gives it toctx.stage({ base64, mime, role? }), which the host decodes, checks to be whatmimenames by its first bytes (bytesMayBe) and at most 16 MB — though what crosses from JavaScript is itself at most 16 MB of text, so an inline file tops out near 12 MB — keeps on disk under the plug-ins’ data folder behind a handle (StagedMediaStore,s_and 24 hexadecimal digits), and answers{ handle, mime, bytes }: JavaScript never holds the bytes past that call, and the host never fetches adata:address. Only a chain whose call may answer media keeps any — a render, for now — so astatus, a key check and an action are refused (status,keyCheck,noOutputs); bad base64, amimethat is not an image’s or a sound’s in its own grammar — a clip is fetched from its address — and arolethe definition does not name arebadArgs, bytes not of their typerefused/mimein the app’s words, and a disk that cannot take themrefused/staging; a file written after its call ended is let go at once (callerEnded). An output naming a handle is taken in from the host’s copy, asstagedFileNamenames it, only where the call’s own chain staged it and itsmime, if given, is the one it was staged as; any other handle is that output’shandle. No inline output is bridged: an output’surlwhose scheme is nothttps—httpanddata:alike — is a wrong answer,badResultatoutputs[i].url(plugin_outputs.dart). Each output is fetched on its own: a download that fails, times out or is refused for its bytes, and a handle the chain did not stage, are that output’s alone, recorded beside the others asdelivered: [{ n, fetched, error? }]under the take’sitem,errorbeing{ code }withdownload,mimeorhandle, and the outputs fetched are taken in; where none was fetched the render fails with the first one’s reason. What the plug-in said of the render is kept whole (container_capture.dart): its sharedmetadataand each output’s, objects with an integerv— absent allowed, any other shapebadResultat its path — are neither merged, completed, scanned nor read beyondv. Each take’s provenance carries, besiderunanditem,output: { n, reported: { id?, seed?, role?, mime?, width?, height?, duration_ms? }, metadata? }—nits place in the plug-in’s list, previews counted, and the facts as the plug-in said them, the duration alone respelled — anditem, the same on every take of the render, gains the sharedmetadataanddelivered(metadata_refusedis no longer written; a record from an older version that carries it keeps it as read);error: { code, message? }is named there for a failure’s words, cut to 500 characters. The answer side (render_backend.dart,voice_calls.dart): a failed answer’smessagealone is cut to that bound intoRenderFailed.message, null where it has none. A render refused before the plug-in runs retains the host’s own words in its outcome and the recorded request error. The render answer and kept-facts logs retain its code and message before the job ends. Everyrender,designVoiceandspeakis toldstageand, where the request continues a take,from, added in one place where both roads pass after the host builds its arguments, handed whole and never read as a place to fetch from. A voice answer’s sharedmetadatatravels as a render’s, so one capture and one record serve both roads. The seed a take records is the one the application sent; an output’s own is underreported. A refusal of the answer — a shape the list does not hold, an address that is nothttps, a grant or a server refused, a receipt that changed — and a stop stay the call’s, nothing taken in. What a call staged is let go when the call ends, its outputs taken in or not — stopped, failed or answered with no film to take them in — and what an earlier session left, because it crashed, is swept before the first call of the next. - Platform hooks (
call) run through the plug-in chosen for the model asked for (provider_choice.dart): the catalogue entry → the platforms its access rows name, read from the entry as written, so a local runtime’s row with nomodel_idcounts (or only the one the request names asplatform) → the enabled plug-ins that implement the interface —render, ortextforcomplete— on one of them → those that name the model inmodels, else those that name none → where no platform was named, those on a platform ofkind: localbefore any that bills — before the person’s preference too, which is one per kind and not per model, and would otherwise send every render of a model a local plug-in names to a service that bills → among several still, the person’s preferred plug-in of the kind (media,text) → else none. The choice is made once, before anything runs, and never made again: a render that fails is a failure, and nothing is sent to another plug-in in its place. None isnoProviderwith which cause it was and the app’s sentence —noModel,noEntry,noPlugin(said with the platform where the request named one),choose(More than one plug-in serves Z-Image-Turbo (…): choose one in Settings → Plugins.).estimateCostmust say theoperationit prices,renderorcomplete(elsebadArgs/operation), which picks the interface and is handed on; a render’s estimate is prepared as the render is, without what the entry requires, since it may price a request that is not finished. The log carriesplugins.chosenwith the model, the stem and why (named,generic,preferred,local).ctx.ai.completeuses the LLM decision frozen for its call: the assistant’s turn where present, else the frame’s decision, else a decision captured for that call. It ignoresoptions.modeland the provider’stextModel; none reachable isnoProviderwith its reason. Every answer ofrender,complete,estimateCost,verifyKeyandtransformis held to the definition before the app reads it (badResult, with its path), and a completion’smessagesbefore any plug-in runs —ctx.ai.completeor a string isbadArgs/messages, never an empty conversation sent on a key. A choice made before a wait stands only while its plug-in is on: a completion that waited its turn behind another isnoPluginwhere the person turned the chosen plug-in off meanwhile, rather than run by it, and actx.callthat waited its turn asks its whole gate again of the version then on —notEnabledwhere it is off — while a call that waited for one of the five slots isdisabledwhere its plug-in was turned off meanwhile. Every call a person starts — an action, an entity hook, a platform hook — keeps a usage ledger (usage_chain.dart, §6.7), and a call made throughctx.ai.completerecords into its caller’s: a chain per key the call sends,startedbefore the first request carrying that key —purpose.pluginis<stem>/<hook>of the plug-in the person ran (action.<id>for an action),entrythemodelthe call’s own frame was given (a nestedcomplete’s own, else its caller’s), elseunknown,variant,kindandmodel_idfrom that entry’s access row, the key’s last four, the call’s estimate — and its end once the call answers, with what it spent as What a call spent above says:succeeded,canceled, orfailedwith the outcome’s code. A request whose first record cannot be written is refused (usageLog). A call that sends no key writes nothing. A platform hook is handed its catalogue entry asargs.entry, the entry’s file as it is, so the plug-in reads the platform’s name for the model and its prices. What arenderis told beyond that, prepared once byprepareRender(render_request.dart) for Generate and for the developer page alike (render_inputs.dart):inputsis an object keyed by the catalogue’s own input names (§14.8) —first_frame,last_frame,maskandaudioeach one descriptor,reference_imagesa list — a descriptor being{ media, mime, bytes, width?, height?, durationMs? }, wheremediais the reference as an entity’s file writes it and the rest is what the import measured and recorded in the batch, never invented — read throughProjectMedia.recorded, the one lookup, so that a descriptor’smimecannot disagree with the kind the app itself believes. A record is a file and may say anything: a measurement that is not finite, is negative or is beyond any file’s size is dropped rather than passed on or thrown. The application reads and sends the media, never JavaScript: a render with inputs reads what it was given, and sends a file only by its descriptor’s grant (The film’s media going out, below). The inputs are the entry’s to allow: a name itscapabilities.inputsdoes not list — the format’s own list where the catalogue has no entry — a value that is not a reference and a reference that is not media are eachbadArgs/inputswith the name, one the entry marks required and the caller left out isbadArgs/inputs(i2v-kling-30 needs first_frame) — a required list that names fewer than the entry’smin, none where it says nothing, being left out — as is a requiredpromptleft empty (badArgs/prompt), and each piece of media is held to what its input takes (inputRefusal, §5): one of a format its input’sformatsdoes not list — by neither its descriptor’smimenor its file’s name — isbadArgs/inputswith the name (i2v-ltx-25 takes first_frame only as PNG, JPEG or WEBP.), and one the import measured as larger than itsmax_mblikewise (i2v-kling-30 takes first_frame of at most 2 MB., the size as the entry names it, never rounded) — and the plug-in never runs; inputs with no film open are refused (refused/noFilm), not emptied; nothing at all is{}, and a list, empty or not, is refused as any other wrong shape.seedis the caller’s where one is given —seed, orparams.seed, the two agreeing where both are — and one the app makes where none is, from the platform’s secure source, so that Generate twice does not give the same picture from a server’s cache: 0 to 4,294,967,295 inclusive, 0 being a seed like any other and a number with no fraction being one; anything else given —-1,4294967296,1.5,"7"— isbadArgs/seed, never replaced, since a person who asks for a render again and gets another seed in silence has been guessed at. It is never the plug-in’s to choose.sourceis{ stem, type, tag, epoch? }of the entity the render is for, absent where there is none.render’soutputsarehttpsURLs, or{ server, path, mime }on a server one of the answering plug-in’s ownserversettings names:serveris that setting, bare or qualified as<its tag>:<setting>, or another plug-in’s where the answering plug-in declares a call on it that does work on that server (s: a plug-in allowed to run jobs there can already make the server produce anything it can read, so naming a path to download adds nothing to what consent gave). The declared call must belong to theplatforminterface and passplugInsMayCall: todayrunandrequestauthorize the fetch; custom offers andstatusdo not. It is spelled against the contract and not against those two names, soplatform’scollectandcancelare admitted the day they stop beingplanned: a plug-in that may collect a job’s outputs is precisely one that must fetch them. A tag it does not call, one it calls only for custom offers orstatus, one whose plug-in is off — which runs nothing there — and one that cannot run at all are eachrefused/no-grant; both forms in one output is refused by the definition before anything is downloaded (badResult,severalShapes, with its path), anhttpurlisbadResultat its path, and a name that is noserversetting, a missingpathand oneresolverefuses areshape; a held value that does not parse isno-server, as it is at the door. Because a route on a person’s own server need not end in a file name — ComfyUI’s is/view?filename=…&type=output— the name a staged output is known by, from which the insertion reads its kind, is the query’sfilename, else the address’s last segment where it has an extension, else a name from the declaredmime, and an address whose last part cannot be read is named by its type rather than thrown. A redirect is not followed, as the door refuses one: the address the person gave is the address, and a302from a plain-httpserver on a network is the easiest thing to forge. A render answers bystate: throughcall,succeededanswers itsoutputsas the plug-in gave them,failedis the call’s failure with the plug-in’s ownerror.codeand words, andpendingisnotSupported/pending, in the app’s words, since only a render job can be asked about again. A call takes nothing in (§6.13): a render’s takes come through the insertion, the one place a take is written, from a render job (Render jobs, below), whose outputs are fetched and staged first. An output whoseroleispreviewis never staged, and one whose bytes are not the type itsmimenames is not kept: staging reads each file’s first bytes as they arrive (output_check.dart: the signatures of the common pictures, clips and sounds, and markup or JSON, which is what a service answers when it fails), and that output is not taken in,mime— the job failingmimewhere it was the first of outputs none of which was fetched; bytes that say nothing the reader knows are taken for a type it has no signature for, never for a PNG, a JPEG, a GIF or a WebP (signedPictures), whose every file starts with one — the same reading for a file staged or uploaded; and a picture answered inline, throughctx.stage, must end as its type ends where the end can tell — a PNG with itsIENDchunk, a GIF with its trailer, a WebP at the length its header gives (bytesComplete) — since base64 cut on a boundary of four still decodes; a JPEG, which may carry bytes after its last marker, is judged by its start. Each staged output is taken in as a take of the source entity by the derived handle<viewer>.<tag>, in akind: "plugin"batch naming the plug-in and the hook, and each take’s item records the request as the app prepared it —model, theplatformthe choice was made on,prompt,paramswith the seed, and the inputs twice over (PluginRun.item,PreparedRender.recorded):inputs[], the Project Specification §12.2’s flat list of media references, written whatever the render was given and empty where it was given none; andinputs_by_name, the same media under the catalogue’s input names (first_frame,reference_images,audio, …, §14.8), each descriptor as the plug-in received it —media,mime, and thebytes,width,heightanddurationMsthe import measured where it measured them — written per name — a name holding media recorded, a name given an empty array not, and the key itself absent when no name holds anything — since a flat list cannot say which reference was the first frame and which were references, and a copy, the batch being a document of its own (the format’s draft 1.15) — and from the answer onlystack, whole, andseed_applied: false, the item’sparamsthen holding no seed. Which film a render’s takes land in, and whose they are, is the insertion’s: the place captured when Render was pressed (§6.13, §6.15). A stop between the answer and the write is honoured bywriteVersions, which reads the call’s control:usage.endwrites the call’s last usage record between the plug-in’s answer and the versions, and that is a file write a stop can land in. A render job stopped while its outputs are fetched lets the fetch finish and gives up what it staged. What a run reads is the bound viewer’s index: the current open index when the app’s viewer is the bound viewer, otherwise the latest rawHostEnvironment.baseIndexderived withforViewerfor the bound viewer. A closed or different film uses the captured index. A derived index is never re-derived for another person. What a request may carry is read live: the keys a plug-in may send come from the registry as it is now, so a grant the person takes back while a call runs is taken back, the developer page excepted, its plug-in being a working copy the registry does not hold. A prompt is kept in the record its takes are written with. LLM messages and answers are logged whole, excluding credentials; usage records keep their accounting fields. - A job’s turn (
plugin_call_runner.dart). A render job’s call, a server call, a key’s check and a prompt writer’s call during Auto-generate pictures (§6.15) wait their turn at a plug-in another call is running (wait) — a Save on Provider keys is not refused because the provider’s servers are being looked after, and waits at most twenty seconds (checkWaitsAtMost, fixed when the runtime is made, a platform’s check too) before it says the plug-in is busy — where a person’s other gestures are toldbusy; an ending call hands its turn straight to the first one waiting, which a call that does not wait never overtakes, and one stopped while it waits never runs and hands its turn on, so serial calls of a plug-in still run one at a time. A call on a program whose number of copies is above 0 never takes or frees that serial turn. Admission logs carry one frame identity from entry through the plug-in and global waits, admission or refusal and release, with the function, hook, whole arguments, resolved program, wait settings, decision, reason, terminal outcome and elapsed milliseconds. - Servers on demand (
host_runtime.dart,server_backend.dart,server_gateway.dart).callServeris the one door to the seven functions of theserver-providerinterface —serverCapabilities,serverOffers,deployServer,listServers,resolveServer,wakeServer,stopServer— called by the application’s server sessions alone (§6.15), the only road on which a server is started or woken (PluginManifest.startsAServerrefuses every other). Each runs as the interface’s function on an enabled plug-in that implementsserver-provider, its answer held to the definition, so that an offers answer whose list is missing or not a list, or holds a row that is not an object, is refused with the definition check’s code before anything reads it; any other call, or a plug-in not enabled, is refusednotFoundand logged asplugins.server.refused{tag, plugin?, function, code, reason:not-enabled|not-implemented}. Every call answered is logged asplugins.server.answered{plugin, function, args, outcome:donewith the answer’svalue|failedwith itscode,reason,messageandpath, an answer the definition refused logged as the provider gave it (answered), ms}. A valid empty catalogue stays valid. The sessions’ reports reach the log as they are, atwarningfor a failure or a refusal — an event named…Failedor…Refused,servers.notMade,servers.usageNotRecorded, or one whose data carries anerror, acode,refused: trueor anoutcomeother thanlisted— and atinfofor the rest (serverReportLevel). Offers arguments pass unchanged: an empty object asks for the catalogue, andgpuClassasks for one class’s capacity. A presentquotapasses through unchanged, including null; the sessions decode the raw/quotasobject’scontainer_groups_quotas.container_replicas_quotaandcontainer_replicas_used, failing closed on exhausted or malformed values, while absence leaves the provider’s deployment gate in force. A quote says where a list emptied: eachserverOffersanswered is logged asplugins.serverOfferswith the offers the plug-in answered, how many it answered, how many of them a person is shown once read, and how long it took.PluginServerBackendforwards optional discovery limits through the private host dispatch: one absolute instant and cancellation signal cover admission and execution. A scoped call uses the smaller of the parent remainder and its per-call cap for its admission, wall and fallback limits, with cleanup inside that child; an expired instant, completed cancellation or remainder shorter than the cleanup reserve refuses before admission. Cancellation and expiry retain their first terminal code. At the operation end the model has its terminal answer and requests are closed; admission is released as soon as durable Usage is written. Nested engines may drain CPU for their established grace. Unscoped calls retain their per-call behavior. Model stacks, every member optional:serverCapabilitiesmay answerstacks, each{id, engine, label, requiredVramGB, downloadGB?, coldStartMinutes?}— the minutes an estimate unless measured; an answer with a row lacking any of the four required members is refused whole by the host’s check against the definition, and the sessions then read no capabilities (ServerStack) —;deployServeris sent astackonly where the person chose one, and anengineonly where the provider offers two or more — the window’s engine picker’s, its first by default —, so that with one engine and no stack chosen a deployment asks what it asked before and the provider chooses its default; the server window deploys only at the hourly price it showed (§9.7a); aServerStatusmay name thestackits server holds, which its session keeps, else the one it was deployed with, through adoption and the journal; the engines named arecomfyuiandwan2gp. The sessions log what they read and ask —servers.capabilitieswith the engines, the idle range and every stack read, or the code of an answer that is not capabilities;servers.deploywith the arguments sent, the offer’s label and price;servers.deployedwith the answer’s state, the provider’s word, its stack, the session’s phase, problem and stack, and the milliseconds it took;servers.deployRefusedwith the code of a deployment refused before it was asked;servers.capabilitiesalso for a kept read (cached) and for a spent or cancelled discovery (refused, its code), each with its milliseconds;servers.quoteFailedandservers.journalFailedwith the error’s type and words. What the sessions report reaches the log as it is (serverReportData), a provider’s code and words whole: a report holds no credential.PluginServerBackendis the sessions’ backend: the providers enabled now with the platform their key manages servers on (providers('server-provider')), each call through that host dispatch with every endpoint taken out of its answer — an address stays with the host — and a compute session’s records in the usage log, theirkeythe last four characters Provider keys recorded. - Every server call is bounded (
host_runtime.dart,ctx_doors.dart;filmopen_state‘sDeadline).callServergives each call the server-discovery budget, thirty seconds (serverCallBound,serverDiscoveryBudget) —deployServeralone a hundred and fifty (deployServerBound), the overall cap that a provider’s finite wait for its server’s creation fits inside, used alike for its turn and slot, its requests, its stop, its wall and its outer answer — the grace held inside it, so that even a terminal Usage record that holds is answered within it —, so that one not done by then endstimeout, is kept uncertain and is looked for by its name, never asked again — from before its turn and its slot, each call’s bound logged as it starts (plugins.server.bound{plugin, function, boundMs, graceMs, scoped, parentMs?}) and a call refused before admission logged with its code and why (plugins.server.refused{plugin, function, code, reason:cancelled-before-admission|too-little-left, ms, parentMs?}): a control of the host’s own stops it a grace before the bound (serverGrace, a second), so that a call still queued behind the five slots or behind its provider’s running call leaves its queue, and a running one is cut with its requests closed by the bound; its slot is released promptly after its Usage record is written, so a held append delays admission release. The call’s frame carries that instant, and every request down its chain —ctx.http,ctx.upload, a nested call’s — is given what is left of it, its owntimeoutMsconsuming the bound rather than restarting it, and is closed when the call is stopped — a request carrying the film’s media as well; a call bounded by nothing in a render’s chain — a job’s, or underrender,collect,cancel,speakordesignVoice— keeps its requests to their own timeouts and to their answers, which say what the service charged; any other call’s requests — an action’s, a model’s completion — are closed when it is stopped (plugins.http.stopped{plugin, frame, function, bounded, reason}), since a model’s head has no bound of the host’s own and Stop is what ends it. A request asked once the call is stopped is refusedcancelledbefore it leaves. A call the bound stopped answerstimeout— whatever the plug-in made of the refusal, an answer arriving just after the stop, and a wall that was the bound alike — with no reason, which the server panel says in the sentence it gives a refusal, and the sessions recover a start answered so through their list; a stop the caller asked for is forwarded and stayscancelled; a call that did not stop within its grace stayshung. A timed-outserverOfferswrites its ownplugins.serverOffersevent with the plug-in,outcome: timeoutand elapsed milliseconds; it has no counts, since no list was read, and no offer details or credentials. AdeployServercut so is an unanswered start, which the sessions keep as uncertain and look for by its name (§6.15), and a render’sresolveServercut so refuses the render as having no running server, its engine never asked. A provider whose quote probes more than the bound allows — every GPU class of a large organisation — answerstimeoutrather than a list; the whole operation’s budget over capabilities, quote and a selected offer is the sessions’ to compose (§6.15). A busy options answer without completed choices contains one not-ready stack and no invented quote, so the dialog waits for an answer it can use. Completed choice ids and schemas are remembered for the model, route and destination across parameter edits. A remembered quote, machine needs and readiness belong only to the same deeply frozen parameter values, compared by JSON value rather than map order. A busy answer for different values carries no price or parameter-dependent facts and marks the retained ways not ready, so the existing dialog holds Render. The latest ask alone may replace the memory. A collect or cancel on an explicit server reads its destination and gateway key as stored when it runs.
Renders on a program. A render declaration’s optional program names this plug-in’s machine program setting. A configured setting adds a program candidate for entries on the declared platform whose tag is in programKinds (default ["t2i"]), restricted to models where listed. Program admission is independent of row render marks and HTTP upload permission. Key routes retain their marked-or-named and upload rules; another program setting creates no route. The candidate follows the ordinary plug-in consent, enabled-state and admission rules. RenderRoute.program is optional, omitted from JSON when null, and included in decoding, equality and hashing, so the same model’s key and program routes remain distinct. Preparation, options and dialog submission copy the whole route, including its program. Settings → Models and the last render options reopen the same concrete key or program route. For any program route the render dialog and Auto-generate route line say through {program}, using the program setting’s label without a subscription or free-use claim; the dialog names it before any options answer. The route decision log names the same program funding label; Usage names the account only after the program answers. The program’s scheduling resource has the capacity of its number of copies — the person’s, else the plug-in’s default, 0 as one (RenderCapacity.of), read as each job is submitted, so a change applies to the next picture, under the manager’s ceiling of 3 across every resource — whatever its platform declares, logged render.resource {plugin, program, copies, capacity, reason}, and carries no FilmOpen monetary bill. Options and render receive the host-selected funding: {route: 'plan', program}. The quote is explicitly unknown, rather than the API catalogue’s price. Before dispatch, the host opens token-only plan Usage under the program setting; the process answer supplies the actual account label, and its tokens settle without pricing or credit operations. render.candidates records the program and admission decision; render.route.program records the route and returned account, and the Usage lines carry funding: plan.
OpenAI reference renders. The GPT Image image-edit access rows are marked for rendering. The stock picker selects GPT Image 2.5 on OpenAI for reference-image edits at every stage: Flare at Prototype and Sunburst at Draft and Final with high quality. The first configured route is used, Codex then the person’s key; a configured but signed-out Codex fails on that route, without fallback. An explicit Settings choice of fal still wins. An OpenAI image-edit candidate without a saved API key stays unavailable even when FilmOpen credit is funded: credit substitutes download URLs for embedded pictures, while the edit route requires inline media. The candidate log names this decision creditNeedsInlineMedia; text-to-image credit and configured program admission retain their own rules. The renderer sends ordered references on the person’s key to the chosen row’s /images/edits endpoint as JSON, using its mapped reference member with image_url placeholders and one host data-URL embed per picture. Text-only requests retain /images/generations; masks and other inputs are refused before sending. Reference media identities participate in the existing durable job fence, so another reference cannot reuse a job. A host admission refusal from the plug-in preserves its own words, including the existing 16 MB combined embed bound, without Usage or another submission. A refusal while freezing an individual reference retains the host’s words in the backend outcome and the recorded request error, which supplies the job tile’s explanation. A refused error-fact write ends as a journal failure. Image-edit Usage retains tokens with unknown key cost where the catalogue has multiple modality-specific rates. Render mapping, dispatch, billing and refusal logs retain the inputs by media, MIME, bytes and path, the body, embed positions and the endpoint; the HTTP door logs the complete answer and elapsed time, injecting the API key after request logging.
Codex pictures. The OpenAI renderer uses one ephemeral gpt-5.6-terra thread on the render call’s ctx.operation lane, after checking that Codex serves that text model; the image tool itself takes no model. Its turn asks the model to call the image tool at once for exactly one image and to end with no closing message, followed by the person’s prompt, at the lowest reasoning effort Codex’s model/list reports for that model — none is sent where it reports none, or none it reports is a known effort — and the turn is still read to its completion, for its usage. A completed imageGeneration item’s inline base64 result is staged as PNG through ctx.stage; its savedPath is ignored. The render accepts text-to-image without media and image-to-image with ordered reference_images carrying staged paths. An edit’s turn sends the text first, naming the attached pictures in order as image 1, image 2 and so on before the person’s prompt, followed by one localImage item per path. It returns Codex’s own account label and tokens and stops only its lane. A turn ending without an image fails with no-image and any text Codex returned, without retry or route fallback. Program options answer ready with an unknown quote without checking a key or starting Codex; the program is checked at render. render.codex.start, effort (the effort sent, the efforts reported and the reason), event, image and fault, with the protocol and cleanup lines, retain the operation, thread, whole arguments, prompts, events and results, decision reasons and failures; the host adds elapsed milliseconds. Logging masks and scans nothing, and credentials remain outside the plug-in’s logged arguments.
Grok sign-in. The xAI plug-in chooses its signed-in slot once for text readiness and the request; Check reads the model list on a stored sign-in. A refused plan call retains xAI’s words and does not change to another route. Its plan renderer sends text-to-image and ordered reference edits to the image API with the catalogue model and selected enums, receives inline base64 and MIME type, and stages the outputs through the host. One reference uses an image URL object and several use an ordered array of URL objects, with granted media embedded by the host. Options retain an unknown quote. Provider-reported dollar ticks stay in the log; plan Usage keeps account and tokens without pricing. The video renderer uses the host job’s selected slot for submission identity, receipt checks and polling, retaining its existing durable fence and refusal rules. The credential-free request is logged before injection; whole answers, image bytes, staging results and failure stages are logged as they arrive. Elapsed time comes from the host.
Grok Build pictures. The xAI renderer supports text-to-image and image editing on the person’s Grok Build program route; the unmarked image catalogue rows offer no key or credit image route. Each render uses one fresh ACP session on its operation’s owned lane, in the host’s private working directory, with no MCP server and a profile limited to image_gen, image_edit. The prompt asks for exactly one image and no closing message; editing gives every frozen reference’s absolute path in order. Grok Build chooses the image model, which FilmOpen does not select or claim. Each observed tool call and update is logged by name, kind, status and whole input/output before only image tools enter media handling. Each completed image tool’s file is staged by path, with its JPEG, PNG or WebP MIME derived from the extension; extra pictures are kept too. A failed tool returns grok-refused with its words after the original turn drains for usage; a stop other than end_turn is grok-failed, and no image is no-image with Grok’s text. A missing or unsupported image filename extension is grok-image. A host staging refusal retains its code and names the path Grok gave and the host’s reason in its message. Finally the render stops only the lane it started. Options start no program and offer one grok stack, an unknown quote and only aspect_ratio: auto, 1:1, 16:9, 9:16, 3:2, 2:3, default auto. Native input, output, cached and reasoning tokens label the account Grok Build plan; missing usage stays unknown and USD ticks are logged without monetary billing. The render.grok.*, options and ACP lines log whole arguments, paths, prompts, tool inputs/results, protocol answers, usage, failures and cleanup decisions; the host adds elapsed milliseconds. Nothing is retried, masked or scanned, and no route fallback is made.
Grok video. The xAI renderer animates a required first frame through the signed-in plan with an unknown quote, or through the key or FilmOpen credit API route at the catalogue’s per-second resolution price, fences each paid submission durably before POST, polls once per host collection, delivers the returned MP4 URL through the host, keeps reported charges even on failure, and also offers Grok Build’s reference_to_video at 1–15 seconds and 480p/720p with an unknown quote, returning grok-zdr with an explicit API choice when zero data retention prevents a clip and logging whole prompts, requests, answers, receipts, usage, decisions and failure details with host elapsed times.
Exact-job storage convention. The key job:<exact ctx.job.id> is the disposable dispatch and recovery receipt of that job, in the store of the route’s plug-in or of the platform’s plug-in it calls. Per-job history and any record that must survive settlement belong under another key. Settings, caches and keys outside this reserved exact-job namespace are never removed by the host.
Recovery of a render job across a restart belongs to BackendRenderManager, which registers the job before submission and restoration and settles its terminal Usage. A direct PluginRenderBackend caller without that manager has no supported cross-restart recovery. An answer whose stable id the usage log already holds — a cancellation repeated from the original pending receipt — is not appended again, whatever it says. Registering a job without a manager that settles it leaves its terminal Usage unsettled and is not a supported mode. The executing owner’s job:<exact ctx.job.id> is its disposable receipt and cancellation fence; long-lived history and billing never belong under that key.
Receipt retirement. Managed provider-end confirmation requires an understood affirmative answer: success, confirmed cancellation or a verified terminal failure. Unknown, unreachable, receipt-unavailable, job-mismatch, host failures and unverified failure codes retain uncertainty. The same confirmation rule governs backend failure and provider-driven managed ledger closure; every call’s reported Usage is captured independently of that authority. The manager’s journaled local terminal settlement or Dismiss may close the ledger without provider-end confirmation. Local Dismiss never proves provider completion or grants the removal of a receipt. A settlement the host prepares after an end confirmed in this session says so (providerEnd), and the manager journals it with the complete prospective terminal or output-ready state before applying it. Settlement replays Usage once, then removes job:<id> from the store of the route’s plug-in and of its platform’s plug-in, in each plug-in’s storage turn, logging plugins.storage.remove.start and plugins.storage.remove.done (removed, with the value, or absent), then plugins.receipt.removed (the plug-ins, the job, the reason providerEnd and the milliseconds), or plugins.receipt.refused with the plug-in and the store’s reason; a settlement without that word keeps the receipt and logs plugins.receipt.kept. A collect and a cancel log render.receipt.read: the receipt as saved, the fields handed on (job, made, usage) and those left unread, such as an earlier build’s binding; a cancel then logs render.cancel.state with the state, its decision (answered, endedOnCredit, callFailed with the failure’s code, reason, message and path, or unreadAnswer) and the milliseconds. A job’s call refused before its plug-in is asked — the plug-in off, unable to run or not the developer page’s, a queued render’s provider changed, its server unresolved, its inputs of another film, its preparation or first frame refused — logs render.call.refused with the job, the plug-in, the function, the model, the route, the destination, the receipt, the function’s arguments, the code, reason, message and path and the milliseconds, since no render.call follows. Each reload logs plugins.settings.read per plug-in: the values read and the decision — saved, absent, or default with the reason notRead, plugins.storageUnreadable naming why — and plugins.settings.overlaid for an accepted edit held over the read. An unreadable store or failed write retains saved settlement for Retry without collecting or submitting. Uncertain settlements, and those an earlier build journaled without that word, keep their receipts. Initial uncertainty without an acknowledged pending receipt cannot be collected through the current contract; it finishes locally while retaining its plug-in fence and receives no retirement authority. A collection HTTP error, including a client refusal or gateway failure, observes no end of the accepted work and cannot close its ledger or retire its receipt; any cost and safe reason it reports are retained for later collection or local settlement. An initial render HTTP rejection is definite only for status400,401,403,404,405,413,415 or422; other HTTP failures retain uncertainty. Cancellation confirms an end only with an explicit cancelled answer. An unmanaged call forgets its affirmative-end authority when that job ends, so a later managed use of the same id cannot inherit it.
A collect and a cancel read the key and the settings stored when they run, as every other call does. Render candidates and options on a key route require a saved key; a slot with no saved record answers noKey, except where FilmOpen credit pays the route’s platform (the resolver answers CreditFunding for its sends, §6.6): a signed-in person with no key then reads a ready route, and its options are the plug-in’s own, its quote included. A route through a running server requires that server provider to be enabled. Where the provider declares a selected gateway key, that key must be saved or FilmOpen credit must fund the provider’s HTTP operation under the same funding test as a billing route; without either, a missing record answers noKey, and a disabled provider its own off reason. A provider declaring no key has no gateway-key requirement; billing does not create a key slot. Candidate readiness reads metadata only, opening no credential and sending no request to resolve or submit to the gateway; where no key is saved on a billing route or its gateway provider it asks the resolver locally once per platform a reading, an answer slower than the options’ ten seconds being no credit, and a saved key asks nothing. The portal credential is asked only when a funded request is sent. render.ready.gateway logs the server, provider, road and reason. Ordinary calls keep their existing key access; the record id Provider keys stores beside a credential is never read as one. Each render gets fresh input grants, and each render or collect gets fresh staged-output handles; an expired grant or another call’s handle grants nothing.
An acknowledged receipt restored from the render journal is collected with its original opaque Job and is never submitted again. When collection completes, each paid Usage chain has one effective start and end. If the provider accepted a submission but its answer never reached the host and no receipt was journaled, restoration makes the job terminal and interrupted. It neither collects nor resubmits that work, retains the plug-in’s dispatch fence, and leaves each started Usage chain open without a terminal settlement. The current backend has no restoration hook through which the host can recover the owner’s stored receipt.
For a render managed by the application’s queue, the host waits for every in-flight request to settle before preparing terminal Usage records in memory. The manager’s cancelled disposition wins a local Stop while retaining the call’s billing answer, and an unchanged failure keeps the call’s original code and reason; no request may add a terminal record after the settlement payload was copied. Their ids are deterministic from the render job and the original Usage chain. The manager saves them with its terminal render state before the host appends them. Replay serializes reading and appending, skips a record whose id the log already holds, whatever that line says, and refuses an incomplete Usage reading or a malformed payload; it logs usage.settling, usage.settlementRead and usage.settled with the lines appended and skipped, each with its id, amount and chain, a skipped one with the logged line and whether it matches, a refusal usage.settlementRefused and a failed append usage.settlementNotWritten, with what was appended before it. A restart between the append and the consumer’s batch write therefore replays the render effect once; the insertion’s own reconciliation rules remain unchanged.
A synchronous answer’s provenance. A render answered at once — its outputs inline, no receipt — that fails after its service answered a request on the person’s key, whether the provider’s own error in the body or an output the host refuses, is a failed answer: the call keeps reachedService, so the render says it may have cost, keeps the handles it had staged until the backend lets go of every one no output keeps, and keeps its Usage chain — closed as a charged failure, or held for the managed job’s end where the provider confirmed none. It has no receipt, so nothing collects it and nothing sends it again. A failure thrown by the host once the plug-in has answered — its answer being held, its Usage being kept — is a failed answer still (failed, logged as plugins.renderAfterAnswer with the plug-in, the job, the error’s type and its message): the call keeps reachedService and its staged handles for the backend as above. A failure thrown before any answer is not a failed answer: nothing was produced, and every handle the call had staged, the first frame’s copy included, is released before the exception leaves the call.
An installed agent keeps its render journal in a private folder and never sweeps the staged folder shared with the ordinary application. On Linux and Windows, a nonempty FILMOPEN_AGENT_DATA selects the fixed filmopen-agent-render child of that private root before any platform fallback. Without it, Linux uses that child of XDG_DATA_HOME, and Windows preserves FilmOpen/filmopen-agent-render beneath LOCALAPPDATA. The folder is created rather than temporary, so relaunch with the same root recovers the journal and removal of an owned root removes it. A Linux agent started directly still requires private XDG roots; absence of both the explicit root and the platform data root refuses durable startup. Other platforms use a private process temporary folder. Its render manager never reads or writes the supplied personal jobs folder, on any platform. The ordinary application keeps its file journal beside the jobs, and a build supplied no directory keeps the journal in memory. The installed automation host selects the private journal before its container is attached; operational availability is not the startup identity test.
- A render bound to a server (
server_gateway.dart,run_binding.dart,http_door.dart,plugin_outputs.dart). A job’s call whose request names a destination of kindserverresolves it once, as the call begins (resolveServer), where the sessions hold it live — starting or running, no stop asked — and never for one stopped, stopping or that no session holds, the session’s own reference the one resolved: a running server’s endpoint, an origin overhttps, the key its provider makes servers with, its lease and its recipe, in the run’s binding (BoundServer), where every call the run makes finds it. Itsserverrequests, uploads and outputs target that endpoint alone for the setting namedserver, every otherserversetting reads as unset inctx.settingsand is refused at the door — a cloud run reaches no local model service — and none follows a redirect; the provider’s key goes along only where that key’s platform file lists the endpoint’s host and path (Project Specification §14.9) — by name, or by a*.entry, which stands for a host of exactly one label before its domain and reaches only an endpoint a provider resolved (mayReceiveCredentials(resolved: true)), never an address a plug-in names, as the hosts SaladCloud makes per deployment are listed — asked of the registry at each request, so a provider turned off since sends nothing more (no-grant). With a saved key the request goes directly to that endpoint; without one,CreditFundingroutes it tohttps://<kFilmOpenDomain>/v1/portal/pass/<platform>/<host><path>with the query intact, the portal headers andX-FilmOpen-Operation. No credit offered isno-key; credit offered without a portal credential isnot-funded, with nothing sent.BoundServercarries the job’s identity in UUID spelling, whether its generator emits 32 hex digits or an already separated UUID, shared by its render, collects and output downloads; a call without a job takes a fresh UUID. The gateway’s encoded upload is bounded at 17 MiB, admitting 16 MiB of media plus 1 MiB of framing; the HTTP response bound remains unchanged.plugin.gateway.routelogs the server, host, road (none,ownKeyorcredit), operation, target and milliseconds; a request proceeding without gateway credentials logsroad: noneand reasonno-gatewayornot-listedat info, while a thrown refusal logs its reason, error type and message at warning, andplugin.gateway.boundlogs the encoded bytes, limit and refusal. Output downloads log their output index, operation and target at start, status, bytes and milliseconds on success, or status, refusal code, operation state, response text and milliseconds before refusing. Cancellation and unexpected failures log their phase, status where known, reason or error type and message, and milliseconds; a stopped gateway is not retried. A server its provider does not own or that is not running, an endpoint that is not an origin overhttpsand a provider that is off are refused before anything is sent (no-server, with its sentence,pluginsServerNotRunning). A platform’s health where a render would run (_statusWhere): the render backend asks the platform plug-in a route runs on — the route’s own plug-in, where it is a platform, else the one it declares a call on (PluginRenderBackend.platformOf) — for itsstatus, bound to the destination as a render is — on the server theoptionscall before it resolved, never resolved twice — a check that spends nothing and waits its turn a bounded while, a failure without words of its own given the app’s (pluginsPlatformNoAnswer). No setting of the person’s changes, and a request to a bound server opens no usage chain of its own: the website’s gateway forwards without metering a call, and the compute session’s record counts its time (§6.7, §6.15).
ctx.machine() reads this computer’s OS, CPU, RAM and GPUs once per session through filmopen_machine, without a declaration. Each local probe is bounded to five seconds; an unread fact is explicitly unknown and its dotted name appears in the answer’s unknown list. Linux uses captured standard sources and nvidia-smi; Windows reads its GPUs alone, with the same nvidia-smi query and parser — found on the PATH where NVIDIA’s driver put it, else not read — and its other facts, like macOS’s, answer unknown with noParser, phones with unsupportedPlatform, and the web answers os.name: web through its stub. Shared GPU memory denotes the same physical pool as RAM. Probe stages, reasons, statuses, a command’s arguments, output, errors and exception message as they are, a file probe’s length and text, elapsed times, the facts read and memoized reuse are logged. The read-only exposed builtin/this-computer command compares every catalogue entry with a local block against these facts: a failed comparison of a stated verified requirement is short; fits requires both stated verified requirements and every required check to pass; everything else is unknown. Missing requirements are explicit unknown checks, and unverified comparisons stay visible. The largest GPU is used, rather than the sum of cards; a shared pool must cover the system and GPU sum once. Recommendations are informational checks. The answer includes the Machine, every local model with its checks and only its stated min_vram_gb, recommended_vram_gb and system_ram_gb memory fields, and one text line per model. A short verdict names its verified failing facts with available and required GiB first, followed by any unverified notes. Entries without a local block are omitted without a conclusion. This comparison promises no installed renderer.
-
A run is of the film it started in (
run_binding.dart, s).RunBindingis made once for every call a person or the app starts —call,runEntityHook,runAction,checkStatus,verifyKey,devRun— and handed down to every call that call makes, a nestedctx.callorctx.ai.completekeeping its caller’s. It carries the film and its key (§5.15), the viewer, the guards, the language and the media store as they were when the run began, and a copy of what this computer held for every plug-in, which is where aserveraddress comes from. Before it, everything a run touched was read from the running app when it was asked: a render begun in one film as one person, with another film opened while it ran, wrote its takes into the other film under the other person’s handle and let the plug-in read that film, and a copy of a film with the same stems and the same text passed the staleness check that was supposed to stop it. -
ctx.project.manifest,get,entities,scriptandresolveanswer the bound film at its newest index while that film is still the open one, and as it was at the start otherwise — never another film’s — and resolve for the bound viewer. A handle typed while a run waits does not reach it:ProjectControllerre-appliesforViewerin place, on the same source, so the film keeps its key and its newest index is the other person’s — their pick of the project file, and so their project-scoped settings — so a door derives the bound viewer’s index from the latest rawbaseIndex, preserving fresh writes without carrying over the other person’s project pick.ctx.settingstherefore reads that film’s project-scoped settings as the person who began the run sees them, beside the bound machine settings;ctx.l10nand a plug-in’s own strings are in the language the run began in.ctxis built after a call has waited for one of the app’s five slots, which is the one window in which a binding and the running app can differ, and a test holds the five slots to prove it. -
The recycle bin (Project Specification §7):
entitiesleaves out an entity whose version in use is deleted,scripta row whose drawn version is (what a deleted folder holds taking its place), andresolveof one refuses withnotFoundand the reasondeleted, so a plug-in tells the bin from an entity that never was;getof an exact file answers it as written. Each logs its start,plugins.door.<door>.start {plugin, args}, and one line by the plug-in’s tag with its outcome andms—no-filmwhere no film is bound,bad-typeorbad-argsfor a request it cannot read, elseplugins.door.entities {outcome: ok, type, stems, skippedDeleted},plugins.door.script {outcome: ok, rows, stems, skippedDeleted},plugins.door.resolve {outcome: found | deleted | none, type, tag, epoch, stem}—, the stems those answered and left out; a hook’s export line carriesskippedDeleted, andassistant.open’sfilmits count of the entities the film index left out. -
A render’s takes land in the place captured when Render was pressed, through the insertion (§6.13, §6.15), with the group of that place, or none for takes alone: no group is chosen by kind.
-
writeVersionswrites into the bound film, through its own source and store, under the bound viewer, and publishes them to the app (HostWrites.noticeWritten: one publication with a listing, nothing read) only when that film is the one open. A folder film no longer open — closed or swapped for another — still takes the transform’s versions and its batch, which are written withcreateand replace nothing, but is not published into the app; where the film changed on disk meanwhile the write is refused rather than made, since every version goes out withcreate: true. Atransform’s staleness is judged against the same film and never across films. A film with no folder that is no longer open is not written to: a session-only film — the bundled sample’s copy, a test’s fixture — has nowhere for a version to land once another project is open, sinceProjectControllerholds the new one and Open sample makes a fresh snapshot, so the write would go into aMemoryProjectSourcenothing holds and be lost without a word. A durable key therefore keeps the rule above, while a session key that is no longer open refuses the write with a sentence of its own, before anything is written, so nothing half-written is left; the call was paid for before that, and it is the write that is refused, not the call. Only a door that changes the film without a tap reaches it today — the agent layer’sopen_project(§4.3) — and a run that outlives its dialog will. -
Closing a film is visible to the host.
ContainerEnvironment.projectreturns null whileprojectClosedProvideris true, its listener triggers a registry reload on close, andrefreshProject— the plug-in installs’ door, the stock install and Install from file — asksProjectController.refresh(library: true), which drops the held library so the next read lists and reads it afresh, and reads no closed film (loggedplugins.install.reread,project.library.dropped). The retained index, with the app’s landed writes laid over it, remains available throughbaseIndex; it does not make the film open. A new film entity hook is refused withnotAvailable. Library actions that require no film remain legal. -
When the plug-ins are read again. Once as the host attaches, then when the library, the viewer or the language changes or the retained film is closed — never on a content save, a pointer, a pick or another film, since the plug-ins are the library’s; a plug-in edited on disk in the development folder is read again when the developer page opens or runs it, or on any switch. Every reading waits for the start’s one stock install — the one that began it and any asked while it runs, in the stage
stockof its trail — so that none publishes a library the install is still writing; an exception that escapes the install — a bundle that does not read, for one, said first asplugins.stock.failedwith the stagebundle— fails every reading of that start, each said asplugins.loadFailedwith the stagestock, until the app starts again, while a file the install could not read or write is said asplugins.stock.failedand passed over, and does not by itself fail the reading. A reading walks each folder once (SourcePluginPackage.allIn): every plug-in’s files are its share of one listing of the library, and of the development folder, never a listing of its own. A file is read as text once in a reading (SourcePluginPackagekeeps what it read as text until the next reading builds new packages): the manifest the folder parses is the text the registry keeps. The consent hash reads every file’s bytes once beside it, from the source, and the assistant reads its catalog from the source too, so that its choice is loading while a new version’s catalog is read. Of two readings the later is published, and the earlier stops once the later has begun — before it reads any plug-in, or between two of them (PluginRegistry.load’ssuperseded) — rather than reading on for nothing; neither waits for the other. A library that does not list fails the reading (plugins.registry.listFailed, at error, thenplugins.loadFailed): what was published before stays published, and an unreadable library is never published as an empty one. A development folder that does not list is passed over instead (plugins.devFolderPassedOver), and the reading goes on with the library’s plug-ins alone. Each reading leaves its trail in the log:plugins.reload.trigger(its cause —attach,library,closedorsettings— and the decision to read),plugins.reload.start(its generation and the last one published),plugins.reload.stageat trace as each stage is entered, before it waits (generation, stage,ms),plugins.registry.listed(origin, generation, packages,ms), thenplugins.reload.done(plug-ins, enabled,ms),plugins.reload.superseded(the later generation, the stage it stopped at,ms) orplugins.loadFailed(the error’s type, the stage,ms); a reading asked before the host has an environment isplugins.reload.skipped(noEnvironment). The trigger and the listing are at debug, the rest at info and the failures at error; the trail names no path, stem or setting. Beside it, each reading adds oneplugins.registry.readat debug — its generation, itsms, and for each plug-in read its origin, stem, how many files were hashed, those that did not read, a refused folder’s reason, the hash, the code files and their bytes, the problems by path, and whether it is trusted, on and waiting for Allow, with itsms. -
ctx.http({ server, path })and a{ server, path }output resolve the address the run began with, so one typed while a render waits does not redirect its download. -
A render bound to no film is legal:
ctx.project.*answers empty, andcallanswers its outputs as the plug-in gave them — not the vetted ones: thehttpsrule, themimecheck and therole: previewfilter live where a render job’s outputs are staged, the only road into a film. -
Being bound is not being allowed. What the person takes back still stops the next effect exactly as before: a plug-in turned off before a call gets its slot, an owner turned off before its server’s output is fetched, a grant removed, consent withdrawn — each is asked of the registry as it is now, after the binding was made.
-
One pipeline, for the app and for the developer page (H1). A platform hook takes the same road whoever asked for it:
choose → prepare → run → hold to the definition → read the state → close the usage, all of it_call‘s, and a call takes nothing in. A render with an entity asked for from the page is a place’s render (§9.4a): the request the app would make, handed to the insertion to takes alone asRenderOrigin.developer(DevSubmission), which the host runs as the page’s working copy — on, asking no consent, its keys granted — only while the page that handed on exactly that request waits for it (DevBench.pipelineLog, by the request’s canonical JSON), its calls in the page’s log and fileddev.render, Stop using the ordinary insertion action. The page installs cancellation before awaiting submission and receives its optional exact accepted id. Stop is sent once to that id, without a provider submission or global interrupt, and logged as it is sent (plugins.devStop {plugin, job, action, reason: cancelled}) and as it ends (plugins.devStopDone {plugin, job, action, outcome: accepted | refused | failed, code, ms}, a refusal or a failure a warning, a failure with its error’s type and text) — a job stopped before it ran ends cancelled with no line of its own. A refused Stop leaves the page waiting for the job’s actual outcome; any other developer’s render — one whose page stopped waiting, one restored after a restart, another of the same plug-in — is refuseddeveloperPagebefore it runs. The request is the one the app would make: refused before anything is handed on where the preparation refuses it, where the plug-in serves the model on none of its platforms, or where no place is offered the model’s kind, its prompt, params and seed as the preparation read them; one render at a time per plug-in (busy). The page differs in four things it is given —among, the plug-ins to choose from (its working copy alone, which is how As FilmOpen calls it asks the app’s own question of one plug-in);log, its own;onSent, told the request once nothing can refuse it, which is what the page shows and writes to its file; andrecordAs, the hook the usage record carries (dev.<function>). The page’s other roads —status, an action, an entity hook, an offer, a platform’s ownrun, and Exactly what I typed — keep their own front, as they do in the app. Before this the page had a second copy of the choice, the serves rule and the preparation, and the two had drifted: it handed_runno ledger, so a render on a person’s key was recordedentry: unknown,kind: text; its record ended on the raw outcome, so a render the plug-in itself called failed was filed as a success; it never held a completion’smessagesto the definition, so an empty conversation ran from the page and was refused in the app; andverifyKey, reached through Other…, ran with every granted key in hand — as FilmOpen calls it, it is nownotFound, since FilmOpen calls it only from a key’s card with the candidate the person typed, while Exactly what I typed still runs it. A platform hook from the page is also given noctx.selectionnow, as the app’s owncallgives it none; the page used to hand it the entity, which a plug-in could read there and never in the app. A writer’sbuildPromptfrom the page is prepared as Generate prepares it, in_capability, the door both share: where the caller names the writer by its id or not at all — the page’s box (target,writer,blockId,covered,model), andcallCapability— the host takes the plug-in’s writer of the target for the model (promptWritersOf, the named id, else the first; none isrefused/noWriter, an id it lacksnotFound, as Generate says them), the model’s catalogue entry, and the media the Prompt card offers on the entity selected in the app (promptTokenCandidates, named in the person’s words) unless the box names its own, loggedplugins.promptPrepared; the box’s JSON is translated to what Generate hands or refused with its reason:writeris an id — an object is refusedbadArgs/writer, since the declaration is always this plug-in’s own —,tokensa list of{token, name}strings (badArgs/tokens), a clip’scovereda list of block ids, each once (badArgs/covered; that each names one block of the scene is the arguments’ own check), each refusal naming where in the box aspath(writer,tokens[0],covered[1]) and logged with itswhy— a writer object refused before anything else the box holds; the page’s example forbuildPromptnames the plug-in’s first writer by its id; then the LLM decided from that writer’s ownllms(decideLlm), so the plug-in is handedwriter— the declaration — andllmas Generate hands them; and the page passesonContext, sorender.prompt.contextis written for it as for the card, and acontextline is on the page and in its file. A model’s call from the page shows what it was asked: anai.completeline asked with the messages and options whole, before the wait, then the answer — or the refusal’s code, reason and the door’s own message — whether or not the code logged them itself. -
Every word a person reads of a plug-in’s call comes from
filmopen_l10n(H4 and H5). A refusal carries a code and its arguments, andpluginRefusalTextsays it:prepareRenderandRenderInputsused to carry English sentences — a seed is a whole number from 0 to …, $model needs a prompt — which only a developer saw while a render could be asked for from nowhere else, and which a render dialog would show everyone whatever the app’s language.RenderInputsrefuses withRenderInputsRefused, a code and the input at fault, whichprepareRenderturns into the failure’smessageand itspath; the input’s name used to be the message itself.ctx.upload’s refusal takes its words the same way: the plan set it aside as text only a plug-in’s code reads, and it is not — a plug-in that does not catch a door’s refusal hands the sentence straight to the person, which a test incontract_test.dartshows.pluginOutcomeText(outcome, done:)is the one place an outcome becomes a sentence: the dialog on a page and a plug-in’s own page each had a copy of thatswitch, and they had drifted — one said a changed file and a missing handle, the other showedstaleas a code. The developer page keeps its own line, which is not a sentence but the code, the path, the message and where in the plug-in’s files it was thrown. -
verifyKeyruns the hook only of the enabled plug-in that owns the key and provides it for the key’s platform — a candidate key the person just typed goes to no other plug-in — with the candidate credential usable throughctx.httpfor that one call and only to read —GETorHEAD, since a key check runs no model; anything else isrefused/keyCheck— and read from nowhere else, its answer held to the definition, so one of another shape is could not check (badResult) and never a key the platform refused; nothing is stored by the host. The check gives up after 20 seconds, and the candidate writes no usage record (§6.7), though any other key the check sends does; it may not ask a model throughctx.ai.complete(keyCheck). -
Entity hooks (
runEntityHook,transform): refused (applies) on a type the manifest does not name, and (library) on a library file, whoever calls, since the run writes beside its sources in the project; each selected entity is taken from the film as decisions read it when the call is admitted to run — once it has its turn and its slot, immediately before its code starts, never before it waits — so nothing is spent on a version already overtaken — one a write of the app’s own replaced since the caller took it, or while the call waited, is run as the view holds it, one gone isstale— loggedplugins.transform.source(the stem;at: admitted; the decisionasked,viewwith both texts, orgone), and its JSON goes in asargs.entitiesandctx.selection; the returned entities are written byProjectWriter.writePluginRun(§6.1a) — refused asstalewhen a selected file changed during the run — checked against the film as decisions read it (ContainerEnvironment.projectisProjectController.view, andbaseIndexthe controller’sbaseViewfor another viewer, §7.5), so a save of the app’s own that landed and is not yet published counts —, with only the project-scoped settings recorded in its batch, since a machine setting stays on the machine; the writer is the controller’s recorded one (HostWrites.writerFor) and the film on screen is told withHostWrites.noticeWritten— one publication with a listing, nothing read — loggedplugins.outputs.writtenwith the stems and the decision; the writer logs each version as written (entity.pluginWritten, its path and document) and the batch (batch.written), and a failed write aswriter.failed(its path, the document as it would have been written, the error’s message,ms; a refusal before the first write names the returned outputs, and the stamped document where the content rules refuse it), thenplugins.writeFailed(the message, the sources, the returned answer,ms); an answer refused before the writer —stalewith the stem and the texts sent and in the view,shape,cancelled,filmClosed,notAvailable— and an answer the writer’s plan refuses (noSource) areplugins.outputs.refusedwith the code, the returned answer andms; an answer the transform’s contract refuses isplugins.badResultwith the plug-in, the sources, the path and problem, the returned answer andms, and nothing is written; every run, whatever its ending — written, stale, refused, failed, rejected —, is logged betweenplugins.transform.start(the tag, the hook, the selection) andplugins.transform.end(the outcome —writtenwith the stems, or the code with its reason, path and message — andms); a version the run could not take back after it iswriter.cleanupFailedwith its path. -
Capabilities (
callCapability,capability_args.dart):describeReference(reference-analysis),describeStyle(style-analysis) andsaveVoice(voice), called by the app on the enabled plug-in a page names — the app does not choose among plug-ins for a capability — which implements the function’s interface, and fordescribeReferencedeclares thekindasked for; anything else isnotFoundorbadArgs(kind) before anything runs. The arguments are made by the host from the page’s and the entity’s: the media named by reference must be one the entity names in a live entry — what the person chose from its place — and of the kind asked for (a picture forimageand a style, a recording foraudio), and goes as a descriptor granted to that call alone (MediaGrants), elsebadArgs(media); the person’s language fillslanguagewhere the page gives none;describeReferenceis of a character, handed as itssubject;describeStyleis of the project file or a folder, whosescope—projectorfolder— must be the entity’s own, elsebadArgs(scope). A voice keep requires a nonblankmodel(badArgs: model). A supplied preview id must be a held, shown preview of the character on the plug-in’s platform. A supplied reference must name a live audio entry invoice_samples, archived or not; it is described and granted to this call alone (badArgs: reference). Neither source isbadArgs: generatedVoiceId. Both are allowed. Name and description retain their existing nonblank checks. The keep’s start logs the caller’s arguments and source document whole; refusal logs the failed argument check, code, values and elapsed time. The call logs the granted arguments and its answer. The call is filed under its function, for that entity, and its chain ends with it, from the answer as it came — as failed where the answer says so, what it spent kept, as a render’s does. The answer is held to the definition as it came, nothing in it renamed; a description that succeeded whosekindis not the one asked isbadResult(kind) with its path — a failure names no kind, and keeps the plug-in’s own words. What the answer says is the page’s to apply through the person’s draft (analysisChanges, §5; Analyze, §9.3). A package of the plug-in lab’s first contract that describes media —x.referenceAnalysisorx.styleAnalysisbeside an offereddescribeReferenceordescribeStyle, implementing neither interface — is no analyser: it is listed with no interface and no hook, and a call of it isnotFound. -
Voices (
voice_calls.dart,render_backend.dart,host_runtime.dart; Project Specification §9.1). A voice designed and a line spoken are renders, the one road for media into a film: a render job of a model of the catalogue’s kindvoice(ttv) ortts, on the platform a plug-in makes voices on, runs itsdesignVoiceor itsspeakrather thanrender— made from the render prepared as any is: a design’s description (the prompt), the words its previews say (the paramtext, asauditionText) and the seed; a line’s words and the voice the request names (the paramvoice), which must be an id of the grammar and never one the character’s samples know only as a preview’s (badArgs,voice) — and its answer, held to thevoiceinterface, read as a render’s: each preview’s output, or the line’s, with the binding its sample keeps (voice_binding: the platform, the preview it is, or the voice that spoke it), carried on each output’s provenance to the insertion (§6.13). Whether the seed went to the service is the answer’s own seed-applied flag in itsmetadatawhere that is a boolean, else the function’s default — a design’s yes, a line’s no — and the take’s record follows it (voice.seedlogs the function, the flag and whether it came from the answer or the default). Such a plug-in is offered for those two kinds of model on its platform whether or not it implementsrender, except where a row of the model on that platform is markedrender: the platform’s route is then a renderer’s, so the plug-in is no candidate there unless it renders that row itself (render.candidates’decisionsname itvoiceOnRenderRow). A plug-in that implements bothrenderandvoiceon one platform renders a row markedrenderthere throughrender; only an unmarked row is spoken through itsvoicefunction, and only such a route speaks in the character’s kept voice. A candidate that speaks a line says so (RenderCandidate.speaksInVoice, §6.15). Speech carries the requested film’slangat the top level only while that film is open and has one; another open film supplies no language. Design has no language source and receives none. Only an access row on the selected platform whoseparamsdeclaresreferencerequires a sample: the livevoice_samplesentry bound to that platform and exact voice id, archived or not. The host describes and grants that entry, withreferenceTextfrom its own knownaudition_text; it never substitutes the global active sample or the character’s legacy audition text. A missing or invalid bound sample isbadArgs: referencebefore anything runs. Such a request also obeys the other-film media guard before any sample is described or granted. A row without the declaration receives no reference fields.voice.languagelogs the value, open and requested film keys, decision reason, elapsed time and its source (project,noneorotherFilm);voice.referencelogs the outcome (sent,none,notDeclared), model, platform, voice id, descriptor, words, source document, decision reason and elapsed time. A voice is kept once per film, plug-in, selected model and stable source (saveVoice, through the capabilities’ door). The source is the supplied preview id, else the reference as the caller gave it, before the call grants it. An acknowledged save in this session is answered again without another call, so that a failed local write can be written again without another charge or slot.plugins.voiceKeptAlreadylogs the key’s parts, the acknowledged voice and elapsed time;plugins.voiceKeep.acknowledgedlogs the answer retained. The usage chain names the selected model and prices a keep only from its platform’sper: "clone"rows, one clone per request; without such a row its estimate and catalogue cost are unknown. The pricingrender.callline names its stage, model, unit and entry. A design that answers no preview isbadResult, never a render done with nothing in it. Thevoiceinterface is the one voice road: nothing translates the plug-in lab’s names.
The developer page’s side lives in dev_run.dart (DevBench), apart from host_runtime.dart: devOpen, devRun, devClose, the request for the page’s own roads, the run’s own log lines and last-run.json. The runtime keeps the front — the development folder, its preference and its problems — and the three seam methods are one-line forwards. It is built from closures, not from a reference to the runtime: plugin_call_runner.dart imports dev_run.dart for DevLog, so the bench may not import the runner back, and what it needs — the registry, the views, the contract, the language, the data files, a reload, a binding, a run, a call and the three that hold an answer to a shape — is handed in. For the same reason the door it watches arrives as a closure that answers, not as the means of asking. The developer page’s side (dev_run.dart, host_runtime.dart’s four dev… methods). devOpen(stem) reads the plug-in’s files from disk again and answers what it exposes, in the definition’s terms: status first, then the functions of the interfaces it implements, its actions and the functions it offers, each with the definition’s example for a function of the contract and the manifest’s own for an action or an offer — no shape kept by hand — and verifyKey never among them, since its candidate key is typed on the plug-in’s page — with every problem by the file it concerns and where its last run and the developer’s log are written. devRun(stem, function, args, mode:) reads them again and runs the function enabled, asking no consent, with every key its uses names granted (the page is for people who write code, and nothing on it is withheld); the sandbox, every door’s rules, the limits and the usage records are exactly the app’s, or what passes here might fail in the app. The registry, the preferences and the stored consent are untouched: outside the page the plug-in asks exactly as before. The run’s hook in the usage records is dev.<function>. It calls the function one of two ways, or runs a script in the console (PluginDevMode): As FilmOpen calls it, the default, with the request the app would make — status with ctx alone; an action with ctx alone, as its button presses it; a render prepared by prepareRender (its entry, its inputs described and held to the entry, the seed, the source) and a complete or an estimate as the app builds them, an estimate being the function of the interface its operation names, and a request the app could not make refused before the plug-in runs — a model the catalogue lacks (noProvider/noEntry), one this plug-in does not serve because the entry names no platform its manifest implements the interface on or its manifest names other models (noProvider/noPlugin, since the app’s choice would never bring it here), a completion’s params that are no object (badArgs/params); an entity hook with the entities chosen; an offer, or a function of the contract an action’s button also calls, as the contract calls it — and Exactly what I typed, the box as it is, for the protocol and a function’s edges. The console (console) calls no function of the plug-in’s: devRun names the call $console and runs the plug-in with one more file after its own (DevRun.withConsole), console.js, whose value is a plug-in of that one function, its body the box on the file’s first line, called with ctx alone and the page’s entity as ctx.selection, its answer held to nothing; plugins.devConsole logs the script, the entity and the files as it starts, and plugins.devCalled the script and the value whole as it ends. The mode changes what is sent, never the guards: status runs as Check runs it either way, so it may not reach ctx.ai.complete and spend. The request as sent is the run’s first log line (request) — after a writer’s context line, which its preparation writes first — so the page shows it before the answer comes, and the run carries it (PluginDevRun.request, with its mode). An answer of a function of the contract is held to the definition in both function-calling modes, one of the wrong shape being badResult with its path, and an offer’s own args and returns hold a call to it — its arguments as a caller would send them, before it runs, as FilmOpen calls it, and its answer either way. A render as FilmOpen calls it is read by its state and, where an entity was named, saved as a take exactly as the app would, its batch’s item the request as prepared; as typed, nothing is saved, since the app’s record would name a request nobody prepared; with no entity named, the outputs are only listed. last-run.json holds the mode and, as args, the request as sent — null where it was refused and nothing was sent. devClose(stem) forgets last-run.json.
The log of such a run. The doors closure is wrapped for the page and for it alone: the request as sent first (request), then every ctx.log line and a row per door, in order, with what the door was asked for — the setting or the key by name, the address or a server request’s route whole, as the plug-in gave it, the answer in summary and the milliseconds — and what crossed into JavaScript and nothing else, everything in it as it is — the arguments, a line’s words and data, an answer — and no key of the request, since the door adds it after the request is logged; an answer that echoes it crossed as it came. A line’s data and its message are kept whole. Each line goes to the page as it happens and to plugins-dev.jsonl beside the app’s log — there whole, its message and its data as they are, the call’s outcome — the answer and an action’s words as shown, or the failure’s code, reason, path and message, and whether a request was sent — its last line, written before last-run.json, and a call refused before anything went its only line; no request’s key reaches it, since a request is logged as the plug-in built it, before the door adds the key; a line the file cannot take is said in the app’s log (plugins.devLogFailed {stage, path, type, error}) — rotated at 5 MB to plugins-dev.jsonl.1: a developer’s own file to tail, which no issue report ever carries and which a person’s ordinary use never writes to. After every call last-run.json is written under <app support>/plugins/<tag>/dev/ — the function, the mode, the request as sent, the outcome with a failure’s place, and the lines, under a megabyte — results as data, for an agent; one that would pass a megabyte drops its lines, then its request and answer, and points at the developer log, which holds them, and a file that cannot be written is said in the app’s log (plugins.devLastRunFailed). The log events are plugins.devCalled, with the stem, the function, the mode, the outcome (done or failed), whether a request was sent and, for a failure, its code, reason, path and message, and plugins.devFolderSet.
The zip (plugin_archive.dart). PluginArchive.read(bytes, expectedTag:) reads a plug-in’s zip into memory and refuses it whole — nothing kept — when it does not read as a zip or holds nothing, claims more than 64 MB unpacked or 2,000 files, has an entry that would land outside the folder (absolute, a drive letter, a backslash, an empty, . or .. segment) or a link (by its target or its Unix mode), or has no manifest at the folder’s top, manifests of two tags, a folder not named after the tag, or a tag other than the one expected. Each entry is inflated by the archive package’s pure-Dart inflater into a buffer bounded by what remains of the 64 MB — never through entry.content, which on dart:io hands the whole entry to zlib and writes it out only once it is complete — so an entry whose directory claims a few bytes and inflates to gigabytes stops at the limit; an encrypted entry, one compressed other than stored or deflated, and one whose bytes do not match its CRC-32 — a download cut or damaged on its way — are unreadable. What an operating system adds as it zips a folder — macOS’s __MACOSX/ and .DS_Store, any hidden name — is left out, as a folder source skips hidden entries. The folder’s contents may sit at the archive’s top or inside one folder named after the tag.
Lending (lending_supplier.dart; the state’s Lending seam, the account’s AccountLending, §6.6). What this computer shares is a list of offers, one per fixed configuration, kept beside the enabled list (PluginPreferences.sharedOffers, one JSON list of SharedOffer: id — a UUID made once and kept —, tag, function, model, server — null for this computer, else {provider, offer}: the server provider’s tag and the GPU offer its servers are started from, never one session —, stack, params — the options the person fixed on the stack —, name, usd — the price of one unit in whole cents, 0 free —, enabled), read and written whole through PluginSettingsControl.sharedOffers / setSharedOffers (one writer at a time, an entry not of the shape or a repeated id refused at the write before anything is written, an entry not of the shape left out at the read, the listeners told). The person writes the list from Share my plugins’ offer tree (§9.7a), its one writer: a new offer a fresh UUID, an edit keeping its id, Enable changing that field alone, and no Remove — a disabled offer stays listed and the loop withdraws it; a sharing plug-in’s own page says so in one line and has no switch. LendingSupplier starts once the render journal is read and runs while the person is signed in on a registered device. At start it stops every remote render the last session left. Each turn (every 15 s, and on a plug-in change) it reads the list and the server sessions and builds each offer (lendingOffers): its declaration from the installed plug-in and the catalogue entry alone, so an unchanged entry is the same bytes at every turn — pluginId, pluginVersion, configuration (function, model, unit and scale from the manifest’s sharing operation, stack, params, name), inputs {values: <the entry's capabilities.inputs>}, outputs {kind}, rates [{unit, usd}] — and whether it is publishable: the entry enabled, its plug-in enabled and runnable, and its server null or a session of that provider and offer that is running and holds the offer’s stack (the session’s stack the offer’s, or none named — a server from before stacks; holdsStack), so that a running Fast server neither publishes nor runs a Quality offer, which is held server-not-running with nothing deployed or chosen in its place; the decision is logged lending.offer.server {offer, server, stack, running: [{name, stack?}], session?, outcome: serves | no-server-running | no-server-holds-stack} once each time it changes (lendingMatch). An entry whose plug-in is not installed, or whose manifest no longer shares a render of that model, is no offer. The loop keeps, in memory and never in the list, what the website holds of each offer — the declaration, the version it answered and the publication last sent — and writes one thing at a time: a declaration where the offer is new to the session or its declaration changed (the website hashes it: an equal declaration answers the same version, a changed price or option another), then the publication — published where publishable, withdrawn otherwise — where the desired state is not the one last sent for that version, so the first turn of a session sends every offer’s state once and later turns only a change; an offer that left the list, or lost its plug-in, is withdrawn where it was published and forgotten; a write the website refuses is logged (lending.offerRefused) and leaves the other offers their turn. While any offer is published the device is available and the inbox is read every turn, which keeps its presence; with none it is unavailable, the job in hand stopped and failed. A lending notice wakes it: pokeLendingSupplier(), the factory library’s one door to the running loop, runs a turn at once — the turn already running where there is one — and answers whether a loop runs, logged plugins.lending.poke {running}; the relay calls it when the website says a job was made for this device’s offer (§6.6c), and the turns on their own clock stay. It takes one job at a time. A job’s input — opaque JSON, handed over as the buyer sent it — must be the object {prompt, seed?} and nothing else (an array, a scalar, another key or a seed out of bounds is refused, lending.input naming why in one word) — the options are the offer’s, never the buyer’s —, its frozen declaration this plug-in at the installed version and of the offer’s model, and the offer must have a route now (lendingRoute: for this computer the plug-in’s candidate with no destination, for a server the candidate of the first running session holding the job’s frozen stack, that stack set; logged lending.route {job, offer, server?, stack?, destination?, outcome: routed | no-route, ms} and, for a server, lending.route.server with the running servers’ stacks, the one chosen and whether the plug-in has a candidate on it), or it is refused before acceptance (bad-input, not-this-offer); a requested job of an offer not published this turn is refused not-this-offer as well; busy at acceptance leaves it. An accepted job is rendered through the application’s render road as a remote request on its frozen declaration — the stack and the options frozen with it, never the list’s current entry — on the offer’s current server (the server is private and not in the declaration); at outputReady each output’s bytes are read, uploaded through the media client (MediaClient, §6.6) and hashed, and the job is completed with one complete whose opaque result is {model, seed?, stack?, width?, height?, outputs: [{url, contentType, bytes, sha256}]} — the model and what the render reports, then each output’s download URL, length and SHA-256 in order —, the rail’s balance read again once the website took it (ProviderFunding.balanceMoved('lending'), logged lending.balanceMoved with the job, the role and the milliseconds), then the render acknowledged; an upload the media store refuses, or a result over the 64 KiB bound, fails the job render-failed, its code in the log. A job stopped while its outputs upload — the offer off, its server stopped, quit — is failed rather than completed; signed out, nothing more is sent. A render that ends otherwise fails the job render-failed, its own code in the log alone. An accepted job that is not the one in hand — a restart’s, or one a sign-out or an unanswered failure left — is failed supplier-restarted on the next turn and never run again. A refusal by which the website closed the job ends its handling with no retry; any other refusal after acceptance fails it once. Disabling the offer, turning its plug-in off, stopping its server and removing it stop the render in flight, fail the accepted job and withdraw the offer; quit stops and fails likewise and leaves the offers as the website holds them (a stale presence makes them unavailable there; the next session sends their state again); a sign-out sends nothing — the render is stopped, and the website’s presence and deadline close the rest. Each step of a job logs its lending.* line with the job’s id and the milliseconds since that handling began — lending.input (with the reason of a refusal), lending.refusedJob, lending.accepted, lending.render.started, lending.render.ended, lending.result.uploaded (the media key, never the URL, with the type, the bytes and the digest), lending.result.sent, lending.completed, lending.refused and lending.failed with their stage and an unexpected failure’s message, before acceptance as after. An offer that is not publishable says why in lending.offer.held (offer, reason: offer-disabled, plugin-disabled, plugin-not-runnable or server-not-running, the first that holds), as does an entry of the list that is not an offer at all (plugin, reason: plugin-not-installed, or not-shared where its manifest shares no render of the model), once each time its reason changes, never every turn, and before the declaration, so a refused one still says it; lending.withdrawn carries the reason too (not-an-offer for one that left this turn’s offers). What the website acknowledged is Lending.published, the ids of the offers whose publication the website accepted and that are publishable: the loop sets it each turn, before a turn with none published returns, only when it changed, and empties it at sign-out and once a stop’s turn has ended; the offer tree and Stop read it, and nothing else writes it. lendingRoute and runningSession live in filmopen_render, so that the editor asks the options of the route a job would run on.
The audience of what this computer shares is the account’s, not this computer’s: who may use the published offers — the person’s own devices always, the persons and groups of the free list, the public where chosen — is GET / PUT /v1/lending/settings (Lending.audience / setAudience, §6.6), written from the GPU page’s Share my plugins (§9.7a); the loop reads nothing of it, since the website admits a job by the setting it holds. The free list carries ids, and the website says no name for them yet, so this computer keeps the names it resolved (PluginPreferences.freeWithNames, one JSON object of id → username, read and written whole through PluginSettingsControl.freeWithNames / setFreeWithNames, one writer at a time, a value not of that shape read as nothing, the write logged by its count alone and no listener told); the page trims it to the account’s list at each answer. A group the account holds is named from the Groups card’s read instead, which needs no store. It goes when the website answers the names itself.
Rendering on a lent plug-in (lent_render.dart; the requester’s three operations of Lending, §6.6). Another computer’s offer is one more route of the render dialog where this computer has the same plug-in installed and enabled, at any version (pluginId its tag; the supplier refuses a version it no longer runs, so an update on one computer loses the other nothing) and its manifest’s sharing names a render of the dialog’s model: only then are the offers read, bounded at 3 s, a refusal or a late read leaving the local routes as they are. Each decision is a line, ids, tags, codes and counts alone: lending.routes.start (model, sharing: how many plug-ins share it); lending.routes.none where none does (reason: no-sharing-plugin, installed: each plug-in’s <tag>:<enabled>, ms); lending.offers.read (count, ms) or lending.noOffers; for every offer not taken lending.offer.skipped (offer, own, reason: the first that holds of unpublished, unavailable, function, model, plugin-not-installed — no installed, enabled plug-in sharing the model has its tag — and unpriced); and lending.routes.done (routes, ms). Each published, available offer of the model becomes a candidate after this computer’s own routes: the installed plug-in’s route, a destination of kind lent whose id is the offer’s and whose server holds its version, the price and the quantities it was priced at and whether it is the same account’s (own), billing unless the offer is free, queued per offer. The price is the design’s: one of the configuration’s unit and none of any other rate’s unit, every rate times its quantity summed exactly and ceiled once to the cent, zero where the offer is free. options answers a lent route itself — that price, no stacks, no parameters — and the dialog names the route Your other computer or Shared on {domain}, then · and the offer’s name (its configuration.name, the candidate’s label; nothing where it has none), the count the backend adds to two alike carrying its own separator. A render on it prepares one job whose id is the render’s own, journaled before the call, so a restart reads the same job and no second id is ever made: the offer, its version, the quantities, the input {prompt, seed?} — the options are the offer’s, frozen in its declaration, so the request’s never travel — and the price the dialog showed; a preparation left unconfirmed or unanswered is read, never sent again, and any other refusal fails the render with its code (terms-conflict: the price changed; insufficient-funds: not enough credit). Collecting reads the job every 3 s: a published result’s outputs are fetched whatever the money’s state, each from its URL — refused unfetched unless it is https://<kFilmOpenDomain>/v1/media/…, since another person’s app wrote it —, through a client made for the collect and closed at its end, with no redirect followed, no credential and a 30 s bound, the body held under 32 MiB whatever its length header says; each output’s length and SHA-256 are held to the result’s entry and its type to the model’s kind (mediaNotWhatItSays otherwise), and it is staged as a take of the installed plug-in’s run (run its tag and stem, item what was asked, the result’s seed), which the insertion takes in unchanged; once the handling of a published result is settled — staged, or a confirmed failure; never a fetch to be read again or an unconfirmed failure that Retry collects again — the rail’s balance is read again (balanceMoved('lending'), logged lending.balanceMoved with the job, the role, the state, the outcome and the milliseconds, and a handling not yet settled lending.balanceDeferred with its outcome and the reason collected-again); a closed job fails with its code, a result gone expired — as is an output the media store answers 403 or 404, its token or its object gone —, a job never prepared not-found; an output nothing answered, or not in time, and an unreachable job are read again at the next collect; any other refusal of the job proves nothing and is an unconfirmed failure, which the person may collect again, as is an output answered with a status other than 200, 403, 404 or a redirect (mediaUnavailable; a redirect is refused mediaNotWhatItSays, never followed) or that could not be staged (staging). Each read logs lending.collect, with a closed job’s closeReason and failCode, and each output lending.result.fetch (its address whole, its type, size and digest), lending.result.read and, where refused or failed, lending.result.refused with its reason and the value refused or lending.result.failed with its stage, outcome and the error’s message, whole. Cancel sends cancel: closed or never made, it stopped; published already, it finished and is collected. The take names the job (render_id), not the offer or its version.
Streamed HTTP answers. A request with stream: true receives a successful text/event-stream head as {status, headers, stream: true} before the body completes. Other content types and non-2xx answers retain whole-body handling. The call’s frame owns one stream and closes its connection at EOF, failure, cancellation or frame end. ctx.http.read() waits for a complete event or EOF and answers {events: [{event, data}], done} with all events already framed. A missing stream, concurrent read or second streamed request is badArgs with reason stream. Framing decodes UTF-8 incrementally, tolerates malformed bytes, recognizes CR, LF and CRLF, joins data lines with newlines, defaults the name to message, and drops comments, ids, retries and unfinished events. Each event’s data is handed over as it came. Raw bytes count against the response bound over the entire stream; timeoutMs bounds each chunk’s wait. A broken body is refused with reason connection; timeout and oversize are refused with reasons timeout and tooLarge, and abortion is cancelled. A streamed head’s headers and each read’s events are told to the usage ledger as a whole answer’s are (What a call spent).
Partial text delivery. The host passes stream only to a text provider’s complete; ctx.ai.complete passes none. ctx.partial(text) is awaited and accepts a non-empty string only inside a live complete whose own pricing request says stream: true. It appends in order to PluginCallControl.partial and never clears it; the control’s owner, an assistant’s turn, keeps it for the whole turn as its display (§6.11a, The reply that streams). The frame counts cumulative UTF-8 bytes for its log, with no bound. Wrong text is badArgs with reason text and the wrong phase is refused/phase. Under an assistant binding the turn’s check runs first, so Stop or a changed target refuses cancelled or stale. An accepted delivery writes no line; the stream writes one when its frame ends, plugins.partials.done with the count, the bytes and the elapsed time, in the app’s log and as one partial row on a developer page and in plugins-dev.jsonl. A refusal logs plugins.partial.refused with its code, its reason and the text it was given.
Passing notes. ctx.note(text) is the running streamed call’s one current note — a passing word of what the model is doing, such as its reasoning’s latest sentence. It is admitted as partial is — a non-empty string inside a live streamed complete, badArgs/text, refused/phase, under an assistant binding the turn’s check first — whatever its size. Only reasoning is the note: a reasoning note replaces PluginCallControl.note and marks its call as having said something (PluginCallControl.heardCall); a tool’s announcement — <alias> · <n>, its alias named by its command where the turn’s own table has it (c5_create-entity · 3 reads create-entity · 3), and an announcement of a tool the table lacks too, that tool being refused bad-call as it runs — and a provider’s bare Thinking… leave the note as it stood and mark nothing. A note accumulates nothing and never enters the call’s text, a message, the conversation or what the model is told. It outlives its call: it is cleared when a call’s first words arrive (ctx.partial, cause words, the words marking the call heard too) and on Stop (stop), at once wherever the turn is, a tool running or a program’s handover after the answer included, and otherwise stands until the next reasoning replaces it or the turn ends, the session’s disposal stopping its turn. The host numbers the turn’s model calls on the control (PluginCallControl.call) and stamps each call’s start, PluginCallControl.callStartedAt, by AssistantSession.clock (assistant.call.start {call, at, heard, note, ms}); AssistantSession.note, call, callStartedAt and heardCall are the seam the dock reads, empty, 0 and null while idle. Every delivery, refusal and clear is one plugins.note line — plugin, the frame where a delivery has one (a clear has none), session, call where a control numbers it, the text as received and, on a delivery, as shown and its kind (reasoning, tool, thinking), outcome (shown, notShown for a tool’s or a bare Thinking…, unseen without a control, refused with its code and reason, a failed delivery with its error, cleared with its cause) and ms — and every delivery one assistant.note {session, kind, text, call, kept}, kept being the note that stands after it. Not guarded: a note’s frequency — no quota and no timer. The stock OpenRouter adapter sends the reasoning’s visible summary or text, one whole sentence at a time, a sentence that ends as the reasoning pauses (an event without reasoning, or the stream’s end) included; encrypted reasoning shows nothing and stays in its event log. While a tool call’s arguments stream, it sends the call’s tool and its number in the answer (create-entity · 3) once, as the tool’s name arrives — data, not a sentence, so nothing to translate; the OpenAI adapter asks a streamed call for its reasoning summary (except at effort none, which has none) and sends it the same way, and the xAI adapter sends Grok’s streamed reasoning_content the same way. On the Codex program the OpenAI adapter also sends a reasoning summary still open when its reasoning item ends — a bare title — once, and each progress message (an agent message of phase commentary) whole as a note, never as the answer’s words; a shell command Codex runs is logged, not sent.
Streamed assistant requests. Streaming uses the ordinary HTTP transport’s stream flag and frame-bound reader. The assistant policy holds the model, route, output bound, request size and one POST per admitted call; the provider owns its wire-body grammar.
The stream logs plugins.http.stream.open or .whole, each batch’s .read and .read.done — the latter with the batch’s events as they came, under answer —, .first at the first event, and .done or .failed with read/event/byte counts, first-event time, elapsed time and, on failure, the reason, pending-read state, time since the last read completed and, under unread, the events framed since the last read, which the plug-in is never handed, as they came. Where the response ended first and a Stop or the call’s end comes before the plug-in’s last read, .unread carries the counts, the reason and, under unread, the events not yet read, once; a read already waiting takes them itself. .refused identifies stream misuse. An error or response.failed event, or a JSON object with an error member, emits .error with the extracted code, the whole message and the event’s data as it came; the event still reaches the plug-in. .whole carries the content type as it came.
The dock’s readiness row follows the selected action’s current route. Before a selection it uses the first runnable action with a known funding route, else the first action needing a key. FilmOpen credit is ordinary information; another offered action’s missing key does not replace that notice.
The LLM decision. One machine setting, Use the LLM: in Settings → General, overrides every plug-in’s preferences. With no override, decideLlm takes the first reachable preferred id, else the first reachable text catalogue entry in load order. With an override it considers that model alone. Reachability uses metadata only: every enabled provider serving that entry is considered, including generic providers; a configured local server, a stored-key status or a true cached credit answer makes its route reachable. No credential is read and no provider is probed. Among the chosen model’s reachable providers, the person’s text-provider preference wins, else a stock provider, else the first by tag; a tie never advances to another model. The same rule holds for catalogue fallback. Other callers of chooseProvider keep their interface’s ranking. Each decision logs a plugins.llm.decided line: its inputs, provider preference, ordered tried with each route’s provider, platform, server, key status, funding and reason, chosen route, why, ranking, tie and elapsed milliseconds. Inventory decisions write no call-decision lines; assistant.model and assistant.models retain their reasons and trials on change; assistant.models lists every text entry, one no enabled provider serves with reason: noRoute, and on each the text providers that declined it with their reason (untrusted, incompatible or disabled for a package not on, noPlatform, noRow — no access row on the provider’s platform — or notNamed — not among its implements.text.models). Each action’s decision is made once, on the first offers after the service’s changes — the host’s plug-ins, keys, credit, settings or the person’s model, or a catalog — and reused by every rebuild of the dock until the next (assistant.decisions.cleared {decisions, reason: host|catalog}).
An ordinary call chain without a supplied model captures its default on its root frame at its first completion without preferences; later such completions share it, including a refusal. A step supplies that default before the call. A nonempty options.llms list of strings decides that completion alone, after an assistant’s frozen route, and never replaces the root’s default; an empty list counts as absent, and a present value of any other shape is refused as badArgs with reason llms. Returned maps carry the actual route’s model and provider, replacing those fields from the provider; other values are unchanged. The completion start logs the requested llms and route (assistant, call or frame), its end the answer as returned, and its failure the requested llms. An ordinary call with no supplied decision waits for its first completion to decide. A prompt writer receives the up-front reachable model as buildPrompt.args.llm: {id, entry}, alongside the separate media model and its entry; absent where no LLM is reachable. ctx.ai.complete uses the decision selected for that completion for its messages and logs the decided model, reason, trials, messages, answer, outcome and elapsed time. It never retries a failed call through another provider. Media defaults belong to the image, video and voice render picker.
6.11a The field-assistant adapter
Section titled “6.11a The field-assistant adapter”The host’s assistant service (assistant_host.dart) is the catalog road’s (above), installed by the composition wherever the plug-in host runs (installAssistantService, §4); on the web the service is unavailable. It lists the packages that declare a catalog, offers their actions where the person is, and opens an action’s chat on the person’s gesture (openAction): the package and the action admitted, the film captured, a field action’s draft admitted for reading, the model resolved and frozen, then the context object bounded whole; each refusal is logged as assistant.open with its code and stage. The assistant interface requires a catalog and exposes no functions. The target is an entity and a field — AssistantTarget.of(stem, field) keeps the source version’s stem and parses its exchange id, the field identifier, the entity type and the leaf path derived from them; a voice conversation’s session carries what Apply will do (AssistantApply), from the pick at the source’s epoch and never from the version on screen; a text chat’s carries none, since it writes through the commands (§7.3).
The dock runs every action (§9.3): its chip, or the AI icon on the form, which presses the chip. The person’s place (AssistantWhere: the rail, the page’s section and entity, the tab, a field the form published) picks the actions offered — the overview of a project with a version in use being that version’s page (section pj, its entity), as the browser draws it, the overview itself only where none is in use or a project file is to be chosen, and a set record’s page naming its type (record: the dock’s header reads Chat on a chat’s record, Render batch on a batch’s), a form’s field held only while it is of the version the page’s tabs reported (shown) — a project change clears the field and keeps that version, since tabs that survive the change still show it and do not report it again, and their disposal clears it —, each change logged assistant.where {rail, section, type, tab, shown, projectKey, entity, field, landing?, landingStem?, hasProject, hasField, applyable, cause, ms} —, the field’s first, then its form section’s — the catalog’s formSections, offered on every field of that form section, with no finalize, since only a field’s action fills a value; the lookup is actionsFor: field, form section, section, rail, always, the form section being the held field’s —, then the section’s, the rail’s and the always-offered (the catalog’s format is the plug-ins’ repository’s docs/1-20/1-20-catalog-format.md, except that the file has no size bound, an export line’s path list no upper count and a models list, an action’s or the catalog’s, no upper count); a chip opens that action’s chat beside the chats already open, and none of them ends by a move, another chip or another assistant chosen: the dock holds chats (AssistantDockController), by the host’s session id, one shown at a time — the one opened last, of overlapping opens the one asked by the last gesture, the others held; a form section’s chat is a page chat, as the section’s and the rail’s are. An open answered once its film has been left, while its film is being left (the ledgers’ closeAll under way, AssistantSessionLedgers.leaving) or while another film loads in its place (the project loading, the old film’s key still published until it lands), is released, never installed (assistant.dock {session.open, outcome: film, session, leaving?, opening, ms}), so that no chat outlives its film; a move while an open is under way keeps it. A switcher in the portrait row (assistant.chats, an item assistant.chats.<session> per chat, by its chip’s label, the entity it opened at — its name then, else its id — and the place it opened at) shows any held chat, the only way back to a chat that has written nothing; a chat’s record opened in the tree’s Chats (§8.2), by a link or by the move below shows its chat where it is held here, and none where it is not — an archived record has no chat to continue (assistant.dock {chat.row, stem, session, held}, then {chat.current, session, previous, cause: opened|switcher|row|closed|closing|dock, asked?, action, entity, held, ms}). A chat keeps its own request — its action, its place and, for a field’s action, its field — wherever the person goes, and writes through the commands, never the page’s form. The move at the first change: once a chat’s first content item is recorded (a pick alone is none; §7.4, onFirstItem), the app opens its record’s page, on the Project rail, only for the chat shown and only where the browser’s selection and the rail are still those of the gesture that opened it — a person’s move since wins, and a move not made is dropped, never kept for later (assistant.dock {session.moved, session, stem, moved, reason: first change|not shown|rail|person moved|not held, from, gesture, rail, gestureRail, current, ms}). Nothing else moves the page by itself: a write of the page’s own entity is said with Open, as one of another’s is (§9.3). A text chat’s own page is its project, rail, section and entity, whatever field is held or version shown (AssistantRequest.stillOnPage); its voice conversation is offered only there, its button disabled with the reason elsewhere (assistant.dock {conversation.offer, here, session, reason, rail, section, entity, requestRail, requestSection, requestEntity}), and keeps its field as well (stillAt). A held chat is its frozen context, its words and the host’s kept exchanges, in memory: no count of chats is bounded, and none survives the app’s end. A chat ends by its X, or as its session’s close begins, whoever began it — a Discard on its record, a film left, the quit taking the chat with the app —, with no move of the screen and whatever the page reads: the ledgers tell the one dock (AssistantSessionLedgers.onCloseBegun), once that close is kept and never for a request that joins one, and a close the dock began itself is not noticed again (assistant.dock {session.closing, reason: its session's close has begun, session, cause: close begun, current}, then assistant.dock {session.ended, reason: closing, session, current, held, …}, a held chat’s as the shown one’s; the dock’s own going ends every chat it holds, reason: dock; the first cause stays in the ledger’s session.close.start, and the dock’s close joins it as session.close.again). An Apply under way is answered first. A write the chat hears after the close has ended is session.late, not recorded. A chat’s first turn is under way as it opens, but for an action of id ask (the stock assistant’s Ask, whichever assistant’s), which only answers a question: it opens ready, its box open, with no turn sent (assistant.turn.notStarted {session, action, reason: waits for the person's question}), and its first model call is the person’s first message, the film’s context with it. An action that asks (a catalog action’s optional question, Write the Synopsis‘s Tell me your film in a few sentences.) opened on a project with no summary is given that question by the host at its opening (AssistantSession.asks; assistant.open logs asks: none|hasSummary|noSummary and the summary it read, draft, chat, file, in-use or none). The summary is taken once, a logline or a synopsis: from the focused project’s open form where its draft holds one, so a synopsis just typed counts; else from the open chat’s latest version, an AI turn’s changes being the chat’s until they are picked, cherry-picked or discarded; else from the file in use (on any page but the project’s, the project’s version in use); else there is none and the dock asks. The one source decides both whether the dock asks and what the model reads: an open form that holds no summary does not ask over a chat’s version or a file that holds one, and the film the model reads is that version with the draft’s filled fields over it, so a form on an older version never shows the model an empty project to fill (assistant.open.summary {tag, action, source: draft|chat|inUse|none, reason, stem, draftStem, logline, synopsis}), and the dock shows it as the assistant’s first words with no model call (assistant.turn.asked {session, action, reason: noSummary, asks}, assistant.session.asked); the person’s answer is the first turn, the question before it in the messages the model reads. A project with a summary opens as any other action. Opened on the chat’s road (assistant.open.start {…, road: chat|conversation}), a chat no longer depends on the field’s form after its opening: the host’s validity check reads the project, the viewer, the package and the text route, not the field’s draft or its source’s binding, which the conversation keeps to its end. An action uses the shared LLM decision: Settings wins; without it the action’s models, else its catalog’s models, are the ordered preferences before catalogue fallback. Assistant admission then requires a complete endpoint/model row and a verified output bound. A cloud route uses one allowed key slot funded by its stored key or cached FilmOpen credit; a local route uses its named configured server with a null key. Its one bounded POST binds that server, path and service model, and a changed server setting makes the session stale. Funding questions retain their per-platform cache and changed-answer notifications; an opening waits for questions under way, logged in assistant.open.funding, before resolving again. assistant.route, assistant.model and assistant.open carry the decision’s why and ordered tried, the model/provider/platform and funding; the opening also records its stages’ milliseconds. assistant.funding.start, .funding, .funding.changed and .funding.failed record the funding questions, values, outcomes, reasons and errors. A missing-key refusal retains diagnostic provider metadata for the dock’s Add a key action, logged in assistant.model’s repair field, without admitting an unfunded call. The first user message is one fenced object, {film, focus?, activity?, fields?} (a context block), and nothing where the object has no member. fields gives, by type (ch, lo, of, pr, in an opening whose provider takes no tools; where the conversation takes tools — the text provider’s for a typed chat, the voice road’s own for a spoken one, OpenAI’s and not Grok’s (NativeVoiceSession.offersTools), whatever the text provider declares — the opening carries no fields and its preamble says the fields of every type and their shapes are in update-entity‘s description, which lists them once, contentGuide, §7.3), every field of that type’s form with its shape — fieldShapes in filmopen_format (§5), one path: shape line each (appearance.hair.color: text, appearance.marks: list of strings, kind: one of …, default_outfit: the tag create-entity answered for the outfit (<character>.<owner>-<n>); a language field, voice.lang and any other lang, a BCP 47 language code, e.g. en, pt-BR) and one nested-JSON example of the first dotted path where the form has one (none for a prop), prompts and creator notes left out — for each type the action exports (a named scope’s, the types its own selection holds, the index every chat opens with apart) or the page focuses. Where it is present the preamble says to write those keys in those shapes as nested JSON, sending only the keys that change, since an object merges key by key (§7.3), null removing one, a key not in these fields going under metadata, a stale key that is not in fields included, and to leave a reference empty when there is nothing to point at, saying so, and to say a change is made only after its tool call succeeded — a sentence, which no check enforces. Nothing refuses a key for being outside it — the format’s open objects keep whatever is written; only the reserved keys are refused (reserved, §7.3), and content the format finds invalid (invalid). film is one lean export (§6.18) — every $id and the selected content, without sources, hashes, $stem, export id or time, the script walked only where a scene is selected — of one selection: the action’s lines (a registered scope’s recipe likewise), unioned per token with the index every chat opens with when a film is open — the project’s name and synopsis and the characters’ and locations’ names, none in the viewer’s recycle bin —, and, on the Project rail, with the page’s own entity, selected by the action’s line for its type (or a this line) unioned with the field’s read paths, else by summary with them, never whole; the background selections carry no notes, negative prompts or media, while a path an action or a field names is kept as named. Every entity in film is at its working version (R3 of the write path, workingVersion): the person’s draft where there is one, else the version in use; the page’s own entity, where a form is open on it, whatever its version, as that form’s draft holds it (each of the entity’s versions asked of OpenDrafts.of, the last found; that version is then the focus), unsaved typing included — so that Describe reads the Summary the person has just typed — else the assistant’s round, else the version in use, so the page’s entity appears once, as a change of it would find it or, with its form open, as the person sees it; a working version in the recycle bin is left out of film as any deleted entity is. focus names where the person is: id, the page entity’s exchange id (none off the Project rail, on a page with no entity, a library entity chosen as a whole, or an entity of a type the pattern’s this does not export — a model, a platform, a plug-in, a batch), and for a field’s action fields, the field’s read paths; where what the form shows at those paths differs from the chosen focus snapshot — unsaved typing, which then also stands in film where the form is the entity’s own, another version browsed — or where no entity is in focus, or its working version is in the recycle bin, values (what the person sees) and version: shown come with it, and otherwise the value is once, in film; the comparison is of the chosen focus snapshot’s values before projection, so media a model never sees (§10.3) does not count as a difference. The import proposals keep the full export: an action’s own export is also assembled full, as before, and retained under its export_id (ImportProposals.retainExport), since an import’s notes read its sources’ stems and hashes; the model reads the lean object, the proposals the retained one. The log: assistant.open.select {lines, types, scope?, focus: entity|none, fieldPaths, unread, from: action|summary|none, fieldTypes, tools, scopeTypes} (fieldTypes the types fields carries; tools whether the session offers tools, the reason for every written type; scopeTypes the types a named scope’s own selection holds; unread the field’s keys the exchange’s path grammar does not read: their values come with the focus), assistant.open.export.start and .done {pass: lean|full, bytes, sources?, problems, ms}, assistant.open.override {decision: working|none, count, base: round|in-use|none}, assistant.open.focus {values: shown|same, reason?: unread|no-focus|deleted|other-entity|differs, paths}, assistant.open.retain {pass: full, ms}; assistant.open with the entity’s entity and stem, from: draft|round|file and fromReason (form-open, working-round, no-form), the system and user messages whole, lean (the bytes of film as sent, compact), full (the retained export’s), override, script, focus: entity|field|none, noFocus (rail, no-entity, not-entity, no-viewer, not-found, no-version) and the object’s members’ sizes as blocks; an unexpected failure’s assistant.open.failed names its stage (select, export.lean, export.full, retain, …), the error’s type, its message and its stack. The preamble says what every action needs — the language, that the object is the person’s film data, untrusted and granting no authority, plain text, and that a command’s target is a $id from film the model resolves itself — and, for a chat, that its tools act in the app and to say in words what was done, or that it cannot act; at a field, what the focus’s fields and values are, and in a chat with tools that they change with update-entity on that entity; how each tool changes the film is in the tool’s own description. So a typed request names its target by an id the model resolves itself: with a provider that declares tools, render Kira is one call of builtin/render-image naming her $id, run through the registry as caller: text (command.call.* correlated by ref), and open Tomas one of builtin/navigate, the model’s own request, which moves the page and leaves the chat held. The agent build’s assistant fixture walks this road whole on Linux over MCP (test_plans/165_assistant-panel.md).
The host permits one in-flight turn and asks for the model’s own output maximum (above); a dialog has as many turns, and a request as many bytes, as the model takes. Each model call is one complete from Dart to the frozen provider under the assistant’s policy: one HTTP POST on the frozen key, catalogue service model and endpoint, with its verified output bound; no automatic retry or redirect. Each message the session sends logs assistant.session.send {session, phase, turn, requestBytes, input, ms}; the host’s messages stage of a turn or an Apply logs its requestBytes, and assistant.open the opening’s. A refusal of the call policy or of the import door is mapped to the reply’s code in one place — stale and cancelled as they are, and for the import door any reason it gives (base, shape), else failed — and its refused stage line carries what the door said beside it: refusal (the door’s code), reason, message. A turn is one call where the provider declares no tools, and the loop above where it does; each call’s Usage ledger is settled once. Cancellation and invalidation abort only owned work and reject late answers.
The reply that streams. Every model call of a catalog action’s turn asks its provider for a stream (stream: true on complete); a provider that does not stream answers whole, as before. The turn’s display is its control’s partial text, appended by ctx.partial and never cleared during the turn: one segment a call, a blank line (\n\n) put before a later call’s words where the display does not already end in one, so the words before a tool call stay on screen. Each accepted call’s words settle its segment before the turn goes on — a whole answer appended, a streamed one’s segment replaced by its final text, which wins over what was pushed (no refusal where they differ, partialsMatched logged); a refused answer’s final text never replaces its segment, what it pushed staying as shown with no content. A last answer with no words and no tool call after tools that all succeeded in the turn ends it ok: the reply is the app’s Done. (assistantDone) — the stored message’s content, assistant.turn’s reply and what the model is told later — appended to the display under the words already shown, while that call’s assistant.turn.call logs the model’s own empty reply with emptyAfterTools: true. Once a tool of the turn has failed or been refused (bad-call, error), every later assistant.turn.call of the turn logs toolFailed: true, and an empty last answer then is noWords (The assistant gave no answer. Try asking again.), as an empty first answer is; an answer whose text is of the wrong type stays badResult, whatever its length otherwise. The reply carries display (what the person saw) beside reply (the last call’s text, or Done. where it said nothing after its tools, what the model is told later and what Apply may finalize), display absent where the two are equal. A message keeps the same split: AssistantMessage.display beside content, toJson still {role, content}. A turn stopped, timed out or failed after text was shown keeps that text, its trailing blank lines trimmed, as an assistant message with display and empty content; a message with no content is left out wherever the conversation is serialized, by one filter applied the same way every turn, so the unfinished prose is never told to the model, a finish never reads it, and the backend’s kept tool exchanges — the pre-tool words among them, once — still meet the person’s messages they are keyed by. The session exposes the running turn’s partial (empty while idle) and does not notify on it: the dock’s transcript is not rebuilt for a partial, only the growing bubble assistant.partial above the busy line, a finished message showing its display. Stop while a model call is in flight aborts its network waits and sends an End for its credit operation once, independently of the turn’s cancellation (§6.6) — endOperations(…, reason: 'stopped'), not awaited, so the dock is idle at once; the background receipt waits for that End before reading its settled charge, without keeping the dock busy; receipts of earlier completed calls continue independently of Stop; the website’s operation lifecycle remains authoritative; on credit a streamed operation closed without its final usage is charged its hold; on the person’s own key nothing is asked. The turn logs assistant.turn.first {turn, call, ms} once, at its first partial; assistant.turn.call adds partials and shown (whether any was shown, partials > 0 — what reached the screen, apart from whether the HTTP answer was a stream, which the HTTP door’s lines say), and firstMs and partialsMatched where the call streamed; assistant.turn adds partials, shown and display, the text shown; assistant.turn.stopped {turn, call, display, onCredit}, then the usage helper’s usage.end or usage.endFailed and the account’s account.portal.end; the session logs assistant.session.display {session, kept, display, error?} where a message with a display is added — the session id and the text as shown. Not guarded, by design: no retry of an End, no automatic restart receipt reader, no check of the partials against the final text beyond the logged boolean.
Credit admission failures. The dock names the failed step and distinguishes an unreachable payment server (with its transport error), a refusal (with its code), and insufficient credit. Earlier completed work remains. Try again sends an ordinary new turn, Continue from where you stopped., with completed exchanges retained. It is offered while the composer is empty; a drafted correction keeps its words for ordinary Send. Nothing is retried automatically.
The note in the dock, an answer cut off, a provider’s own words. While a turn runs, the dock shows the turn’s last reasoning sentence under the busy line (assistant.note), kept across the turn’s calls as the seam keeps it, in the theme’s quiet style (bodySmall, onSurfaceVariant, italic), at most two lines, wrapping; the composer’s room keeps those two lines free for the whole busy turn, so a long note never overflows the docked band, and a provider that sends no note shows the busy line alone. Beside the busy line the dock shows the seconds the turn has run, m:ss (assistant.seconds), ticking each second whether a note comes or not, counted from the session’s turnStartedAt — kept with the running turn, so a dock rebuilt or reopened keeps the count — so that a silent stretch is visibly alive. The busy line reads Thinking… N s (assistantThinkingFor) while the call under way has said nothing yet — no reasoning, no words and no answer returned since it began (heardCall below call; the host marks a call heard as its answer returns, so a call that answered with tool calls alone runs its tools under Waiting for the assistant…, its post-call assistant.turn.stage line carrying heard) —, N the seconds since that call’s callStartedAt, so a later call counts from its own start and a dock rebuilt from the same; otherwise Waiting for the assistant…. It is redrawn by the seconds’ own one-second tick, no other timer, and each change between the two words is one assistant.dock line, busy.line {session, call, heard, thinking, callSeconds, turnSeconds}. Under it, the last change: the running turn’s last landed create (Added the Ferryman), fork or patch (Changed Mia) of this chat (assistant.lastChange, one line), the entity’s name or else its stem, from the ordered receipts of entityWrittenProvider whose attribution names the chat — a write that landed, never a tool the model only announced or a refused one; the chat’s controller keeps it, so a dock rebuilt shows it, and it goes with the turn; each receipt of the chat’s is logged assistant.lastChange {session, call, kind, stem, name, kept}, a pick or a period not kept. The composer’s room keeps that line free beside the note’s two. A provider whose stream opens items says so as they open: fo-openai notes Thinking… as a reasoning item opens, though it may bring no summary, and each function call’s alias and its number in the answer as the call opens, a summary’s sentence following where one comes, each logged openai.note {text, source: reasoning|tool|summary} — the first two leave the shown sentence as it stood, the busy line saying the call is thinking; a chat-style stream (fo-openrouter, fo-xai) has no item-open event and notes its visible reasoning and each tool call once its name arrives, and an empty reasoning stretch there notes nothing, the seconds being the signal. Each change of what it shows is one assistant.dock line — note.shown {session, call, text, ms}, note.cleared {session, call, text, cause, ms} with the dock’s cause, cleared where the seam emptied or ended where the turn went (the host’s own cause is on plugins.note) — never one per rebuild. An answer cut off at its output length is told by the provider before it decodes the answer, from its terminal evidence (a length finish, or an incomplete response for max_output_tokens), and refused cutOff where tools were offered or the answer holds calls or no words: a cut after whole calls, inside an argument and with nothing read the same. The turn runs none of that answer’s calls and ends cutOff (The answer was too long and was cut off, so nothing from it was used. Try asking for less at a time.); its streamed words stay shown as a message with no content, as any failed call’s do; earlier answers’ tool effects stand; the call settles once, and with no usage of its own its usage record is the request alone (its tokens are in the provider’s logged answer). No repair and no resend. A writer’s words alone, cut with no tools offered, keep their bounded text (finish: length). A provider’s refusal reads in its own words. A stock provider’s HTTP refusal carries the service’s own explanation as its message — a refused key’s after the key’s sentence, OpenRouter’s uncertain answered refusal (a 429, a 5xx) after its uncertain sentence, still uncertain and never resent; a plug-in’s own failure carries it as its message, which the host hands on as AssistantReply.says and logs on assistant.turn; AssistantSession.says is those words while its error is still the code they came with, and the dock shows them after the code’s sentence. The first provider failure ends the turn, as it does; a provider with no words shows the sentence alone. A change not applied. Where a write the model sent was refused by its type’s record (shapes in the tool’s answer), the dock shows one line above its error line, Change not applied: <paths> (assistant.notice); it reads — corrected only once a later successful call of the same write (update-entity, create-entity; a read such as export, or a call with no content, never) to the same target — an update’s id, a creation’s type and name —, its content an object, covers each refused path exactly: the same key path, a block by its id or, a new one, its anchor, a list that replaces whole by the list; for a key its record lacks, that key under metadata at the same place, or the block sent again without it; metadata alone, another block, an empty object or a path the refused content gave no way to name again (a block with neither id nor anchor) never does, while a content that was no object is covered by that later write’s object, and any other answer leaves it as it was (noticeAfter, AssistantSession.changeNotice), and assistant.notice {session, turn, target, paths, corrected, ms} logs each change of it. The model’s nudge is the tool answer it already receives; nothing is retried or repaired.
A provider’s verified output bound is its manifest’s x.assistantText: protocol filmopen-assistant-sandbox-2026-10-04 (assistantTextProtocol, the value every provider already declares) and outputParameter one of max_tokens, max_output_tokens and max_completion_tokens; fo-openrouter on OpenRouter maps max_tokens without one. A route whose provider declares none is refused outputBound.
The app owns a typed conversation turn for start, turn and finish, whose envelope the session sends through the frozen host backend; a scripted backend replaces it in tests without granting project or credential access. This text road carries no native audio (the voice conversation is a separate service). A field’s action whose route’s provider declares no tools is offered disabled, noTools, since its chat could change nothing; its model and provider are carried on the offer, and the voice’s admission is unchanged. A chat proposes nothing: a field action’s chat opens with the focus naming the field and the instruction to change it with update-entity on its entity, as any change, into the person’s draft (§7.3, §9.3); its catalog’s finalize is ignored and logged assistant.finalize.ignored {tag, action, reason: chat-writes|no-tools} — no-tools where the provider declares none, so the chat can change nothing and its preamble says so. A proposal comes only from a voice conversation’s finalizer, reviewed and applied in the voice dialog through applyImportProposal (§9.3).
The native voice session under the state’s contracts (realtime_session.dart, assistant_voice_host.dart, native_conversation_session.dart). The normal composition’s one install on Windows, Android and Linux (installAssistantVoiceService, called by installAssistantService) installs the dialog’s facade through the state’s seam slot (AssistantVoiceService.install) and the native conversation door through NativeConversation.install; the two are one service. Where the platform has no voice route, or the plug-in host does not run (the web), nothing is installed and the log says why: the facade stays absent and the default NoNativeConversation answers not available. The facade’s openAction, called from the Start gesture, opens the action as the text chat does, reads the saved key there and decides the binding’s funding from it (readVoiceFunding): OwnKeyFunding where the record holds a well-formed key, else FundingUnavailable — needsKey for a missing record, keyInvalid for a malformed one (not JSON, the wrong shape, or shorter than twelve characters), unknown for a store that cannot be read — and refuses the gesture with the state’s own sentence (credentials, credentialsInvalid, credentialsUnavailable: dictation’s no key yet and couldn’t be read, the provider keys’ not valid) before any session exists; the key’s last four characters go into the Usage record and nothing of it into a log. It captures the route’s tuple: OpenAI gpt-realtime-2.1, voice marin, the audio WebRTC negotiates; Grok grok-voice-think-fast-2.0, voice eve, PCM 16-bit mono at 24 kHz. OpenAI’s WebRTC session asks for semantic turn detection at eagerness low, interrupts kept, far-field noise reduction, and captions in the app’s language (languages: [<code>], as dictation’s). What the session sends is the binding’s: the WebSocket endpoint’s model and the WebRTC call’s model are read from binding.model, the configured voice from binding.voice (RealtimeProtocol.endpointFor, configure, connectOpenAiWebRtc), and the audio format stays the protocol’s — WebRTC negotiates OpenAI’s, Grok’s PCM is what configure asks for — which audioFormat names. NativeConversationSession is the state’s ConversationSession over the same session — snapshot, captions and Usage read from its notifier, its six operations given typed outcomes (a refused microphone is not ready, a missing key unfunded, a route this device lacks unsupported). The door’s open(binding) answers the session the gesture readied for that binding, and a not-ready session for any other binding; the normal composition replaces this bridge. The one microphone owner (§7.2): Start takes a fresh MicrophoneOwner('assistant') from the arbiter synchronously after the key check and before any resource — the PCM player and capture, or the permission request and the native peer inside connect, whose order is unchanged — and a refusal ends the attempt with mic_busy naming the holder, before a Usage record, a device or a socket exists. The owner is released, with the host’s slot, once End has awaited every release; a release that failed is logged with its error (voice.release.failed) and gives them back as well, but the session ends in error voiceRelease — the dialog says the audio could not be fully released and to close FilmOpen before another conversation, since the platform may still hold the device — released stays false and the adapter’s End answers not ready; voice.released names the failed releases, the phase and the conversation’s messages. Finish (finish, §7.2’s contract): input stopped — the peer’s input disabled, or the PCM capture stopped — then one input_audio_buffer.commit on the voice wire, response.cancel only while a response is in progress and once per response (the provider refuses a cancel with none; a response created during the drain is cancelled as soon as it is known, and Finish’s own cancel after input shutdown does not repeat it), local playback cleared; then the admitted captions drain for at most five seconds from Finish itself, the gate admitting commit acknowledgements, caption events, truncation acknowledgements, response.done’s Usage and errors, and dropping new responses, spoken output and a new turn’s start (a caption of speech heard before input stopped is admitted); then End, the transcript frozen with every partial caption labelled incomplete. On the WebRTC voice wire Finish’s commit is the only manual one, so an empty-commit refusal (error with code input_audio_buffer_commit_empty, read by its code alone) answers it: the drain ends early once that refusal has arrived and no caption is pending — no committed item without its completion, no turn still heard or awaiting its commit — since a committed acknowledgement cannot be told from the semantic VAD’s automatic ones. Grok’s Finish is End with its captions labelled; a Finish before the session is ready is End. End and Close during the drain win at once: the drain settles and the one End runs. interrupt cancels a response in progress, once per response (a second Interrupt on it, or Finish after it, sends no second cancel); on the PCM route it clears local playback, on WebRTC the audio already in the provider’s output buffer plays out. Every acknowledged commit counts as owing a response, Finish’s own included, and a response begun during the drain is accounted before the gate though its output is dropped: a response never reported keeps the conversation’s total unknown (§6.7) rather than priced short. Limits, recorded: a response created between response.create and its response.created is not cancelled; remote WebRTC audio already flowing is not muted during the drain or by interrupt; a failed caption (…transcription.failed) during the drain ends the session as it always does. No cap of the app’s (§7.2’s contract): a conversation runs until the person’s Finish or End, or the provider or the connection ends it; voice.start logs cap_ms: none. A sessionCap given to the session — none in the app — finishes it through its own Finish, the same drain, the transcript kept, and capReached says so; a Finish already under way is the person’s; nothing reconnects. The session checks its validity and the cap, and tells the dialog’s clock, once a second (voice.cap logged when the cap is reached); only the PCM route polls its player’s device position, every 100 ms, telling listeners when the phase turns to listening, and a failed read is logged (voice.playback.failed) and ends the session as voicePlayback.
The conversation on credit (assistant_voice_host.dart, realtime_webrtc.dart, realtime_session.dart). Where the chosen provider has no saved key and OpenAI’s native WebRTC peer would carry the call, readVoiceFunding asks the funding seam (ProviderFunding.funding('openai', 'conversation')); its CreditFunding is the call’s, with no key — for a chosen Grok the call runs as OpenAI’s (the binding, the peer and the Usage record OpenAI’s, voice.funding saying reason: fallback with selected: xai), and voice.provider names the saved Speech service, the route asked for and the one started — written once the context still holds, so that a Start refused as stale leaves no route logged as started — so that a Grok the dialog set aside where it has no route shows in the log too; the saved choice unchanged and a stored Grok key used as before; a call with no peer is never on credit, and without credit the refusal is the missing key’s as before. The host mints the operation’s id, a v4 UUID, on the gesture. connectOpenAiWebRtcOnCredit makes the offer the key’s call makes and posts the same multipart sdp and session, with no redirect, to https://<kFilmOpenDomain>/v1/portal/realtime with the seam’s portalHeaders and the id in X-FilmOpen-Operation, in place of OpenAI’s URL and the key; the answer’s raw SDP from a 200 or 201, bounded at 64 KiB, is the call’s. No portal headers is credentials; any other status is voiceStartRefused:<code> — the string error of the website’s error envelope ({error, says}; says, a sentence for a person, is never read as the code; the body is logged whole) where it is a safe token, unknown where it is not, status-<n> where the body names none — kept through the cleanup (unless the cleanup itself is uncertain, voiceConnectionUncertain), so that the dialog says FilmOpen could not start the conversation ().; a 401 of the portal credential (no-portal-token, bad-portal-token) lets it go once (portalRefused) and nothing is sent again. On credit no key is read: the Usage record’s route is filmopen.ai with no key, the captions have no record of their own, a running record keeps its units with no cost and the unknown basis, and the session’s one End of an answered start (a 200 or 201, marked on the trace before the answer is installed, so that a Close racing the installation still ends it) — whatever ended it: Finish, End, Close, a disconnect, disposal — ends the operation (ProviderFunding.end) and reads its receipt, once each (a start the website refused or never answered sends nothing more: the website’s sweep settles it), after the releases of End have run and before the terminal record, whose cost is the receipt’s chargedCents (USD, charged, its operation and receipt id on the record), pending where the receipt read came back unsettled. The dialog’s billing line for OpenAI says the saved key pays where there is one and the credit, captions included, where there is not; Grok’s is the key’s alone. Its log, on both roads: voice.gesture marks the Start press (the route asked for, ms: 0); every refusal before the session exists writes one voice.refused — its stage (platform, busy, target, plugin, admit, prepare, funding, and connection once for the first check the session, the preparation or the finalizer finds failing after the open; the dialog’s admit with code: admission-moved for a pick moved before Start, and microphone for an occupied microphone), its code, for stale the first check that failed as reason (no-environment, no-project, no-source, draft-changed, project-reloaded, viewer-changed, apply-moved, plugin-changed, consent-changed), the route and the milliseconds since the press — and any other failure one voice.failed with the stage it fell at (open, start, end, finish, mute, release) and the error whole — the host’s own failure of the opening, at its stage, with the error’s type and message —; the dialog writes its lines through the voice service’s note, and voice.error.shown once for a failure the session reported, which stays on screen until the next Start or Close, even after a late clean-up clears the session’s own; voice.funding gives the funding decision (provider, outcome, reason and its own milliseconds), before any operation exists; every later line carries the operation on credit or route: direct, and the milliseconds since the Start gesture — voice.offer.made (the offer’s SDP whole and its length), voice.start.request (the endpoint, the model and the session part whole, just before the credential is added), voice.start.sent (the route and the headers by name, never their values), then voice.start.answered (status, length and the answer’s SDP whole) or voice.start.refused (status, code and the answer’s body whole, or unread with the error where it cannot be read) or voice.start.failed (stage, code and the error whole; at cleanup the cleanup’s error), voice.connection for every native peer, ICE and data-channel state the platform’s peer reports (WebRtcAudioEventKind.state) and every lifecycle event (kind: lifecycle, with its failure’s problem), voice.capture for each stage of the microphone’s capture (start, input, acquire, tracks, attach, enabled; superseded, ended, and failed with the stage it fell at, its problem and the error’s type, or code: timeout where the bound of a Mute or Unmute expired, at its last stage, release, or waiting for earlier input work — which the lifecycle line says too) — the platform, the processing requested (echo cancellation, noise suppression, automatic gain), the constraint form the platform reads (top-level on Linux and Windows, optional on Android), and at enabled what the track reported (its settings as it reports them, its id and label included — the SDK reports no sample rate —, unknown where it reports none) with its source (android-static, desktop-source-options) and effective: unknown, since neither measures the processing in effect — voice.session.config once, when the session is configured (the provider and the session.update message whole as it is sent — the instructions and the declared tools, their aliases, descriptions and parameter schemas after their bounds and cuts), voice.first_event (its type as it came), voice.wire.failed (its stage — message, channel, send, peer —, the error whole and the text of a message refused or not sent), the session’s voice.start for each step of Start (begin with the provider, model, voice, transport, funding, language, device and bounds; prepared with the bytes prepared, never refused for their size; key with its source and whether it is long enough to be one, never its value or its length; microphone acquired, or busy with its holder; playback, capture, usage, captions and connected, each with what it worked on and a failure’s error whole; ready with the elapsed time; failed with outcome: closed, the code and the error whole where a step fails after End), voice.accepted, voice.usage for every Usage record as it is written (the record, its id, status, units, cost and basis, on the conversation the counts its cost was decided on; a failed write with its error), voice.mute for every Mute and Unmute (the requested and the resulting state, the transport and readiness; begin, done with what the capture did and the elapsed time, failed with the error whole, ignored with its reason), voice.truncate (the item, its response, index, heard and generated frames; a response before the acknowledgement at warning), voice.failed for every failure that ends the session (where — start, wire, capture, send, mute, tick, playback, usage, truncate or the event’s type —, its code, the original error whole and the phase), assistant.voice.turn (the turn’s kind and state, its milliseconds since the connection as connectedMs; on both routes, with its transport, the item’s and the response’s identity, a response.done’s status and usage, the person’s words or the model’s answer as the event’s transcript where it carries one — a completed input caption, a finished answer — and an error or a failed transcription with the provider’s error whole, at warning) and assistant.voice.finish (begin; end where Finish is End, with why; drain; done with its reason and elapsed time; a failed stop with the error whole), voice.stop (its reason, the phase, mute, readiness and connected time), voice.release.done for each release as it completes (capture, peer, playback, socket, events, with its time), voice.release.failed (the release, its error and its time), voice.released (the failed releases, the phase, the error, whether every release completed, the elapsed time and the messages), voice.end (its outcome and elapsed time, a failure with its error whole) and voice.receipt (received or none, and the receipt whole — its settlement outcome, chargedCents, releasedCents, observedExpenseCents and receiptId — with its elapsed time, a failure with its error whole) — and never a key, a bearer, a token or a header’s value; the context, sent apart, is assistant.open’s to log, as the instructions are there too.
The voice session’s tools (voice_tools.dart, realtime_session.dart, realtime_protocol.dart). An OpenAI session declares the app’s commands (§7.3) as its tools; a Grok session declares none and logs voice.tools.unsupported once, and a chosen Grok running as OpenAI on credit is an OpenAI session. The table is made once, at configure, from CommandRegistry.all — every command, exposed or not, since the caller is the app itself — and never changes during the session, so a call always resolves to the command it was declared for; a command registered later is not declared to it. Each command is a function tool named c<index>_<name> (its position from 1, every character outside [a-zA-Z0-9_-] turned into _, cut to 64 characters), with its description whole and its input schema without session and operation. Every registered command is declared, with no count or size of the app’s — a provider that takes less refuses in its own words — and voice.tools.declared {count, bytes} says what was; the tools themselves, as sent, are in voice.session.config’s message. A response.function_call_arguments.done is recorded in its response’s turn, never run by the event pump, which never awaits a command; a malformed one ends the session as voiceProtocol, and one arriving after its response’s response.done is logged voice.toolCall.late and dropped. The ToolTurnRunner runs the calls in arrival order, one at a time, on its own future: an unknown alias or arguments that are not a JSON object answer {"error":"bad-call"} with no dispatch; an effectful command gets session and operation = voice- and a digest of the provider’s call id, so a repeated call is answered from the ledger and runs once, while a read-only command gets its arguments as sent; each runs through CommandRegistry.call with caller voice, a 30-second deadline — which bounds admission, the registry refusing a dispatch or an effect after it, not the wait for a command already running, since every command answers within its own bound — and the session’s guard. The guard holds while the session is not closed and not finishing, the call’s response is not interrupted, and no interruption — speech starting, or an explicit interrupt, which blocks its response as speech does — has come since its turn’s first call arrived, so a call outliving its response’s response.done is stopped too. It is read before a dispatch, by the registry’s admit inside an effectful command, and again before the output: a call stopped after its effect was admitted sends nothing, and its answer stays in the ledger, never replayed to repair the lost output. The output is a function_call_output item carrying the result’s structured as JSON, else its text, whole, with no bound of the app’s. The seal: a response’s response.done, once its Usage is counted, seals its turn — unless its status is failed or incomplete, which is logged voice.response.failed with the response’s id, its status and its status_details whole and ends the session as voiceProvider with that Usage kept and nothing sealed, so no continuation follows; once every call of a sealed turn has answered or been dropped, one response.create follows, owed in the Usage accounting as every response is, and only where an output was sent and the guard still holds. The phase follows the runner’s remaining work: while any call is queued or running it stays thinking rather than inviting the person to speak, and a turn that ends with no response hands the turn back (listening) only when the runner holds nothing more and no response is under way. Each call logs voice.toolCall {call, callId, responseId, asked, arguments, alias, source, name, effectful} (callId and responseId the provider’s, which name the operation’s replay and the turn’s interruption), the registry’s own lines, then voice.toolCall.output {call, outcome, bytes, sent, reason?, output, callMs} (output what the model is sent, whole); a turn logs voice.turn {response, calls} at its seal and voice.turn.done {response, responseCreated, idle, reason?, turnMs}; a call arriving during Finish’s drain, or to a session that declared no tools, logs voice.toolCall.dropped {reason}; an explicit interrupt logs voice.interrupt; an unexpected throw in the runner logs voice.toolCall.failed with its call, type and the error’s message in error, and the next call still runs. One function, runToolCall, runs a call of either road — the bad call, the liveness read before the dispatch, session and operation, the registry call and its output, whole — and each road logs it under its own events. Not guarded, by design: a command running when the turn is interrupted completes, and a lost output is not retried. A navigate moves the screen, after which a field-bound conversation ends as stale, as before.
The source binding accepts the original material content or the captured draft’s own normal autosave. For a voice conversation, another source change invalidates the session even while the form has unsaved edits; save timestamps and normal retired-key cleanup do not invalidate its own save. The transport reports a host-only admission flag: only an explicitly unadmitted request refunds the local turn counter. A timeout or unknown result retains its turn, and a still-settling backend refuses a new call without consuming another turn.
A plugin-returned diagnostic code is kept only as a bounded token (a lowercase letter, then at most 39 letters or underscores) — a code ending in /finalizerTargetMismatch kept as that reason — anything else becoming failed, so no sentence of the conversation, nothing beyond one such word, can become a persisted Usage error code.
An OAuth slot refreshes an expired access token before the HTTP door sends its request. The door adds its Bearer header after logging the request, and names its usage by the slot rather than token characters. A refresh response with a 4xx status other than 429 clears the credential and key record and requires sign-in again. A 429, a 5xx or another unsuccessful response fails the call and keeps both records. No failed refresh selects credit or retries the request.
A text completion uses a route of its plug-in, saved under plugins.route.<tag> as a JSON identity: funding with its slot, program or key. Choosing a route makes its plug-in the preferred text provider. The Text row shows the chosen model’s decision: the plug-in admission takes for that model and its routes (Kimi K3, which only an OpenRouter plug-in serves, reads <plug-in> / FilmOpen credit though Codex was chosen on OpenAI; a chosen route that is itself unavailable refuses every model its plug-in serves — the plug-in’s text platform among the entry’s and its models empty or naming the model, whether the plug-in is on or off — the refusal’s tried[] entry naming that model and whether the plug-in is on (enabled), and the row then names it, to be cleared; for a model it does not serve it is set aside, below); where no plug-in funds the model, the one that serves it, the preferred among them, so that a route can be chosen or cleared there. A route chosen on a plug-in that does not serve the model stays stored and is set aside for that model — plugins.llm.decided’s tried[] entry for the model, funded or not, carries preference {provider, route, reason: doesNotServe, model} — and is honoured again once a model that plug-in serves is chosen. A chat keeps the route it opened on for all its turns, whatever is chosen afterwards; its session exposes it (AssistantSessionBackend.fundingRoute) and the dock shows it (§9.1). Automatic clears the saved route while retaining that preference; it keeps the existing order: configured local server, signed-in slot, configured program, stored copied keys in manifest order, then FilmOpen credit. For a plug-in declaring plan render kinds, its programs remain explicit choices and Automatic leaves them out; every other plug-in keeps its order. Explicit choices can offer credit beside a stored API or OAuth key using credit-only account metadata. A saved choice is honoured exactly or refused as routeUnavailable for a model its plug-in serves, including when its provider is disabled; another plug-in’s stale choice does not block the selected provider. An opening uses its existing wait for all pending funding questions, including the first explicit-credit answer. During a refresh the last answer remains available for explicit choices as for Automatic; only a changed answer is published, and a new unavailable answer refuses the next opening. The common completion admission refuses a saved choice marked routeUnavailable before JavaScript, including developer and writer calls. An Automatic route with no funding still reaches the plug-in and its existing door checks. The existing writer dispatch carries its held route to the same HTTP door. Pinned credit never reads a credential; a pinned copied key or OAuth slot must name the selected usable credential and never falls back to credit. The binding passes the route to complete; a refusal or a program’s quota failure ends it without another attempt on a different route. Renders, analyses and speech retain their existing funding rules. An OAuth block with sends_output_cap: false gives the assistant policy no output cap. Key-slot views carry the exact plan label, manifest authentication and the existing record’s account/expiry without opening the credential store.
The route label names its plug-in and origin: the exact signed-in slot or program, your key, FilmOpen credit, this computer, or the selected rented destination. Labels format the decided identity; they do not choose another route or infer one from an unrelated key. An unavailable choice keeps that identity and says where Settings → Models can repair it. On a plug-in that is not the preferred text provider, its page also offers Use Automatic to clear that plug-in’s saved route through the same logged setter while retaining the preferred provider. The same words appear on the plug-in page, in the assistant dock and in the Auto-generate agreement; writer and picture prices remain alongside their origins.
A signed-in HTTP slot opens token-only plan Usage with the saved account label, falling back to the OAuth block’s label. Text process calls open plan Usage on the ledger they run on, including interrupt’s fresh ledger; only their answer’s account label replaces the record’s label. Local assistant HTTP opens local Usage after policy admission, one record chain per model call. Both routes retain tokens without pricing or credit operations and log the opening decision and terminal record.