Skip to content

Borrowed work and resource ownership

A resource accepts one job at a time. If two requests arrive together, only one reaches the upstream. The other receives 503 resource_busy with Retry-After: 2. A job reserves its primary resource and every also_requires dependency together, so two different upstreams cannot both borrow the same GPU or memory resource.

Finishing a job doesn’t immediately reopen lending. Every reserved resource needs a successful capacity sample taken after completion. The end of a job starts that sample at once, and a request that arrives before it lands waits up to 4 seconds for it rather than receiving 503 resource_busy. Owner presence, pressure, configured reserves and freshness checks still apply.

A memory resource's reserve is a floor the job never touches. The job's memory requirement on that resource, configured or learned, must fit in what is free beyond the reserve, so a machine with 31,000 MiB free, a 24,000 MiB reserve and an 8,000 MiB job refuses it, although each figure fits on its own. The memory a job's requirement names is the primary resource's when it is system memory, as on a CPU-only upstream, and an also_requires memory resource's only when a per-resource requirement names it. A memory resource with no requirement still refuses work while free memory is below its reserve.

Availability and voice engines

Compatible voice servers may expose POST /v1/audio/executions for durable synthesis. vacancyd admits it as work and selects its budget from the top-level model, as it does for speech. A clean end of the upstream response releases the reservation. The native server must keep that response open until it has published the durable audio and receipt and released its worker. Returning 202 while synthesis continues does not meet this contract.

Compatible servers must answer capability, execution lookup and audio replay reads without starting synthesis. vacancyd forwards those reads without admission. Result-read IDs must contain 8 to 80 ASCII letters, digits, underscores or hyphens to pass without management permission. Unlisted reads are refused unless the upstream sets management = "allow". These routes do not add retries, a queue or project authorization to vacancyd.

The console's vacancy sign reports admission for each route's default budget at the last poll. It is not a reservation or proof that a specific request will succeed. The overview names the admitting routes; a free helper route does not establish voice capacity. Workloads shows the route's refusal and target-health check separately. A requested model may need more memory, and capacity, owner pressure or resource ownership may change before submission. An unavailable target can fail even while admission has capacity.

The chatterbox adapter also supports compatible voice servers that switch engines. When /status reports an engine, the model observation preserves that name. Only a request naming the same loaded idle engine receives resident credit. Switching from Qwen to Breeze needs Breeze's cold-load budget; a generic/default model alias cannot prove which engine will be reused. Legacy servers that omit engine retain fixed-model residency. A loaded server reporting a null, empty or invalid engine does not prove residency.

Configure each engine's measured requirement by its exact request name, with a default covering the largest supported cold load. For example, on a server qualified for Qwen at 6,000 MiB and Breeze at 11,500 MiB:

# Within the existing voice upstream, after its other scalar settings:
default_min_free_mib = 11500
[upstream.models]
qwen = { min_free_mib = 6000 }
qwen-design = { min_free_mib = 6000 }
breeze = { min_free_mib = 11500 }

Use the values qualified for the actual renderer profile. These examples do not qualify another engine or profile. resident_min_free_mib can keep extra headroom for a loaded engine. Omitted values retain the existing zero extra-memory requirement for proven reuse. The generic status decision uses the default cold-load budget when the loaded engine is named, so it can withhold while a smaller named request still fits; it never assumes the request will choose that engine. An upstream error after forwarding is separate from a pre-forward admission refusal and does not establish that retrying a generation is safe.

Knowing when work finishes

Adapter Completion evidence Model observations
ollama A clean end of the upstream response on a known inference route /api/ps supplies model names, resident VRAM, expiry and the context each model was loaded with. An admitted request starts as loading; a resident-model observation identifies it as running. After completion, a resident model is shown as loaded idle.
openai A clean end of the upstream response on a known synchronous inference route Unknown. The OpenAI-compatible API does not standardize model residency or slots.
llamacpp The same synchronous routes as openai /slots supplies running/idle state. Missing or stale slot observations refuse admission. Use this explicit adapter when the server exposes that endpoint.
comfyui A matching terminal entry in /history/<prompt_id>, or, for a prompt ComfyUI acknowledged or a submission lost before any reply, its absence from the queue and history on two consecutive checks (see below) GET /prompt supplies how many prompts are queued or running, which all show as running; /system_stats supplies the memory ComfyUI's allocator holds (torch_vram_total, reported only for a single device). These endpoints don’t establish which checkpoint is resident, so checkpoint identity and loaded-idle state remain unknown.

