@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
446 lines • 21.8 kB
TypeScript
import type { ProcessPermissions } from '../../process/config.ts';
import { ProcessTable } from '../../process/table.ts';
import { type JsonValue } from '../../types.ts';
import type { JobStore } from './store.ts';
import { Job, type JobRunner } from './types.ts';
import { type Clock } from '../../utils/clock.ts';
import { type RecordFields } from '../../workspace/record/types.ts';
/**
* The shell's job table: bash's per-shell job list, one list per session.
*
* This is job control, not a process table. A job is numbered `%N`
* within the session that launched it, numbering restarts at 1 once that
* session's list empties (GNU bash), and `jobs`, `wait`, `fg`, `kill` and
* `disown` only ever see the calling session's list, exactly as one bash
* never lists another bash's jobs. Runner PIDs are tracked separately:
* `$!` and `jobs -l` report the managed PID, while `%N` names a
* session-local job number. A killed job's process remains stopping until
* the runner actually finishes, and its console ends then, or once the
* runner had `STOP_WAIT_SECONDS` to stop and did not.
*
* The lists live in the workspace's job store, so every workspace on a
* shared store sees a session's jobs with the same numbers. A job runs on
* the table that started it, its owner; another table lists it, reads and
* waits on its console, and kills it by ending that console, which its
* owner watches for. `sync` brings a session's list up to date before a
* builtin reads it. A job whose owner's lease lapsed with no ending was
* lost with its owner and is ended as hung up, as a bash's job is when its
* terminal goes.
*
* The table is still owned by the workspace rather than by a session,
* because the workspace owns the tasks: teardown must stop every job it
* runs in every session (`killAll`), snapshot capture reads every finished
* one (`allJobs`), and a disowned job keeps running after its shell forgot
* it. Those are the only cross-session views; every other method takes
* the session whose list it reads, and the empty session id is the list a
* caller with no session (a bare table in a test) shares.
*/
export declare class JobTable {
readonly processes: ProcessTable;
readonly parent: JobTable | null;
readonly clock: Clock;
store: JobStore;
readonly owner: string;
private jobs;
private disowned;
private lease;
private checking;
private stopping;
private publishing;
private publishTick;
private closed;
private reaping;
private reaped;
private reapingStopped;
private swept;
private sweepTick;
private plane;
private jobTables;
/**
* Stops the native runs a lapsed owner left, given them and the first
* signal, and answers the ones this workspace cannot reach; the workspace
* sets it, since it holds the runtimes that reach them.
*/
reaper: ((runs: Record<string, JsonValue>[], signum: number) => Promise<Record<string, JsonValue>[]>) | null;
/**
* @param store the job plane holding each session's list and each job's
* console; null keeps them in memory, private to this table.
*/
constructor(store?: JobStore | null, processes?: ProcessTable, parent?: JobTable | null, clock?: Clock);
/**
* A table for a child shell (`bash -c`, a script, `( )`, `$( )`): a job
* list of its own on the same processes, kept in memory, since a child
* shell's jobs end with it on this workspace and no other one lists
* them. `parent` is the table a `$( )` still lists in `jobs`. It lists
* and stops other workspaces' processes through this table, on the
* workspace's store.
*/
child(parent?: JobTable | null): JobTable;
private sessionJobs;
/**
* Register a job in its session's list and start it.
*
* The job's number is taken from the store first, so two workspaces
* starting a job on one session never share one; a process slot is
* reserved next, and a refused one gives the number back. The runner is
* deferred until the job and console are ready; failed setup cancels the
* reservation without entering the supplied runner. Only fully
* initialized jobs join the table.
*/
submit(init: {
command: string;
run: JobRunner;
abort: AbortController;
cwd: string;
sessionId?: string;
parentPid?: number | null;
limit?: number | null;
simple?: boolean;
}): Promise<Job>;
/**
* Insert a finished job restored from a snapshot into its session. Its
* output moves onto a console in the store, so every workspace on it
* reads the job the way it reads one that ran there. It keeps its number
* unless another workspace's job holds it, and then takes the next free
* one rather than hide that job.
*/
load(job: Job): Promise<void>;
/**
* Bring a session's list up to date with the store.
*
* Adds the jobs other workspaces started, drops the ones they reaped or
* disowned (a job this table runs keeps running, on the side list
* `disown` uses), and settles a job whose console another workspace
* ended, or another workspace's job whose owner's lease lapsed with no
* ending. A job is the one listed under its number only while their
* consoles match: once the list empties, numbering restarts and a new
* job can take a reaped one's number.
*
* A console attached to a job first seen here is the job's only while
* the list has not changed since it was read: a store drops the console
* of a job reaped meanwhile, and attaching then would open an empty one
* that never ends. So the list is read again after such an attach, and
* the sync retried should it have moved. This table's own jobs are
* matched after the last read, with no wait between, so one it started
* meanwhile is never taken for another workspace's; a running one the
* list no longer names stays listed, unless the list holds it as
* disowned or another job took its number, and then it runs on unlisted,
* as a disowned job does.
*/
sync(sessionId?: string): Promise<void>;
get(jobId: number, sessionId?: string): Job | null;
listJobs(sessionId?: string): Job[];
/** The jobs `jobs` shows: a `$( )`'s caller's, then its own. */
listing(sessionId?: string): Job[];
runningJobs(sessionId?: string): Job[];
/**
* Every session's jobs, for the workspace-wide entry points only. Snapshot
* capture and the server summary read this; a shell builtin never
* does, since bash lists only its own jobs.
*/
allJobs(): Job[];
allRunningJobs(): Job[];
/**
* Stop a job and record it as killed.
*
* The job answers as killed at once, but its console ends only once the
* runner has stopped, so a program that handled the signal leaves its
* own status in the ending every workspace reads. Nothing here waits for
* the aborted runner. The signal is only observed where someone checks
* it, which today is the executor between nodes and the commands that
* take it, so a job sitting inside one long command would not notice
* until it finished on its own; joining would hang the shell on exactly
* the runaway job the caller is trying to stop. A runner that has not
* stopped `STOP_WAIT_SECONDS` later has its console ended for it, with
* the status the kill recorded, and one that has not begun has it ended
* at once: no program ran that could leave its own.
*
* Another workspace's job is killed by ending its console; its owner
* sees the ending and stops the runner. Should the job have ended
* first, the ending it wrote stands and the job was not killed.
*
* @param signal the signal the job is stopped with; its status is 128
* plus it, KILL's 137 by default.
*/
kill(jobId: number, sessionId?: string, signal?: number): Promise<boolean>;
/**
* Drop a job from its session's list without stopping it (`disown`):
* the job keeps running, `jobs` no longer lists it and `wait` no
* longer knows it. It stays on a side list so `killAll` at teardown
* reaches its task.
*/
disown(jobId: number, sessionId?: string): Promise<boolean>;
/**
* Drop a session's job list when the session closes, stopping what is
* still running, and return what was stopped.
*
* What happens to a bash's jobs when that bash exits: they are hung
* up, and a later shell that reuses the same id starts from an empty
* list numbered from 1 rather than inheriting jobs it never launched,
* under a profile it may not share. Session closure revokes process
* entry points and stops disowned runners too. Closing a session closes
* it everywhere, so the jobs other workspaces run in it are stopped too,
* the disowned ones included: their consoles are ended, which their
* owners watch for. The list goes only while it is the one the stops were
* taken from: a job another workspace starts meanwhile changes it, and
* the close stops that job too before it tries again.
*/
closeSession(sessionId: string): Promise<Job[]>;
/** Stop every running job this table runs in every session, returning
* the ones that were running. Disowned jobs are stopped too: the shell
* forgot them, the workspace did not, and a teardown that left them
* running would leak tasks. Another workspace's jobs are its own to
* stop. The reaping of lapsed owners' runs stops here as well, before the
* runtimes it reaches them through close: none starts, and one under way
* is waited for, bounded as a reap is. */
killAll(): Promise<Job[]>;
/**
* Stop renewing this table's lease, at workspace teardown. Called after
* killAll(), so every job this table ran has ended and other workspaces
* need no lease to settle them; a killed job whose runner is still
* stopping has its console ended now rather than at its bound. The
* store itself belongs to the workspace's state store.
*/
close(): Promise<void>;
/**
* Block until a job ends, then return it.
*
* Joined on the console's ending chunk, never on the status field:
* kill() and settle() both flip the status before their final
* appends, and every await yields a microtask, so a status-based
* return could let the caller snapshot and reap the job before
* `Killed` or the ending chunk is persisted. A restored job has no
* task and its console already holds the ending chunk, so it
* returns without waiting.
*
* A killed job's console ends once its runner has stopped, or
* `STOP_WAIT_SECONDS` after the kill: what it started stops in that
* time (a handler finishes, a sandbox is reaped) and the status it
* leaves is the one answered, as bash's `wait` answers once the child
* has gone. An ending another workspace wrote is taken at once, before
* this table's watcher stops the runner. The runner is joined too, for
* at most `STOP_WAIT_SECONDS`, so what its task holds is let go before
* this returns.
*
* Another workspace's job answers the status its ending records. While
* none is stored its owner's lease is checked every third of it, and a
* lapsed one ends the job as hung up.
*
* @param signal abandons the wait, so a caller that gave up stops its
* reads: a promise cannot be cancelled, where Python cancels its task.
* The job keeps running.
*/
wait(jobId: number, sessionId?: string, signal?: AbortSignal): Promise<Job>;
/**
* Join every job in a session's list, returning the ones still
* running.
*
* Every job, not only the running ones: a killed job's `Killed`
* marker can still be in flight (see wait()), and bare `wait`
* snapshots each console right after this returns. Joining a
* finished job costs one read.
*/
waitAll(sessionId?: string, signal?: AbortSignal): Promise<Job[]>;
/**
* Remove one job from its session's list.
*
* What a targeted `wait`/`fg` does once the job has ended, matching
* GNU bash, where a job waited on by id is deleted from the job list,
* so a later `jobs` or `wait %N` no longer finds it.
*/
reap(jobId: number, sessionId?: string): Promise<void>;
/**
* Return a session's completed/killed jobs and remove them from its
* list.
*
* A reader holding a job's console keeps reading it: the console
* outlives its table entry and dies with its last reader.
*/
popCompleted(sessionId?: string): Promise<Job[]>;
/** Another workspace's job, as its list entry describes it. */
/**
* Publish the processes this table's workspace runs, now that one
* started: the publishing goes on while any runs (`publish`).
*/
processesChanged(): void;
/**
* The processes other workspaces on the store run, owner to PID to
* process, as each last published them. A lapsed owner's are left out:
* whatever it ran was lost with it, and the native runs it left are
* stopped meanwhile.
*/
remoteProcesses(): Promise<Record<string, Record<string, RecordFields>>>;
/**
* Signal a process another workspace runs, named by a prefix of its
* owner's id and the owner's PID: its owner stops it once it takes the
* stop, within `PUBLISH_SECONDS`. Signal 0 only probes. A lapsed owner's
* process is gone, and the native runs it left are stopped instead. The
* grants are read once the store has answered, just before anything is
* stopped, so a profile replaced meanwhile stops nothing under the old
* one. Answers whether one live process was signalled; throws EPERM for
* one the session sees but may not stop.
*
* @param owner a prefix of the owner's id, as `ps` shows it.
* @param pid the process, by the owner's PID.
* @param signal the signal, 0 to probe.
* @param sessionId the calling session.
* @param permissions whose processes it may see and stop, null once the
* caller's view is revoked.
*/
signalRemote(owner: string, pid: number, signal: number, sessionId: string, permissions: () => ProcessPermissions | null): Promise<boolean>;
private remote;
/**
* End a killed job's console once its runner has stopped, or
* `STOP_WAIT_SECONDS` after the kill, whichever comes first. A runner
* that settles ends the console itself, with the status it left; this
* ends it with the status the kill recorded when the runner never got
* that far (aborted before it began) or is still stopping, or at once
* when cut (close()). Mirrors Python's `_end_killed`.
*/
/**
* Stop a job this table runs and record it as killed by `signal`. One
* that has not begun has its console ended at once: no program ran that
* could leave its own status. One that has has its console ended once its
* runner stops, or `STOP_WAIT_SECONDS` later.
*/
private stopRunner;
private endKilled;
/**
* End a killed job's console with the status its kill recorded, unless
* its runner already ended it.
*/
private endStopping;
/**
* Keep how a job this table ran ended in its list entry, or drop its
* disowned record, which nothing else would. The entry outlives the
* console's output, which a shared store expires, so a workspace that
* attaches after the output went still reads how the job ended instead
* of taking it as hung up. A failed write is logged: the console still
* holds the ending.
*/
private recordEnding;
/**
* Settle another workspace's job from its console's ending, or end it as
* hung up when its owner's lease lapsed with none.
*/
private refresh;
/**
* Block until another workspace's job ends, or `signal` aborts. One read
* follows the console across every lease check, so a long wait never
* stacks readers, and an abandoned one stops reading.
*/
private joinRemote;
/**
* Add a job to a session's list under the next number. Numbering
* restarts at 1 once the list is empty, as GNU bash restarts it; without
* that, reaping after a targeted `wait` would leave a later `wait %1`
* pointing at nothing.
*/
private allocate;
/**
* Add a job to a session's list under its own number, or under the next
* free one should another job hold it, and return the number it took.
*/
private insert;
/**
* Drop jobs from a session's list, named by their consoles, never by
* number: another workspace may have reaped one and a new job taken its
* number. With `giveBack` the jobs never started, so the last number
* handed out is free again, as no job took it. With `disowned` the jobs
* still run, disowned: their entries move to the list's disowned ones,
* by console, where the session's close still reaches them.
*/
private remove;
/**
* Write a session's list over the one read, iff still current.
* `disowned` is the disowned jobs still running, by console; null keeps
* the ones read. A list written where none is starts at a random
* generation rather than at 1, so a list deleted and written again never
* repeats the generation a reader of the old one holds: a close that read
* the old one deletes the new one only once it has stopped its jobs.
*/
private write;
/**
* Renew this table's lease while it runs jobs on a store that leases
* owners, with the consoles of the jobs it still runs, so a quiet one
* keeps its output while this table lives.
*/
/** Check the lists of the jobs this table runs every
* `LIST_CHECK_SECONDS` while it runs any. */
private keepChecking;
/** Stop checking once every job this table runs has ended its console. */
private dropChecking;
/**
* Stop each job this table runs that its session's list dropped.
*
* A close, a reap and a disown of a running job each end its console or
* keep its entry, so a list that dropped one whose console is still open
* went with its workspace, deleted elsewhere: an ending written there
* first would be deleted before this table's watch read it. The job is
* stopped as a closed session's is. Each session's list is read once a
* round, however many of its jobs run here; a round that failed for a
* session, on its list or on a console, is tried again the next round.
*/
private checkLists;
/**
* The live processes this workspace runs, as its record keeps them, each
* with the native runs it has going.
*/
private records;
/**
* Publish the processes this workspace runs every `PUBLISH_SECONDS`
* while it runs any, and stop those other workspaces asked it to stop.
* Once it runs none, its record goes and the publishing stops. The
* owner's lease is renewed once ahead of the first record it vouches for,
* then kept by the table's lease renewal (`keepLease`) while any process
* runs, on its own timer whatever a round costs. The record is written
* again every third of the lease, so it outlives its store's expiry and
* comes back should a workspace have taken this one for lapsed. Each
* record carries `seen_at`, when its processes were read, the last time
* this workspace vouched for them and their native runs, on the host's
* clock, which a host reap compares with `ps`. A failed
* round is logged and tried again the next one, and a stop that fails
* leaves the others it came with to be applied; the sleep between rounds
* keeps no host alive on its own.
*/
private publish;
/**
* Stop one of this workspace's processes another workspace asked to
* stop: a job's runner the way `kill` stops a job, through the table
* that holds it, a child shell's included; any other runner directly.
*/
private stopAsked;
/**
* Stop what the lapsed instances on the store left running, without
* waiting: the workspace calls it as it starts, so what a crashed instance
* ran stops before anything lists it. Once per table, and only on a store
* that leases owners: on any other, no owner lapses.
*/
reapLapsed(): void;
/**
* Reap the lapsed owners on the store, and once more a lease later while
* any other owner still held one: an instance that died just before this
* one started still holds its lease until then.
*/
private sweep;
/** Stop the native runs a lapsed owner left, without waiting. */
private reapLater;
/**
* Stop the native runs a lapsed owner left, those this workspace reaches,
* and keep its record holding only the rest, for a workspace that reaches
* them. The owner's lease is checked again before each run is stopped, so
* one that renewed meanwhile keeps its runs, and its record as it is. Each
* run goes to the reaper with its record's `seen_at`.
*/
private reapOwner;
private keepLease;
/**
* Stop renewing once every job this table runs has ended its console and
* no process it publishes runs: a killed one still stopping keeps the
* lease, or other workspaces would end it as hung up, and a published
* process keeps it, or they would take its record for lapsed; the
* publishing drops it as it stops.
*/
private dropLease;
}
//# sourceMappingURL=table.d.ts.map