feat: job engine#2718
Conversation
fl0rianr
left a comment
There was a problem hiding this comment.
Thanks for putting this together, I'm all for this! The overall direction is strong, and the separation between job definitions, operation registry, expression handling, persistence, and forward-only control flow looks promising.
However, I found several issues that should be addressed before merge:
-
GET /jobs is shadowed by the automatic 405 handler.
register_post("jobs", ...)registers a GET 405 route beforeregister_get("jobs", ...). Since cpp-httplib uses the first matching handler, the list endpoint likely always returns 405. Please excludejobsfrom the automatic GET fallback or register GET first, and add an integration test for listing jobs. -
Deleting an active job skips cleanup.
remove()immediately erases the active job fromjobs_. Afterexecute()returns, the worker can no longer observe anInterruptedstate, soreconcile_unload()is never called. A running exclusive job can therefore be deleted while leaving its loaded model behind. Active jobs should be marked for deletion and only removed after interrupt handling and cleanup complete. -
Chat interruption is not actually immediate.
The chat op ignores the cancellation flag and synchronously callsrouter_->chat_completion(). The job only becomes interrupted after the chat request returns. This does not match the documented “cancel the current step now” behavior and can hold the exclusive slot for a long time. -
Interrupt cleanup unloads every model.
reconcile_unloadcallsrouter_->unload_model(""), so interrupting any exclusive job unloads all resident models, including models that were loaded before the job or pinned. A chat-only job can also unload the model it needs when resumed. Cleanup should restore the pre-job state or track only models loaded by the job. -
The exclusive load gate has a race.
A normal load can passwait_for_slot_clearance(), then wait onis_loading_. If an exclusive job starts while it is waiting, the load resumes without checking exclusivity again and can run inside the supposedly exclusive job. Both conditions need to be checked together or exclusivity must be rechecked after everyload_cv_wake-up. -
The nuclear retry is not cancellable.
The initialnew_serverreceives the load cancel flag, butretry_serverdoes not. Interrupting during the retry therefore cannot stop backend startup. -
Expression validation only tokenizes.
validate_steps()callstokenize()but does not parse the expression. Invalid conditions such as1 +,(true, or1 < 2 < 3are accepted at creation and only fail during execution. Creation-time validation should run the full parser, while deferring only reference-path resolution. -
The new tests are not run by CI.
CMake definesJobExprTestandJobGraphTest, but the workflow builds a fixed list of targets and uses a restrictedctest -Rexpression that excludes both.test/server_jobs.pyis also not integrated. The current workflow runs are stillaction_required, so there is no actual CI signal yet.
Smaller follow-ups:
id_counter_is not restored after restart, so IDs can collide within the same second.- Branch-skipped steps remain
pending, which makes completed-job progress misleading. - Step IDs such as
inputsor IDs containing dots can collide with or become inaccessible through the context reference syntax.
I would currently request changes. The architecture is promising, but the lifecycle, cancellation, exclusivity, and cleanup guarantees need to be made reliable and covered by CI before this becomes a safe server-side execution engine.
|
Aight, fixes are up. |
fl0rianr
left a comment
There was a problem hiding this comment.
Thanks — this is significantly improved, and most of the original review points appear to be addressed correctly.
In particular, the following now look fixed:
GET /jobsis no longer shadowed by the automatic 405 handler.- Active job deletion is deferred until interrupt handling and cleanup complete.
- Chat cancellation is passed through Router,
WrappedServer, and curl. - The exclusive-load race is addressed with a combined wait condition.
- The nuclear retry receives the cancellation flag.
- Expressions are now fully parsed during validation.
- Reserved/inaccessible step IDs and misleading branch progress are handled.
- The new C++ unit tests are included in CI.
I still found two functional issues and one important test gap:
-
Job persistence is likely broken on Windows after the first write.
persist_locked()writesjobs.json.tmpand then directly callsstd::filesystem::rename(tmp, jobs.json). On Windows,rename()does not reliably replace an existing destination file.The first persistence may work, but subsequent status updates can fail once
jobs.jsonalready exists. This could leave running, paused, interrupted, completed, resumed, or deleted job states stale on disk while everything still appears correct in memory.Please use a platform-safe atomic replacement mechanism, ideally the same helper already used elsewhere in the repository, and add a test that performs multiple consecutive persistence writes on Windows.
-
A model loaded by the job with
pinned: trueleaks after interrupt or delete.The new reconciliation correctly snapshots the models that existed before the job and unloads newly introduced models afterward. However,
unload_models_not_in()skips every currently pinned model unconditionally.A job can therefore:
- load a new model with
pinned: true, - be interrupted or deleted,
- and leave that model resident permanently because reconciliation skips it.
Pinning performed by the job itself should not bypass cleanup. Only models that were already present before the job should be preserved. The snapshot may need to include the original pin state, or reconciliation should otherwise track which models the job introduced.
Please add an integration test that loads a model with
pinned: true, waits in a later step, deletes the active job, and verifies that the model is unloaded. - load a new model with
-
The server-level job tests still do not appear to run in CI.
test/server_jobs.pynow contains valuable regression coverage for listing, active deletion, exclusive cleanup, pre-existing model preservation, queueing, and restart persistence. However, the workflow still appears to run onlyJobExprTestandJobGraphTest.These integration tests are exactly what would catch the remaining lifecycle and persistence issues, so they should be included in an appropriate server CI job.
One smaller safety improvement: a successfully loaded WrappedServer still retains the raw load-cancellation pointer after startup. It should be reset to nullptr immediately after the load attempt, ideally with a scope guard, so the long-lived server never stores a pointer to a job control object that may later be deleted.
This is much closer now, but I would still request changes for the Windows persistence behavior and the pinned-model cleanup leak, with the server integration tests added to CI.
1961fe0 to
2eaaebe
Compare
|
Aight, should be good now. |
fl0rianr
left a comment
There was a problem hiding this comment.
Thanks — the previous review findings are now largely addressed. The Windows persistence path uses a platform-safe replacement helper, pinned models introduced by the job are cleaned up correctly, load cancellation pointers are reset, and server_jobs.py is now integrated into CI.
I am treating the current CI failure as unrelated, as noted.
After reviewing the current head again, I still found several issues:
-
A late load interrupt can still unload all pre-existing models.
After
router_->load_model(...)returns successfully, the job load provider checks the cancellation flag again and calls:router_->unload_model("");
if cancellation arrived in that small window.
An empty model name unloads every resident model, not only the one loaded by the job. The later snapshot reconciliation cannot restore models that were already removed.
Please remove the global unload from this path. Returning an interrupted error and letting the snapshot-based reconciliation clean up the job-introduced model should be sufficient.
-
Deleting an active job is not durable across a crash.
Active deletion currently sets only in-memory
interrupt_requested,cancel, anddelete_requestedflags and immediately responds with{"status":"deleted"}. The job is removed from persistence only after its running operation and cleanup finish.If the server crashes after acknowledging the DELETE but before that final removal, the job is recovered after restart as
interruptedand becomes visible again.The delete request should be persisted as a tombstone or durable state before acknowledging success. A restart after an acknowledged deletion must not resurrect the job.
-
Pause and interrupt do not take effect immediately for queued jobs.
Both operations accept jobs in the
queuedstate, but they only set control flags. The job remains queued until it reaches the worker, is briefly changed torunning, and only then becomes paused or interrupted.This can leave the API reporting
queuedfor a long time after returningpausingorinterrupting, and the job cannot be resumed during that period.For queued jobs, pause or interrupt should remove the job from
queue_, update and persist its status immediately, and let resume enqueue it again. -
The exclusive gate is not fully exclusive.
begin_exclusive()waits for an active load to finish, but it does not wait for already-running inference requests. An external chat that passed the gate just before the job starts can therefore overlap with the supposedly exclusive benchmark job.Some model-touching Router paths, including
tokenize()andslots_action(), also do not callwait_for_slot_clearance()at all and can run during an exclusive job.All model/backend operations should use the same gate, and beginning an exclusive session should wait for existing in-flight operations to drain.
-
The pre-job model snapshot cannot restore models evicted by a job load.
The snapshot stores only loaded model names. If a job load evicts a pre-existing model because of the slot limit, reconciliation removes the job-loaded model but cannot reload the evicted baseline model.
It also cannot restore changed options or pin states of a model that existed before the job.
This conflicts with the documentation that interrupted jobs leave pre-existing models untouched. Either the snapshot needs enough information to restore the previous Router state, or the documented guarantee should be weakened explicitly.
-
The documented 50-job cap is not actually enforced.
When the history exceeds 50 entries, only completed or failed jobs are eligible for eviction. If all existing jobs are queued, paused, interrupted, or running, creation continues and the collection can grow without limit.
If no terminal entry can be evicted, new job creation should be rejected with a clear status such as 429 or 409, or active and historical limits should be documented separately.
-
Completed recovery jobs can report incomplete progress.
Progress counts only
completedandskippedsteps. A failed step handled throughon_fail: continueor a recovery branch remainsfailed, even when the overall job later completes successfully.A completed two-step recovery job can therefore report only one completed step. Handled failed steps should count as terminal progress, or progress should expose separate processed/succeeded counts.
The implementation is much closer now, and the earlier issues were addressed well. I would still request changes for the late global unload, crash-durable active deletion, queued control handling, and incomplete exclusivity before merge.
fl0rianr
left a comment
There was a problem hiding this comment.
Thanks — after another pass over the current head, this is now very close.
I still see one lifecycle issue before I would consider this fully merge-ready:
A job can lose ownership of models it introduced across pause/resume.
Example:
- An exclusive job loads model A.
- The job is paused during a later step. The exclusive gate is released, but A remains loaded.
- On resume, a new exclusive snapshot is taken. A is now included in that snapshot as though it predated the job.
- If the resumed job is then interrupted or deleted, reconciliation preserves A instead of cleaning it up.
There is a related case when deleting the job while it is paused: because it is no longer the active job, it is removed directly and no reconciliation runs, so a model introduced before the pause can remain resident.
This conflicts with the current lifecycle wording that interrupted or deleted exclusive jobs clean up models they introduced.
Please either:
- preserve model ownership across pause/resume and reconcile it when the job is later interrupted or deleted, or
- explicitly define pause as committing all model-side effects up to that point, document that semantic, and add tests covering pause → resume → interrupt and deletion while paused.
Apart from that issue, I do not see another code blocker. Once it is addressed and the very simple conflict is resolved (main at a C++ unitest file update) additively, I would consider the PR merge-ready, assuming the job-engine tests pass on the updated head. The currently known unrelated CI failure should not block it.
fl0rianr
left a comment
There was a problem hiding this comment.
I re-reviewed the actual current head (4306f790) after the conflict resolution, focusing only on merge blockers and ignoring the known unrelated CI failure.
The previously reported pause/resume ownership issue is improved, but I still see two blocking lifecycle cases:
-
Cleanup can unload models introduced externally while the job is paused.
The job keeps its original model snapshot across pause/resume, while pausing releases the exclusive gate and allows normal model traffic.
Example:
- the job starts with no model and loads A,
- the job is paused,
- an external request loads B,
- the job is deleted while paused, or resumed and then interrupted.
Reconciliation compares the current model set against the original job snapshot and unloads everything not in that snapshot. This removes both job-owned A and externally loaded B.
Cleanup therefore needs explicit per-job ownership tracking, rather than treating every post-snapshot model as job-owned.
-
An interrupted job may no longer be resumable after cleanup.
Example:
load A -> chat using AIf the chat step is interrupted:
- the chat step is reset to
pending, - reconciliation unloads A because the job introduced it,
- resume starts again at the chat step,
- the chat fails because the completed load step is not replayed.
The same applies when interruption happens during a later step after a successful load.
Resume must either restore the job-owned model state or rewind to a safe step that reloads the required model.
- the chat step is reset to
These affect external model residency and the core interrupt/resume contract, so I consider them merge blockers. Apart from these two cases, I did not find another blocker in the current head. The known unrelated CI failure should not block this PR.
fl0rianr
left a comment
There was a problem hiding this comment.
Thanks, found only one remaining detail:
The model ownership and restore state is kept only in memory.
JobModelState lives in the server-local job_states map and is not serialized with the job record. After a server restart, active jobs are recovered as interrupted with their cursor intact, but the corresponding ownership and captured model configuration are gone.
Example:
load A -> chat using A
server crashes during chat
restart
resume
After restart:
- the job is restored as
interrupted, - the chat step remains pending,
job_statesis empty,restore_exclusive()has nothing to restore,- resume re-runs the chat step without model A loaded.
The existing persistence test uses only a sleep step, so it does not cover this model-dependent resume case.
Please either persist the job-owned model restore state with the job, or reconstruct it safely from the completed steps before resuming after restart. A regression test should cover load -> later step -> hard restart -> resume and verify that the dependent step completes successfully.
Apart from this restart/resume issue, I do not see another code blocker in the current head. The known unrelated CI issue should not block the PR.
fl0rianr
left a comment
There was a problem hiding this comment.
Nice work! Let's see if we can get through the CI trouble...
…ion evaluator, graph validation
Phase 1 of the generic job engine (own PR). Header-only and unit-tested with
no server dependency:
- job_types.h: Job/StepRecord/Case model with JSON round-tripping.
- job_expr.h: ${ref} resolution + the when/branch evaluator (references,
literals, comparisons, boolean logic, basic arithmetic; no arbitrary code).
- job_graph.h: step-graph validation — unique ids, known ops, forward-only
jump targets (forward-only ⇒ acyclic ⇒ no loops).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Z9WMqprhqjWEDuv1gnGaH
Document the when/branch condition grammar and ${ref} resolution: paths,
type-preserving vs interpolating params, literals, operator precedence,
truthiness, comparison/arithmetic semantics, errors, the non-short-circuit
caveat, and worked examples. EBNF included.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Z9WMqprhqjWEDuv1gnGaH
Add the generic job engine on top of the Phase 1 pure core: a single persistent worker runs one client-posted job at a time, passing data forward through the job context, evaluating forward-only branches, and persisting every transition to <cache_dir>/jobs.json (atomic temp+rename, 50-job cap). A restart marks in-flight jobs interrupted but resumable from their cursor. - OpHandler/OpRegistry plus read-only ops (system_info, system_stats, models, sleep), built from injected providers so job_ops does not depend on the whole Server. - JobManager lifecycle: create/list/get/pause/interrupt/resume/remove. - Seven quad-prefixed HTTP handlers; path-param routes registered manually across /api/v0,/api/v1,/v0,/v1. - test/server_jobs.py drives the full lifecycle end-to-end with an isolated lemond (no model load), including kill-and-restart persistence recovery. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Z9WMqprhqjWEDuv1gnGaH
INFO on create/run/complete/pause/interrupt/resume/delete and startup recovery; WARN with the failure reason on on_fail continue/goto and on persist/load errors; ERROR on job failure; DEBUG for per-step running/completed/skipped. Category 'Jobs'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Z9WMqprhqjWEDuv1gnGaH
Add the load/unload/chat exclusive ops and a Router-level exclusive gate keyed by owner thread so a running job queues all normal load/inference traffic behind it, plus cancel-of-in-flight-load. - Router: exclusive_active_/exclusive_owner_/exclusive_cv_ under load_mutex_; begin_exclusive/end_exclusive and wait_for_slot_clearance inserted at load_model, unload_model, execute_inference, execute_streaming. - WrappedServer::set_load_cancel_flag + wait_for_ready abort-and-kill; load_model threads a cancel flag and skips the nuclear retry on cancel. - OpProviders/OpRegistry gain load/unload/chat callbacks and begin/end exclusive; JobManager holds the slot for any job with an exclusive step, releasing it exactly once via a guard on every exit path. - server_jobs.py: real load->chat->unload job (asserts timings/usage), queue-behind-job timing test, interrupt-mid-job clean-release test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Z9WMqprhqjWEDuv1gnGaH
An interrupted exclusive job abandons its run and could leave a model loaded. Reconcile by unloading (router unload-all) while the worker still owns the slot, before releasing the gate. Test asserts /health reports no model loaded after interrupt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Z9WMqprhqjWEDuv1gnGaH
Post the AutoOpt methodology as a job: two configs, each unload->load(args) ->timed chat->unload, extract timings into context, then branch on the measured tps to pick a winner. Proves the engine runs the real bench recipe end-to-end with no engine changes. Also asserts no model left resident. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Z9WMqprhqjWEDuv1gnGaH
…ase 5) Add docs/dev/job-system.md (concepts, ops, recipe schema, control flow, lifecycle, exclusivity/queuing, persistence, API, bench example); a Job Engine API section in docs/api/lemonade.md; and the jobs endpoints in AGENTS.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Z9WMqprhqjWEDuv1gnGaH
Strip all comments from the job-engine sources and tests, and remove the comments added to the shared router/wrapped_server/server files. The concepts they described are documented in docs/dev/job-system.md and docs/dev/job-expression-language.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Z9WMqprhqjWEDuv1gnGaH
- GET /jobs no longer 405s: exclude jobs from the POST GET-fallback - delete of an active job defers erase until the worker finishes cleanup (reconcile unload), so exclusive jobs never leak a resident model - chat interrupt is now a genuine in-flight HTTP abort (curl XFERINFO) threaded via chat_op -> Router::chat_completion -> forward_request - interrupt cleanup unloads only models the job loaded: exclusive session snapshots the loaded set at begin and reconcile unloads (current - snapshot), leaving pre-existing and pinned models - load gate uses one combined predicate wait on load_cv_ so an exclusive session that begins mid-wait cannot be bypassed; begin/end_exclusive notify it - nuclear retry sets the load cancel flag so interrupt aborts backend startup - expression validation runs the full parser (syntax-only) to reject malformed when/branch expressions at creation, deferring ref/type errors - id_counter restored to max parsed suffix on load; pending steps marked skipped at completion; step ids reject 'inputs' and '.' - CI builds and runs JobExprTest/JobGraphTest and server_jobs.py Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Z9WMqprhqjWEDuv1gnGaH
…lusive snapshot Two concurrency defects found by adversarial review of the review-fix commit: - The chat cancel flag lived on a shared per-server member, so a concurrent non-job chat to the same model adopted the job's flag (aborted on interrupt) and the RAII reset raced an HTTP thread reading the non-atomic pointer. Move it to a per-thread (thread_local) pointer: each request thread carries its own, no sharing, no race. - begin_exclusive set the gate but did not drain an in-flight concurrent load, so a model that finished loading just after the job claimed the slot was missing from the snapshot and got evicted by the interrupt reconcile. Wait for is_loading_ to clear before returning so the snapshot is complete. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Z9WMqprhqjWEDuv1gnGaH
…cleanup, CI - persist jobs.json via atomic_replace_file (MoveFileExW on Windows, rename on POSIX, copy+remove cross-device fallback) - reconcile-unload now evicts job-introduced models even if pinned; only pre-job snapshot models are preserved - clear load_cancel pointer after every load via RAII guard - run server_jobs.py in the Linux PR CI job - tests: multi-write persistence, pinned-model delete/interrupt cleanup, preexisting-pinned survival Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Z9WMqprhqjWEDuv1gnGaH
Adversarial review of the round-2 fixes found: - LoadCancelGuard (function scope) wrote set_load_cancel_flag(nullptr) into a WrappedServer already freed during exception unwinding on every load-failure path (file-not-found, cancelled, retry failure) — a UAF write on a hot error path. Clear the flag inline while each server is still alive, right after its load attempt; drop the guard. - persist_locked promoted the temp without checking the ofstream, so a partial write (disk full) could atomically replace a good jobs.json with truncated JSON. Verify the stream succeeded before replacing; drop the temp otherwise. - atomic_replace_file did not propagate the copy-fallback error_code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Z9WMqprhqjWEDuv1gnGaH
Removed source section from job-system documentation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Z9WMqprhqjWEDuv1gnGaH
… full exclusivity, scoped cleanup, cap, progress - load op: drop the late-cancel global unload; snapshot reconciliation cleans up - active-job DELETE persists a tombstone before the ack; restarts drop tombstoned jobs and they are invisible to GET/list immediately - pause/interrupt on a queued job dequeues and persists the new status immediately instead of waiting for the worker - begin_exclusive drains in-flight requests after closing the gate; tokenize, slots, get_slots, and set_model_pinned now check the same gate - the exclusive snapshot records pin state and reconcile restores it on surviving pre-job models; the docs state the preserve-if-still-resident guarantee explicitly (evicted models are not reloaded) - POST jobs returns 429 when the 50-job store has nothing terminal to evict - summary progress counts handled failures (on_fail continue/goto) as processed Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Z9WMqprhqjWEDuv1gnGaH
…ct, cancellable drain, tombstone scrub, test hardening - the idle-eviction/downsize engine suspends during exclusive sessions, and evict_if_committed rescues a model that got (re-)pinned or caught by an exclusive session after its EVICTING mark — fixes the reconcile-repin TOCTOU - begin_exclusive(cancel) polls the job's cancel flag while draining, so interrupt/delete/shutdown abort the acquisition instead of blocking behind a long-running stream; an aborted acquisition marks the job interrupted and never runs a stale-snapshot reconcile - the job cap counts only live jobs (a tombstoned active job no longer 429s creation), and startup scrubs tombstoned payloads from jobs.json - tests: pin-restore of a surviving pre-job model is now asserted end-to-end; the durable-delete test asserts the tombstone on disk before the crash; the drain test tolerates client-read lag; real-backend tests self-install the cpu backend once and seed it into each test cache so they run in CI Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Z9WMqprhqjWEDuv1gnGaH
…pdate in place - load_from_disk closes jobs.json before the tombstone-scrub persist, so the Windows replace does not hit a sharing violation on the startup it targets - 'pinned' no longer participates in the option-change reload diff: it never reaches the subprocess command line, so a pin-only load updates the pin in place instead of evicting and reloading the model - the pin-restore test asserts PID stability so it fails if the pre-job model is reloaded rather than surviving; the one-time backend install records failed attempts and handles request timeouts instead of retrying per test Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Z9WMqprhqjWEDuv1gnGaH
set_pinned now updates recipe_options_ alongside the pinned_ flag (guarded by state_mutex_, since recipe_options_ is no longer immutable after publish), so /health no longer self-contradicts after an in-place pin update and a *_bin hot-swap reload cannot resurrect a cleared pin from stale stored options. Also fixes the same pre-existing mismatch in the set_model_pinned endpoint. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Z9WMqprhqjWEDuv1gnGaH
…used reconciles The exclusive snapshot is now per-job and captured once, the first time the job acquires the slot — a pause/resume cycle no longer re-baselines it, so a model the job loaded before pausing is still recognized as job-introduced and cleaned up on a later interrupt or delete. Deleting a paused or interrupted job routes through the worker for the same snapshot reconcile instead of being erased without cleanup. Jobs that end completed/failed commit their model side-effects (snapshot discarded). Reconcile is keyed by job id and is a no-op for jobs that never held the slot. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Z9WMqprhqjWEDuv1gnGaH
…loads set_pinned records the option only for a genuine pin and erases it otherwise — an absent key already means unpinned, so the flag/options consistency for the *_bin hot-swap is preserved while auto-load keeps its request-scoped-field contract (server_endpoints test_013, test_ollama test_015a). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Z9WMqprhqjWEDuv1gnGaH
…al residency Reconcile no longer treats every post-snapshot model as job-owned. Ownership is explicit and scoped to the specific residency the job created: the load op records the backend instance (pid) it produced, the unload op relinquishes ownership, and reconcile unloads a resident model only when it is that same instance — so external loads of any model, including the same name while the job is paused or interrupted and the gate is open, are never touched by job cleanup. Pre-job snapshot membership still takes precedence (residents preserved, pin state restored). Interrupt cleanup records each owned instance's options and pin before unloading, and resume reloads them (cancellable) before re-running from the cursor, so a step that depends on an earlier completed load — e.g. a chat interrupted mid-generation — finds its model again. If an external client re-loaded the same model meanwhile, the restore adopts that residency instead of replacing it and the model stops being job-owned. A failed restore logs and lets the dependent step fail with its normal on_fail semantics. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Z9WMqprhqjWEDuv1gnGaH
…estart resume Ownership and captured restore state are in-memory, so a hard restart lost them: a job recovered as interrupted would resume its pending step without the model an earlier completed load had established. Resume now also receives a manifest reconstructed from the job's persisted record — completed load and unload steps in execution order (forward-only, so list order), with parameters resolved against the persisted context. In-memory captured entries take precedence when present; manifest leftovers are loaded, or adopted if an external client already loaded the model. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Z9WMqprhqjWEDuv1gnGaH
|
Yaaaay! |
This is the backend powering the #2613 llama.cpp AutoOpt (#2696 ), but not only - figured it would be useful to have an actual server-side job module, with more complex sequences of tasks that can be queued on the server. Currently, it's impossible to do something like this without either (a) hardcoding the sequence in the backend, which is inflexible and requires updating the backend every time we want to add a new sequence or (b) driving everything from the frontend, which is not very versatile.
For now, the engine, language governing the jobs etc. are kept pretty minimal (bare sequences with simple if/then/else branching).