A ComfyUI submission response only confirms that the job was queued. Its reservation survives a disconnected client and stays until matching history confirms success or failure, or until the prompt can no longer run at all, as described below.

vacancyd chooses each borrowed prompt's ID itself, a random UUID that it records with the reservation before forwarding, and appends it to the submission as prompt_id. The borrower's workflow is otherwise forwarded byte for byte. A prompt_id the borrower set is replaced, so a borrower cannot name, and later have vacancyd cancel or complete, a prompt that is not its own; the reply reports the ID to use. ComfyUI 0.3.45 and later queue the prompt under that ID. Earlier releases ignore it and report their own in the reply, which vacancyd then tracks instead. A /prompt body that is not a JSON object could never be queued, and ComfyUI's error for it would leave the reservation uncertain, so it is refused with 400 invalid_submission before anything is reserved or forwarded.

The submission exchange runs apart from the borrower's connection. A borrower that disconnects, or an owner-return deadline that answers the borrower with 503, does not stop vacancyd reading the reply and recording the job. When the reply cannot be used (an error that does not prove rejection, a malformed or oversized body, or a connection that fails after sending), the reservation follows the ID vacancyd assigned: ComfyUI's history under that ID releases it as usual. On a release that ignored the ID, nothing appears and the reservation is held when tracking expires. A connection that fails before any of the reply arrives can also be released by the prompt's absence, as described below. A reply that has not arrived within completion_timeout_s holds the reservation.

A prompt the owner deletes or clears from ComfyUI's queue never runs and never reaches its history, and a finished prompt's history can be cleared, evicted, or lost when ComfyUI restarts. So one more rule releases a reservation, and it applies only to a prompt ComfyUI acknowledged: its submission reply named the prompt's ID. ComfyUI queues a prompt before it sends that reply, and it moves prompts from pending to running and from running to history under one lock, the same lock under which it reads its queue for either check below. An acknowledged prompt that one such reading finds neither running nor pending has therefore finished or been removed, and will never run. While tracking such a prompt, vacancyd checks where ComfyUI holds it every 5 seconds, then reads its history. The reservation is released when two consecutive checks each find the prompt neither queued nor running and without a finished history entry. A failed or unreadable check counts as neither and starts the count again.

The same rule covers a submission whose connection failed after sending and before any of the reply arrived, on a ComfyUI that queues prompts under the ID vacancyd assigned. Such a ComfyUI never received the prompt, queued it under that ID, or is still validating it and about to. vacancyd waits a fixed 5 minutes, chosen to cover normal prompt validation, then reads the version /system_stats reports. Nothing in these checks reports that validation has finished. If ComfyUI takes longer than 5 minutes to validate the prompt, the reservation could be released before the prompt starts. On 0.3.45 or later, the checks above begin: a prompt found queued or running is followed like an acknowledged one, and a lost submission that two consecutive checks find neither queued, running nor in history is released. A release before 0.3.45, or one that does not report its version, may have queued the prompt under an ID of its own, so its absence proves nothing and the reservation is followed by its history only, as below. A failed version read is asked again at the next check.

Each check asks about one prompt. ComfyUI 0.6.0 and later answer GET /api/jobs/<prompt_id> in a few hundred bytes whatever the queue holds. Earlier releases have no such route, and vacancyd reads /queue instead, stopping at the prompt's ID, so a prompt still waiting in a long queue costs the part of the queue ahead of it. Reading /queue makes ComfyUI serialize every queued workflow on its only event loop, which takes seconds with a long queue and delays everything else it serves, so vacancyd reads it only on those older releases.

Nothing else counts as completion. Apart from a submission lost before any reply on a release that keeps assigned IDs, an ID vacancyd assigned but ComfyUI never acknowledged, including one on a release that ignores assigned IDs, is released only by its history: its absence from the queue proves nothing, because ComfyUI may still queue it or may know it under another ID. An empty queue on its own, a missing history entry on its own, and an increase in free memory are never proof that a job finished. completion_timeout_s bounds tracking, with a default of 3600 seconds. Expiration stops polling and leaves the reservation held for administrator recovery.

