Imported from zeroaltitude/theseus (
crates/theseus-kernel/AGENTS.md). Install upstream withnpx skills add zeroaltitude/theseus --skill theseus-kernel. Copyright stays with the author.
theseus-kernel
The durable kernel (spec §3.2a, §3.15, §3.16; Part II M2): every state transition of executions and actions, written
as WAL frames through the Store contract. Synchronous and deterministic. Read by theseus-core, theseus-discord,
theseusd, and theseus-sim.
Key modules: kernel.rs, tx.rs, locks.rs, job.rs, children.rs, outbox.rs. Read by: core, discord, theseusd, sim.
What's here
kernel.rs: the transitions.Kernel::viewandturn_ofgive a turn its view;observetakes the push's one observer.types.rs: the durable objects (executions, actions, completions, budgets, wakes).locks.rs: one writer at a time per execution (theseus-id9).tx.rs: the kernel transaction (Kernel::frame, theseus-0owd): several transitions staged, then one frame.job.rs: the job wrapper (detached, durable, cancellable),job::Stopping,holder, andwrapper_alive. Since M4 18a a wrapper catches SIGTERM: the daemon's cancel asks it alone (ask_to_stop, bysigqueue, the grace in the signal's value), and it stops its whole tree, writes its verdict to the spool'sstops/, and exits with no completion.Stoppingtells it from a wrapper from before 18a by/proc/<pid>/status'sSigCgt(catches_sigterm) and stops an older one by its process group, as before (verified_by: group).job_wait.rs(Tier 7.1): the wrapper's wait on its command, asleep until something happens: the command's pidfd (an L1 job's init's), and a wake pipe its SIGTERM and SIGCHLD handlers write a byte to, polled with the time left before the deadline. It looked every 20 ms before. A spooled completion is taken (Kernel::take_completion_with): the drain and the turn waiting on the job both read it, and the second finds it settled and writes nothing.tree.rs(18a): a job's process tree, found through each task'schildrenfile, and stopped in three phases: SIGTERM to every process, the grace, the freeze (SIGSTOP, rescanning until nothing new appears and all read stopped), then SIGKILL and the reap. Each process is signalled through a pidfd checked against its start time.cancels.rs: a cancel's steps on one action (cancel_acknowledged, thencancel_verified,_unsupported, or_uncertain), each settle with itsVerdict(ACTION schema 3).job_l1.rs(M4 17b): the wrapper's L1 path, whenWrapperArgs.sandboxis set: the command belowtheseus_sandbox's init, its scratch summary, and its stop by the init's pid namespace; andjob::self_test,/bin/truestarted the same way, whichtheseusd checkruns on demand (theseus-gyin). It never falls back to L0.job_egress.rs(18c): for a job whoseL1.egressis not empty, the listener in its namespace, the proxy's variables, the proxy on the wrapper's threads, stopped once the job has ended (a stop and a deadline included), and itsSummaryindetail.egress. A job with no list gets none of it.mcp_l1.rs(M7 43a): an MCP server in L1,theseusd mcp-sandbox: itsServer(argv, environment, cwd, more read-only binds, the job'sL1view) rides inTHESEUS_MCP_L1; it hands its own stdin, stdout, and stderr to the init and keeps no end of the pipes, runsjob_egress's proxy for a server with a list, and waits on the init's pidfd, a signalfd (SIGTERM, SIGINT, SIGHUP: SIGTERM to the init, a 2 s grace, then SIGKILL), and the daemon's pidfd (itskill -9: SIGKILL to the init at once).children.rs: the daemon's children: what it spawned, what it adopted, and who reaps each. Job wrappers and tenders (the index tender, row 51) are reaped by their pids, a tender's exit reported to its supervisor; anopis left to tokio; anything else is an orphan.outbox.rs: posts that must reach a channel, as actions of their own record kind,OUTBOX.spool.rs(completions on disk),redact.rs(granted secrets withheld from a job's output),stops.rs(the soft stop),tasks.rs(task executions and their carve),wakes.rs,repeat.rs(a repeating wake's series: its span, days, anduntil, by jiff's zoned arithmetic inKernelConfig::zone; 37a),gate.rs(a confirmation's proposal and its digest),clock.rs, andumask.rs.
Invariants
- One frame per mutating method, or per transaction: the records that change, plus the ledger rows that describe the change. A crash between two frames leaves a state some earlier call produced, never one no call produces.
- Several transitions in one frame are a transaction, never a new combined transition (
Kernel::frame):kernel.frame(&[ids], |k| { k.bind_confirm(..)?; k.wake(..)?; Ok(()) }). It locks the executions named and their parents first, in id order. The transitions called onkstage their records and read what was staged, andk.stageadds the caller's own (a node, a row).Okcommits one frame, observed once;Errwrites nothing. Aframeinside one joins it, and its failure takes back only its own part. The_withfamily,admit_input,plan_and_dispatch, andauthorize_and_dispatchare such compositions. - The lock. A transition that reads an execution, or one of its actions, and writes it back holds that
execution's lock from the read until its frame is indexed. Never call a transition that locks an execution
this thread holds: it panics ("locked twice on one thread"), and inside a transaction, so does one of an
execution it did not name. Compose in a transaction instead, as
mark_unknowndoes. Several executions:Kernel::lock, in id order; a task and its parent:lock_family. Readers that write nothing take no lock. A transition's commit waits for the store's writer on the thread that holds its locks, and a wait for a lock another thread holds runs intheseus_store::blocking: neither holds a runtime worker (theseus-vni9). A lock is its thread's, soExecLockis!Send(Review 2's R7), and a build-time check beside it fails the build if it ever becomesSend. - Lock order is always the session, then the execution, and no kernel transition takes a session's lock.
- Time is injected (
Clock:RealClock,VirtualClock). The kernel never reads the wall clock. - Every child goes through
children::spawn. A child spawned another way, and waited for, can be reaped by the sweep as an orphan, and itswait()fails withECHILD. Never callwaitpid(-1). - A wrapper's pid can be reused, so "alive" is
wrapper_alive(pid, job), whose command line names the job. A process in the middle of its exec has an empty command line:holdercounts it as still starting (Item 35). - A cancel's verdict says how it knows (M4 18a):
termination_verifiedonly with a means (pidns,tree,group,task;cgroupis read from old records alone), andverified_by: nonefor a call nothing can stop. A cancelled job writes no completion, so a cancel never races the drain into afailedsettle; its verdict isstops/<id>. A deadline uses the cancel's stop, and its verdict rides in the completion'sdetail.stop. A SIGTERM that is not a cancel (noSI_QUEUE) still ends the wrapper by the signal once its tree is stopped (theseus-6uo). - A stop is not a cancel. A cancel is terminal;
stop_executionhalts the work and keeps the conversation (Item 9). A cancel settles every action that was never dispatched, in its own frame (Item 17). The results of the calls a cancel ended are the core's sweep (ToolRuntime::answer_after_cancel, theseus-0o8): it runs underKernel::frame, and only onceholds_turnis false, since a running turn owns its transcript. stop_callstops one running call and leaves its execution as it is (theseus-ht82): the daemon stopping a job below the disk's floor. A job the spool says runs isSpool::running(its pid file) orwrapper_lives(the pid file, or the lingering marker of a wrapper whose command has exited and whose child holds the output open).- A task never waits on input with nothing to wake it (37b, theseus-7kg). No one gives a task input: it waits on
input only beside a wake of its own (the core parks it so,
task::parks_on_wake), and a frame that would leave it waiting with none queues it instead (wakes::task_unparked, incancel_wakeandend_turn), so its next turn finds nothing new, ends it, and it reports. A/stopstill leaves a task waiting on input, as it leaves a conversation. - An attempt that may have run is
OutcomeUnknown, never "not sent". - Read by state, never every record (theseus-lv2). The store's index keeps
terms.rs's terms for each execution and action (s:<state>,due,x:<execution>, …). A reader on a path that runs often (the start, the driver's tick, the reconcile, health, a stop) asksexecutions_by/actions_byfor its terms; only the listings that show every one callexecutions()oractions(). A change to what a term means renamesterms::PROJECTION, so every store builds its terms again once. kernel-sim'scheck_termsholds every read by state to a full read.
Tests
- In
src/:tests.rs(whose fixtures run on aVirtualClock),tests_stops.rs,tests_tasks.rs,tests_tx.rs(the transaction),tests_wakes.rs, andtests_repeat.rs(a series: the re-arm, missed occurrences, a cancel,until, the cap, and both changes of offset of a year, in zones from POSIX TZ strings, so no tz database is read). tests_frames.rsis a golden: every frame a scripted run of the transitions commits, record by record, againsttests/golden/kernel_frames.txt. A refactor leaves it byte-identical;THESEUS_GOLDEN=writerewrites it, for a change you mean.tests/children.rsmakes its process a subreaper, so it is a test binary of its own: a sweep reaps any child of the process, other tests' included.tests/tree.rs(harness = false, 18a) re-execs itself as a job's wrapper, and as stand-ins for a wrapper from before 18a and a deaf one; each case ends with a/procscan for its ownsleepmarker.theseus-sim kernel-simdrives the kernel under seeded faults and races (--p-race; 0 is fully deterministic) and checks its invariants. A new transition belongs in its random operations.- Run this crate's tests as
cargo nextest run --workspace -E 'package(theseus-kernel)', nevercargo test -p, which builds a second copy of the dependencies.
Traps
- A kernel view holds the store open (an
Arcto it). Don't keep one across a simulated crash's reopen: the reopen waits on the store's lock and fails. - To show a held runtime worker in a daemon test, run the daemon with
TOKIO_WORKER_THREADS=1. - A core test with
InlineLaunchermust not kill a job: its "wrapper pid" is the test process.