A refused or failed connection to the upstream releases the reservation for any adapter, because no part of the request reached it. A failure after the connection is established stays uncertain.

Recognized ComfyUI validation rejections release the reservation because the job was never queued. This requires HTTP 400 with the complete known validation-error shape; a generic error status, malformed reply or successful reply without a job ID stays uncertain. Reuse still requires fresh capacity samples.

A request on a route the adapter does not recognize is never admitted. A GET or HEAD the adapter does not list as a borrower read is refused with 403, because it could return the owner's data; any other method is refused with 501 before it is forwarded or reserved, because vacancyd could not tell when its work ended and the reservation would otherwise stay held until an administrator released it. The error names the route and where to report it; the route tables list what each adapter recognizes. An OpenAI Responses request (POST /v1/responses) that asks for background mode is refused with 400 unsupported_request before anything is reserved, as is one whose body is not a JSON object vacancyd can read to rule that out. A background response answers queued at once and keeps generating after its reply, so releasing its reservation when the reply ended would lend the hardware twice while the first job still ran; vLLM implements this, and Ollama and llama.cpp answer the flag synchronously. Send such requests without background, streaming them where the client wants progress. Routes that manage the upstream (Ollama's model store, ComfyUI's queue, files and settings), read the owner's data on it (llama.cpp's /slots, Ollama's /api/ps) or are unlisted reads are never admitted as work: they are refused with 403 unless the upstream sets management = "allow", and then forwarded without a reservation. Gated WebSocket protocols have no completion guarantee and retain their reservation after the connection closes, but a WebSocket upstream that refuses the connection releases it, as for HTTP. Discovery routes and ComfyUI progress WebSockets remain available while lending is refused.

Where an upstream identifies a loaded model, vacancyd compares free bytes at admission with the first fresh capacity sample after that observation. The measured footprint becomes a conservative prior for another request for the same model. It can raise the required free memory; it cannot lower the configured minimum. A zero delta can mean that the model was already resident, so it is not learned as a zero-cost model. Priors are bounded to 128 identities and reset with the daemon. They are estimates of allocation changes, not per-process memory measurements.

Requests to a resident model

A configured requirement and a learned prior both describe a load. A model that the upstream already holds has left free memory, so its follow-up requests are charged its resident_min_free_mib (default 0) instead of either, on every budgeted dependency. The credit applies only on proof: a model observation that is ready, at most 4 seconds old and taken after the last job or yield on every dependency, plus the adapter's own evidence. Ollama needs the requested model loaded with an unload time at least 30 seconds away, a request that sets no runner option other than options.num_ctx, and a loaded context the request is known to reuse. That is the case when an earlier request of the same kind was seen to leave the model loaded with exactly that context, when its explicit num_ctx equals the loaded context, or when it leaves num_ctx unset and the model's Modelfile sets num_ctx to the loaded context. The kind is the canonical model, the explicit num_ctx or its absence, and the route family (/api or /v1). vacancyd learns this from the first /api/ps observation taken after such a request finished and before another request was admitted. That covers requests that leave num_ctx to Ollama's default, including every /v1 request, and parallel slots, where /api/ps reports a multiple of the requested context. vacancyd reads each loaded model's Modelfile num_ctx once per model digest from /api/show, which is what lets a model loaded before vacancyd saw any request for it, such as one an installer preloaded, be credited on its first borrowed request. Once a learned context shows parallel slots, only learned contexts are credited, because Ollama then loads a multiple of what the request asks for. With nothing learned, or a learned context that differs from the loaded one because the model was since reloaded differently, the request is charged in full. Learned contexts survive unloads (up to 8 kinds per model) and restarts: vacancyd keeps them with their model digest in learned-contexts.json in the admission state directory, and forgets them when /api/ps reports a new digest for the model. The file is a cache. If it is missing or unreadable, vacancyd starts without it and logs why, and deleting it with vacancyd stopped only costs full charges until requests teach the contexts again. llama.cpp needs idle slots. ComfyUI's allocator memory counts as free on a route with one GPU. Everything else is charged as a load, and memory that evicting another idle model would free is never counted. If the observation was only too old, the refusal path observes the upstream once more before deciding. A refusal says why no credit applied, and /status shows residency for a route whose vacancy depends on one.

Owner return

Once owner policy refuses a host-scoped upstream, new work is refused in every reclaim mode. Resource-scoped upstreams are exempt from owner presence, as described below. on_owner_return = "drain" allows 30 seconds by default, then cancels the proxy connection. drain_timeout_s changes that bound. cancel closes it immediately; finish lets accepted work finish without an owner-return deadline.

A drain stands down if its trigger clears before the deadline, judged by the checks admission itself applies. For an owner-return reclaim, the owner veto has released, presence would not refuse the upstream (for a host-scoped upstream that includes complete session evidence), and every pressure-gated resource the upstream needs admits work. For a pressure reclaim, every GPU whose pressure started it has a fresh reading that is calibrated, below its threshold and not on its way to engaging, every other pressure-gated resource the upstream needs admits work, and for a host-scoped upstream the owner checks above pass too. "Not engaged" alone is not enough: after an unknown reading the owner's load may have been building unseen. Nothing is then cancelled, deleted, stopped or unloaded, the hold on the upstream ends as it does after a completed reclaim, and the log says reclaim abandoned; its trigger cleared with the reclaim id, the upstream and the elapsed time. A later owner return or pressure refusal starts a reclaim of its own with its own deadline. The wait in finish mode stands down the same way. Cancel mode has no wait, so it never stands down. A proxied response waiting out the same drain follows the decision of the reclaim that answers its own owner or pressure edge, so its connection is cut exactly when that reclaim goes ahead, and kept when it stands down. If that reclaim never decides, the connection is cut 30 seconds after its own deadline.

Work that outlives its connection is cancelled at the upstream where the adapter can do it. For ComfyUI, vacancyd knows the prompt ID of every job it admitted, so at the same deadline it deletes those prompts from the pending queue (POST /queue with {"delete": [ids]}), reads the queue again, and stops one only if the queue shows it running. A deleted prompt, or one no longer in the queue, releases its reservation. A stopped prompt releases its reservation when its history shows a terminal result, the same evidence ordinary completion needs. Cancellation requests name only borrowed prompt IDs. ComfyUI 0.3.57 to 0.25.x still has an interrupt race with the next prompt; 0.26.0 and later cancels under the queue lock. A borrowed prompt that finishes inside the drain window is not stopped. If a cancellation request fails, the reservation stays and the log names the entry and the reason; unloading still goes ahead. Finish mode never cancels. Ollama, OpenAI-compatible and llama.cpp work is cancelled by closing the proxied response.

A submission still waiting for its reply at the deadline is not abandoned. Cancellation waits up to 2 seconds for such replies, then continues; a reply that arrives later is cancelled as soon as it is recorded. Each prompt is cancelled once, by whichever side sees its ID first. When the reply never arrived, absence from the queue does not show that the prompt stopped, because ComfyUI might still queue it: the reservation is released only if the queue showed the prompt before it was deleted, or its history shows it ended.

How a running prompt is stopped depends on the ComfyUI release, because older /interrupt requests stop whatever is running, which in the moment after a borrowed prompt ends is the owner's next one:

ComfyUI release How a running borrowed prompt is stopped
0.26.0 and later POST /api/jobs/<prompt_id>/cancel, which checks that the prompt is running and interrupts it under ComfyUI's queue lock, so no other prompt can be reached.
0.3.57 to 0.25.x POST /interrupt with that prompt_id. ComfyUI skips the interrupt unless the prompt is running, but checks outside its queue lock, which leaves a window of a few Python statements inside ComfyUI.
Before 0.3.57, or a version it does not report Not stopped. The prompt keeps running and keeps its reservation until its history shows it ended; the log says why. Its pending prompts are still deleted.

vacancyd tries the job cancel first and falls back only when ComfyUI answers that the route does not exist (404 or 405). It reads the release from system.comfyui_version in /system_stats.

The queue and history replies carry whole workflows, so an owner's long queue can run to tens of megabytes. vacancyd parses them as they arrive and keeps only prompt IDs and statuses, so memory does not grow with them. It reads at most 256 MiB from either. A read's time limits follow its progress rather than its size, so a large reply on a busy host is not cut short: ComfyUI must start replying within 60 seconds (it builds the whole reply before it sends any of it), may not fall silent for more than 10 seconds, and the whole read, parsing included, ends after 5 minutes. A read that exceeds any of these fails, and a cancellation that needed it keeps the reservation and logs the reason. Model observations count queued work with GET /prompt and never read the queue.

The requests that delete prompts or stop a running one are small, but ComfyUI's work to answer them is not: to stop one prompt, ComfyUI 0.37.2 copies the whole pending queue while holding its queue lock, which took about 40 seconds with 100 MiB of workflows pending. Each such request may take up to 2 minutes, and ComfyUI completes it even if vacancyd stops waiting. After a stop request, vacancyd checks the prompt's history for up to 10 seconds before the unload goes ahead; a prompt that stops later is released by its tracker when its history shows it ended.

When automatic unloading is enabled, it follows cancellation. Cancel mode requests it immediately. Drain mode requests it as soon as the affected work has ended, or at the deadline. Finish mode waits for affected work that is still being forwarded or tracked, for at most completion_timeout_s. Held work never delays unloading in any mode, because nothing is finishing it: it keeps its reservation, and a warning in the log gives the entry and the reason it could not be released.

Before it unloads ComfyUI, vacancyd reads the queue once more. If ComfyUI is running or holding any prompt vacancyd did not admit, such as one the owner submitted directly, the unload is skipped: ComfyUI applies /free after the prompt it is running and unloads every model, so the owner's next prompt would start from cold. vacancyd's own prompts are those admitted through any upstream that reaches the same server. The read stops at the first prompt that is not one of them and reads at most 16 MiB, enough for several large workflows ahead of it. The log says model unload skipped; the upstream holds work vacancyd did not admit, without prompt ids or contents, and the reclaim log records the skip. The unload goes ahead as before, with the reason logged, when that cannot be told: a submission to the server still awaits its reply or was never confirmed, or the queue cannot be read within that bound. Adapters that cannot list an upstream's work unload as before.

The same options apply when required GPU pressure becomes refusing or unavailable. Only work depending on that resource receives the reclaim event; unrelated resources continue lending. Engaged pressure and missing calibration start reclaim when observed. An unavailable reading, such as a failed attribution sample, refuses new work at once but reclaims accepted work only once the GPU has gone without a good sample for the pressure tracker's gap limit (three times owner.gpu_pressure.poll_ms, at least 20 seconds), counted from its last good sample. The gap allows for the polling interval and attribution read. At the default polling and attribution window, one missed sample is normally within the limit; slower reads can cross it. A good sample that ends a longer gap reclaims as it arrives, so runs of failures between samples cannot hide the owner's load. A missed sample does not reset how long the owner's load has been above the threshold, but it does restart the time pressure must stay quiet before it releases, since quiet that was not observed is not evidence. A reading that becomes engaged or uncalibrated while it is waiting out that time reclaims at once. The earliest owner or resource event fixes the drain deadline, even if later events arrive before the cancellation task runs. This applies independently of yield_on_veto, which controls model unloading rather than proxy cancellation.

Closing a proxy connection doesn’t guarantee that the upstream stopped. ComfyUI keeps queued work after its submission reply, which is why it is cancelled through its queue API; /free only unloads models. Completion tracking continues independently within its bound. Interrupted synchronous requests retain a hold because the connection alone cannot establish their outcome. With yield_on_veto = true, a reclaim under way holds its resources, which refuse new work until it ends, even if the owner leaves again. If its trigger clears during the wait, the reclaim stands down as described above; once cancellation or the unload has begun, it runs to completion. Either way, new work then waits for a fresh capacity sample. Without automatic unloading, a reclaim holds nothing: new work is refused only while the owner or pressure still refuses it.

Unloading acts on the whole server behind an upstream: Ollama unloads every model it has loaded, and ComfyUI's /free empties its allocator (which is why it is skipped while ComfyUI holds prompts vacancyd did not admit). Two upstreams whose targets reach the same server must therefore borrow the same resources, and configuration validation rejects them otherwise, as a conservative protection against server-wide unloading affecting the other route's work or resident models. Any loopback address and localhost count as one host, and only the host and port are compared. A server that spans several GPUs is one upstream: name one GPU as resource and the others in also_requires. Two upstreams may still reach one server through different dialects, such as Ollama's native and OpenAI-compatible routes, when both name the same resources. vacancyd cannot tell that a LAN address and a loopback address reach the same server, so point every upstream for one server at the same address.

Rejected or malformed unload responses are logged as failures. Ollama must acknowledge a completed unload request; ComfyUI acknowledges acceptance of a worker flag and does not report a model count. vacancyd_yields_total counts these acknowledgements, not recovered VRAM or completed jobs. Verify actual resource recovery from fresh capacity and upstream observations. An unload acknowledgement never clears an uncertain work reservation.

Resource-scoped upstreams

By default an upstream is host-scoped: the owner veto and incomplete session evidence refuse its new work and reclaim its accepted work, as described above. owner_scope = "resource" exempts one upstream from both, so it keeps lending while the owner is at the machine. It is the one exception to "vacancyd refuses borrowed work while the owner is present", and the default is "host". An administrator can change the exemption in the configuration or the console's advanced workload permissions; a service restart applies it. The running status remains authoritative until then. See settings permissions.

An exempt upstream still answers to everything that is not about the owner's presence: the lending switch, its resources' switches, their capacity and reserves, and GPU pressure. What reclaims its accepted work is narrower than what refuses its new work, because automatic reclaim is GPU-only:

  • NVIDIA GPU pressure (Windows, with owner.gpu_pressure.enabled) refuses new work and reclaims accepted work on the pressured GPU, including while the owner veto is active. At that point pressure reclaims only exempt upstreams, because the veto has already reclaimed the host-scoped ones. The reclaim follows the upstream's on_owner_return, drain_timeout_s and yield_on_veto, which for an exempt upstream apply only to pressure.
  • Memory and CPU are checked when a request is admitted: a low-memory or busy-CPU sample refuses the lane's next request, but nothing reclaims a request that is already running when the owner's workload later needs that memory or CPU.
  • A GPU without pressure (Linux, where attributed pressure is not implemented, or pressure disabled) is limited only by its free memory.

Pressure is judged against the attended or unattended baseline, so it is sampled only while presence is known. While the presence backend has no trustworthy answer, for example for a moment during a sign-in, the last pressure reading stands until it is stale (twice owner.gpu_pressure.poll_ms, at least 15 seconds). It then reads as unknown, which refuses an exempt upstream at once. Its gap limit counts from that last good sample, so accepted work is reclaimed once the limit is reached, like any unavailable pressure reading, until presence and sampling resume.

Use it for an always-on lane on a card the owner's workload does not use, such as a small helper model on a display GPU while a game runs on another. Calibrate that card's pressure for its normal desktop load. Keep the owner's workload off the lane's other dependencies, such as system memory on a machine that games in memory: nothing measures the owner's use of them, so the lane keeps its accepted work while the owner competes for it.

An exempt upstream needs its own server. Owner-return reclaim can affect both routes on that server, depending on adapter and policy, so configuration validation refuses an exempt upstream whose target host and port match a host-scoped upstream's (any loopback address, 0.0.0.0 or localhost counts as the same host), even when both borrow the same resources. Two upstreams, exempt or not, can still share a resource. Shared-resource admission still applies. While owner-return reclaim holds a host-scoped upstream's shared resource, the exempt upstream gets 503 resource_busy until that hold ends and a fresh capacity sample arrives.

The exemption is visible wherever the veto is:

  • /status reports each upstream's owner_scope and lists the exempt ones in owner_veto.exempt_upstreams. While owner_veto.active is true, only the other upstreams are paused, and each exempt upstream's vacancy says whether it is admitting. See the status contract.
  • vacancyd status says "paused for host-scoped upstreams only" and names the exempt upstreams still lending, and marks each exempt upstream's line.
  • The owner console says Only exempt routes are lending instead of Owner policy pauses new work while an exempt route admits or holds work, names the exempt routes on the overview, and explains on each exempt route's card what can still stop it. The tray says the same.
  • The service logs the exempt upstreams once at startup, and vacancyd check-config prints the same line. It warns about any exempt upstream with a dependency that no pressure measurement watches. The vacancyd_upstream_owner_veto_exempt metric is 1 for each exempt upstream.

Recovering a held reservation

/status and the console's Workloads page show the entry ID, upstream, model when known, resources, age, completion method and current phase. forwarding and tracking mean the daemon still owns an active operation. held means completion is uncertain and needs attention. vacancyd status prints a line for each held entry with its ID, age, upstream and resources.

Stop or verify completion of the job at the upstream first. Then an administrator can send POST /admin/work/<id>/release with JSON {"upstream_stopped": true} and the same authentication and intent header used by the lending controls. A false confirmation, missing intent or insufficient scope is rejected. Active forwarding or tracking cannot be released this way. Releasing a held entry still requires fresh capacity samples before reuse. The owner console can pause and resume lending, switch resources and save settings (see console controls), but it has no control for releasing held work. Use this request, or vacancyd admin-request with a release_work action run as an administrator (see local administration on Windows).

Reservations are saved before forwarding. After a daemon restart, unresolved records return as held entries; restarting is not a way to release work. A ComfyUI entry keeps its prompt ID, so the prompt can be found at the upstream and, at a later owner return, cancelled. When ComfyUI reports its own ID for the prompt (releases before 0.3.45), the record is rewritten with that ID: the new record is written beside the old one and renamed over it, so a crash leaves one or the other, and a complete new record left beside the old one is completed at the next start. If the record cannot be rewritten, tracking continues with ComfyUI's ID, the reservation still survives a crash, and the log names both IDs. Keep the recovery directory through upgrades, rollback and uninstall. Do not delete it to clear a busy resource. A corrupt record or changed resource mapping stops startup so the old work cannot silently disappear. The one exception is the newest record when a crash cut it short while it was being saved: that request had not been forwarded yet, so the partial record is discarded with a warning in the log.

server.admission_state_dir selects a private recovery directory. By default it is beside the configuration with the .admission extension. Under the supplied systemd unit, it uses $STATE_DIRECTORY/admission, normally /var/lib/vacancyd/admission. A Linux run started without the service manager, such as sudo vacancyd run, has no $STATE_DIRECTORY and uses the location beside the configuration. While the setting is empty, such a run checks the service's recovery directory, /var/lib/vacancyd/admission. If that directory exists and holds records of unfinished or held work, the run refuses to start and names the admission_state_dir line that makes every start use those records. If the directory exists without records or cannot be read, the run logs a warning that the two locations differ and starts. If the directory does not exist, this check says nothing. A relative value resolves against the configuration's directory. vacancyd creates only that default location on first start, including any missing parents, after checking the path for replacement or redirection by untrusted accounts. A failed path check stops startup. This applies whether the setting is empty or names the default. Any other directory the setting names must already exist, or startup stops with an error naming the path: a moved configuration, a changed setting or a typo would otherwise start with an empty ledger and forget the held work recorded in the old place. For a new installation, create such a directory empty and private to the account the service runs as. New directories are private from creation, and vacancyd never changes an existing recovery directory's permissions or owner. On Linux an existing directory must be owned by the account the service runs as and have mode 0700; on Windows it must already be owned by and accessible only to Administrators and LocalSystem. Otherwise startup stops with an error naming the path. On Windows the recovery directory, the default or one the setting names, must also be inside ProgramData\vacancyd, with no junction or other reparse point on the way and no folder from ProgramData\vacancyd down giving access to another account; a configuration elsewhere therefore needs admission_state_dir set to a directory there. Only one daemon may open a recovery directory. It contains job metadata, never prompts, responses or credentials. Age after a restart is reconstructed from the recorded start time and the system clock.

Borrowing clients must use the proxy.

Requests sent directly to an upstream do not acquire a reservation. Queue and slot observations can refuse visible external work, but they cannot make direct access and proxy admission atomic.

Protocol references

The adapter behavior follows the Ollama running-model API, ComfyUI server routes and llama.cpp slot API. Unknown or malformed observations never count as successful completion. Observation and tracking requests do not forward borrower credentials. If these endpoints require authentication, configure [upstream.credential]; without a usable credential, their observations remain unavailable and tracked work stays held.