okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
57a82be
main
1"""Programs start as background jobs, and jobs are owned process groups. 2 3run(program, *args) starts one program without a shell. A job runs in its own 4session, so its program leads a process group and one signal reaches it plus 5every descendant that stayed in that group. The group 6is ended when the command ends, when its deadline passes, and at kernel 7shutdown. What survives is reported, and unfinished work stays owned. 8When the command ends on its own, members a process that left the group still 9parents stay with it: ssh's ControlPersist master daemonizes out of the job 10but leaves its ProxyCommand in the group, and ending that ends the master. 11 12A job that finishes with its result unread wakes the session: the kernel tells 13the host, the host submits a user turn naming the job, and the model never has 14to poll or await. Reading the result (tail, poll, await, output.read) or 15stopping the job withdraws the wake; an await cut short by an interrupt or a 16deadline read nothing, so it leaves the wake owed. A busy session is retried 17until it goes idle, so a notice can never overtake the read that satisfies it. Jobs 18still owed when the retry lands share one notice, and so one turn. 19""" 20 21from __future__import annotations 22 23from albedo_output import JobOutput 24 25from collections import OrderedDict 26from collections .abc import Callable ,Generator 27from typing import cast 28from albedo_api import ( 29Host , 30PythonApi , 31OutputCapture , 32Send , 33check_timeout , 34) 35import albedo_proc 36import albedo_shell 37import albedo_shims 38import albedo_trace 39import asyncio 40import os 41import shlex 42import shutil 43import sys 44import uuid 45 46loop :asyncio .AbstractEventLoop 47capture_factory :Callable [[str ],OutputCapture ] 48preview_limit :int # internal file searches use the kernel preview budget 49jobs :dict [str ,Job ]= {}# every handle the session can address 50active :dict [str ,Job ]= {}# unfinished work: the bounded resource 51retained :OrderedDict [str ,Job ]= OrderedDict ()# finished handles, completion order 52owed :dict [str ,Job ]= {}# finished with results unread, in completion order 53announcer :asyncio .Task [None ]| None = None 54send :Send 55host :Host | None = None 56watch_output :Callable [[str ,Callable [[],None ]],None ]| None = None 57forget_output :Callable [[str ],None ]| None = None 58 59ACTIVE_LIMIT = 64 # active jobs in one kernel 60RETAINED_LIMIT = 64 # finished handles still addressable through `jobs` 61COMPLETION_GRACE = 0.1 # seconds to keep reading after the command exits 62SHUTDOWN_TERM = 0.25 # shared by every live group at shutdown 63SHUTDOWN_KILL = 1.0 64NICENESS = 10 65# macOS runs utility-QoS work below the interface, so a busy swarm yields. 66TASKPOLICY = shutil .which ("taskpolicy" )if sys .platform == "darwin" else None 67NOTICE_RETRY = 2.0 # seconds between wake attempts while the session runs 68NOTICE_COMMAND_CAP = 200 # command characters a wake notice carries 69 70 71class Outlet : 72"""A job's output on its way to other jobs' stdin, besides its capture.""" 73 74def __init__ (self )-> None : 75self .feeds :list [Feed ]= [] 76self .closed = False # the program's output ended; feeds get EOF 77self .reading :asyncio .ReadTransport | None = None 78self .held :set [Feed ]= set ()# readers whose stdin is full 79 80def write (self ,data :bytes )-> None : 81for feed in self .feeds : 82feed .write (data ) 83 84def close (self )-> None : 85if not self .closed : 86self .closed = True 87for feed in self .feeds : 88feed .end () 89 90def cut (self )-> None : 91"""Stop reading the program's output once nothing wants it, so its 92next write fails the way it would into a closed shell pipe.""" 93if self .reading is not None and not self .reading .is_closing (): 94self .reading .close () 95 96def hold (self ,feed :Feed )-> None : 97self .held .add (feed ) 98if self .reading is not None : 99self .reading .pause_reading () 100 101def release (self ,feed :Feed )-> None : 102self .held .discard (feed ) 103if not self .held and self .reading is not None and not self .reading .is_closing (): 104self .reading .resume_reading () 105 106 107class Feed : 108"""One job's output into another's stdin, the way a shell pipe carries it: 109bytes wait until the reader starts, a full reader pauses the writer, and 110once every reader has stopped, the writer's next write fails (SIGPIPE).""" 111 112def __init__ (self ,writer :Job ,retained :bytes )-> None : 113self .writer = writer 114self .pending = bytearray (retained ) 115self .pipe :asyncio .WriteTransport | None = None 116self .ended = False 117self .broken = False 118 119def write (self ,data :bytes )-> None : 120if self .broken : 121return 122if self .pipe is None : 123self .pending .extend (data ) 124else : 125self .pipe .write (data ) 126 127def end (self )-> None : 128self .ended = True 129if self .pipe is not None and not self .pipe .is_closing (): 130self .pipe .close () 131 132def connect (self ,pipe :asyncio .WriteTransport )-> None : 133self .pipe = pipe 134pipe .write (bytes (self .pending )) 135self .pending .clear () 136if self .ended : 137pipe .close () 138 139def lost (self )-> None : 140"""The reader's stdin closed before the writer's output ended.""" 141self .writer .outlet .release (self ) 142if self .ended or self .broken : 143return 144self .broken = True 145if all (feed .broken for feed in self .writer .outlet .feeds ): 146self .writer .outlet .cut () 147 148 149class Command (asyncio .SubprocessProtocol ): 150"""Separate command exit from pipe EOF, without a timer per running job.""" 151 152def __init__ ( 153self ,capture :OutputCapture ,outlet :Outlet ,feed :Feed | None 154 )-> None : 155self .capture = capture 156self .outlet = outlet 157self .feed = feed # what fills this program's stdin, if a job does 158self .exited :asyncio .Future [int ]= loop .create_future () 159self .drained :asyncio .Future [None ]= loop .create_future () 160self .transport :asyncio .SubprocessTransport 161self .pid :int 162 163def connection_made (self ,transport :asyncio .BaseTransport )-> None : 164self .transport = cast (asyncio .SubprocessTransport ,transport ) 165self .pid = self .transport .get_pid () 166self .outlet .reading = cast ( 167asyncio .ReadTransport ,self .transport .get_pipe_transport (1 ) 168 ) 169 170@property 171def returncode (self )-> int | None : 172return self .transport .get_returncode () 173 174def pipe_data_received (self ,fd :int ,data :bytes )-> None : 175# Capture is bounded and synchronous; no unbounded reader queue. 176self .capture .write_bytes (data ) 177self .outlet .write (data ) 178 179def pipe_connection_lost (self ,fd :int ,exc :Exception | None )-> None : 180if fd == 0 : 181if self .feed is not None : 182self .feed .lost () 183return # stdin written and closed; output is still coming 184self .outlet .close () 185if not self .drained .done (): 186self .drained .set_result (None ) 187if self .exited .done (): 188self .transport .close () 189 190def pause_writing (self )-> None : 191if self .feed is not None : 192self .feed .writer .outlet .hold (self .feed ) 193 194def resume_writing (self )-> None : 195if self .feed is not None : 196self .feed .writer .outlet .release (self .feed ) 197 198def process_exited (self )-> None : 199code = self .returncode 200assert code is not None 201self .exited .set_result (code ) 202if self .drained .done (): 203self .transport .close () 204 205 206Data = str | bytes | os .PathLike [str ] 207 208 209async def spawn ( 210argv :list [str ], 211capture :OutputCapture , 212cwd :str | None , 213env :dict [str ,str ]| None , 214stdin :Data | None , 215outlet :Outlet , 216feed :Feed | None , 217)-> Command : 218"""Start argv at a lower priority than the person at the machine, with 219stdin fed from text, bytes, a file, or another job, and stdout and stderr 220joined.""" 221if TASKPOLICY is not None : 222argv = [TASKPOLICY ,"-c" ,"utility" ,* argv ] 223file = open (stdin ,"rb" )if isinstance (stdin ,os .PathLike )else None 224piped = stdin is not None or feed is not None 225source = file or (asyncio .subprocess .PIPE if piped else asyncio .subprocess .DEVNULL ) 226try : 227_ ,process = await loop .subprocess_exec ( 228lambda :Command (capture ,outlet ,feed ), 229* argv , 230stdin = source , 231stdout = asyncio .subprocess .PIPE , 232stderr = asyncio .subprocess .STDOUT , 233cwd = cwd , 234env = env , 235start_new_session = True , 236preexec_fn = _lower_priority , 237 ) 238finally : 239if file is not None : 240file .close () 241pipe = cast (asyncio .WriteTransport | None ,process .transport .get_pipe_transport (0 )) 242if pipe is not None and feed is not None : 243feed .connect (pipe ) 244elif pipe is not None and isinstance (stdin , (str ,bytes )): 245pipe .write (stdin .encode ()if isinstance (stdin ,str )else stdin ) 246pipe .close () 247return process 248 249 250def _lower_priority ()-> None : 251try : 252os .nice (NICENESS ) 253except OSError : 254pass 255 256 257class Job (JobOutput ): 258"""One background program and the process group it owns.""" 259 260def __init__ ( 261self , 262argv :list [str ], 263timeout :float , 264* , 265cwd :str | None = None , 266env :dict [str ,str ]| None = None , 267stdin :Data | Job | None = None , 268traced :bool = True , 269service :bool = False , 270 )-> None : 271self .service = service # expected to run on; its end is not what we wait for 272self .source :Job | None = ( 273stdin if isinstance (stdin ,Job )else None 274 )# whose output is our stdin 275self .feed = self .source ._reader ()if self .source is not None else None 276self .outlet = Outlet () 277self .id :str = uuid .uuid4 ().hex 278self .argv :list [str ]= argv 279self .command :str = shlex .join (argv )# what ran, as one line to read 280self .process :Command | None = None 281self .group :albedo_proc .Group | None = None 282self .exit_code :int | None = None 283self .timed_out :bool = False 284self .started :float = loop .time () 285self .duration :float | None = None # seconds it ran, once it ended 286self .termination :albedo_proc .Termination | None = None 287self .capture :OutputCapture = capture_factory (self .id ) 288self .ending :asyncio .Task [albedo_proc .Termination ]| None = None 289self ._awaited = False # an await returned this job; its result reached them 290self ._waiters = 0 # awaits still waiting; one cut short leaves the wake owed 291self ._read = False # the finished result was read; no wake is owed 292self ._remote = os .environ .get ("ALBEDO_REMOTE_TARGET" )or None 293self ._starting = ( 294False # the OS spawn began; from then on it must finish, not be cancelled 295 ) 296if watch_output is not None : 297watch_output (self .id ,self ._mark_read ) 298if traced : 299albedo_trace .note ("run" ,self .command ) 300data = None if isinstance (stdin ,Job )else stdin 301with albedo_trace .unobserved ():# the cell ran argv, not the wrapper around it 302self .spawning = loop .create_task (self ._launch (cwd ,env ,data ,self .feed )) 303self .task :asyncio .Task [Job ]= loop .create_task (self ._run (timeout )) 304self .task .add_done_callback (self ._cancelled ) 305active [self .id ]= self 306jobs [self .id ]= self 307 308async def _launch ( 309self , 310cwd :str | None , 311env :dict [str ,str ]| None , 312stdin :Data | None , 313feed :Feed | None , 314 )-> Command : 315self ._starting = True 316return await spawn (self .argv ,self .capture ,cwd ,env ,stdin ,self .outlet ,feed ) 317 318def _reader (self )-> Feed : 319"""A feed of this job's output for another job's stdin: what it wrote 320so far, then the rest as it comes. Piping it retires its own wake; the 321reader's notice names the pipeline.""" 322if self .capture .seen > len (self .capture .data ): 323raise ValueError ( 324f"job { self . id } wrote more than it retains ( { len ( self . capture . data ) } bytes); " 325"pipe from it before it runs: run(...).pipe(...) in one expression" 326 ) 327feed = Feed (self ,bytes (self .capture .data )) 328self .outlet .feeds .append (feed ) 329if self .outlet .closed : 330feed .end () 331self ._read = True 332return feed 333 334def pipe ( 335self , 336program :object , 337* args :object , 338cwd :str | os .PathLike [str ]| None = None , 339env :dict [str ,object ]| None = None , 340timeout :float = 300 , 341 )-> Job : 342"""Start a program reading this job's output, like a shell `|`: 343run("git", "log").pipe("rg", "fix"). Returns the reader's handle.""" 344return run (program ,* args ,cwd = cwd ,env = env ,stdin = self ,timeout = timeout ) 345 346@property 347def pipeline (self )-> str : 348"""The command line this job ends, with every job piped into it.""" 349return ( 350f" { self . source . pipeline } | { self . command } " 351if self .source is not None 352else self .command 353 ) 354 355def claim (self )-> albedo_proc .Group | None : 356"""The group this job owns, derived again when a late spawn beat the assignment.""" 357if self .group is None and self .process is not None : 358self .group = albedo_proc .Group ( 359self .process .pid ,albedo_proc .leader_token (self .process .pid ) 360 ) 361send ( 362 { 363"type" :"job_start" , 364"id" :self .id , 365"pid" :self .process .pid , 366"pgid" :self .group .pgid , 367"leader" :self .group .leader , 368"command" :self .pipeline [:4096 ], 369"service" :self .service , 370 } 371 ) 372return self .group 373 374def _cancelled (self ,task :asyncio .Task [Job ])-> None : 375if task .cancelled (): 376# Cancellation before _run starts skips its finally block. 377loop .create_task (self ._stop ()) 378 379async def _spawn (self )-> Command : 380process = await asyncio .shield (self .spawning ) 381self .process = process 382self .claim () 383return process 384 385async def _run (self ,timeout :float )-> Job : 386try : 387process = self .process = await self ._spawn () 388await asyncio .wait_for (asyncio .shield (process .exited ),timeout ) 389self .exit_code = await self ._drain (process ) 390except asyncio .TimeoutError : 391self .timed_out = True 392except asyncio .CancelledError : 393pass 394except Exception as error : 395self .capture .write (f" { type ( error ). __name__ } : { error } \n" ) 396finally : 397self .outlet .close () 398if self .feed is not None and self .feed .pipe is None : 399self .feed .lost ()# never started, so never read 400ending = await self ._stop (exited = self .exit_code is not None ) 401if self .exit_code is None and self .process is not None : 402self .exit_code = await self ._status (self .process ) 403if self .process is not None and ending .gone : 404self .process .transport .close () 405self .duration = loop .time ()- self .started 406if self .timed_out : 407self .capture .write ( 408f"\n[deadline exceeded after { timeout :g } s; { ending . report () } ]\n" 409 ) 410elif not ending .gone : 411self .capture .write (f"\n[cleanup failed: { ending . report () } ]\n" ) 412send ( 413 { 414"type" :"job" , 415"id" :self .id , 416"exit_code" :self .exit_code , 417"timed_out" :self .timed_out , 418"cleanup" :ending .as_json (), 419 } 420 ) 421self .capture .end_spill () 422release (self ) 423self ._owe_unread () 424return self 425 426async def _drain (self ,process :Command )-> int : 427"""Read output while the command runs; a descendant holding the pipe does not extend it.""" 428code = await asyncio .shield (process .exited ) 429await asyncio .wait ({process .drained },timeout = COMPLETION_GRACE ) 430return code 431 432async def _status (self ,process :Command ,window :float = 1.0 )-> int | None : 433"""The command's exit status after its group is gone; bounded, never assumed.""" 434try : 435return await asyncio .wait_for (asyncio .shield (process .exited ),window ) 436except asyncio .TimeoutError : 437return process .returncode 438 439def __await__ (self )-> Generator [object ,None ,Job ]: 440return self ._wait ().__await__ () 441 442async def _wait (self )-> Job : 443"""Wait for the job. An await cut short (an interrupted cell, a 444wait_for deadline) never delivered the result, so the job still wakes 445the session when it ends.""" 446self ._waiters += 1 447try : 448await asyncio .shield (self .task ) 449self ._awaited = True 450finally : 451self ._waiters -= 1 452if self .task .done (): 453self ._owe_unread () 454return self 455 456def poll (self ): 457if self .exit_code is not None : 458self ._read = True 459return self .exit_code 460 461@property 462def returncode (self )-> int | None : 463"""`exit_code` under subprocess's name; both spellings answer.""" 464return self .poll () 465 466def _mark_read (self )-> None : 467"""output.read reached this job's channel; a finished result is read.""" 468if self .exit_code is not None : 469self ._read = True 470 471@property 472def _unreported (self )-> bool : 473"""Finished, and its result has reached no one yet.""" 474return not self ._read and not self ._awaited and not self ._waiters 475 476def _owe_unread (self )-> None : 477"""Queue the finished job's wake unless its result reached someone.""" 478if host is not None and self ._unreported : 479owe (self ) 480 481def _facts (self )-> dict [str ,object ]: 482"""What a wake notice says about this job.""" 483command = self .pipeline [:NOTICE_COMMAND_CAP ]+ ( 484"..." if len (self .pipeline )> NOTICE_COMMAND_CAP else "" 485 ) 486outcome = ( 487"timed out" 488if self .timed_out 489else f"exit_code= { self . exit_code } " 490if self .exit_code is not None 491else "exit status unknown" 492 ) 493seconds = ( 494f"ran { self . duration :.1f } s" 495if self .duration is not None 496else "unknown duration" 497 ) 498return { 499"id" :self .id , 500"command" :command , 501"where" :f" on { self . _remote } " if self ._remote else "" , 502"summary" :f" { outcome } , { seconds } " , 503"exit_code" :self .exit_code , 504"timed_out" :self .timed_out , 505"duration" :self .duration , 506"host" :self ._remote , 507 } 508 509async def stop (self )-> albedo_proc .Termination : 510"""End the job's process group. A failed attempt stays retryable.""" 511self ._read = True # an explicit stop is its own report; no wake is owed 512return await self ._stop () 513 514async def _stop (self ,exited :bool = False )-> albedo_proc .Termination : 515"""End the group; once the command `exited` on its own, what a process 516that left the group still parents stays with that process.""" 517if self .termination is not None and self .termination .gone : 518return self .termination 519process = self .process 520if process is None : 521if not self ._starting : 522self .spawning .cancel () 523try : 524process = await self ._spawn () 525except (Exception ,asyncio .CancelledError )as error : 526self .termination = albedo_proc .Termination ( 527None , (),True , (f"spawn failed: { error } " ,) 528 ) 529release (self ) 530if self .task .done (): 531send ( 532 { 533"type" :"job" , 534"id" :self .id , 535"exit_code" :None , 536"timed_out" :False , 537"cleanup" :self .termination .as_json (), 538 } 539 ) 540return self .termination 541if self .ending is None or self .ending .done (): 542group = self .claim () 543self .ending = loop .create_task ( 544albedo_proc .end (group ,exited )if group is not None else _unstarted () 545 ) 546ending = await asyncio .shield (self .ending ) 547self .termination = ending 548release (self ) 549if self .task .done (): 550send ( 551 { 552"type" :"job" , 553"id" :self .id , 554"exit_code" :process .returncode , 555"timed_out" :self .timed_out , 556"cleanup" :ending .as_json (), 557 } 558 ) 559return ending 560 561def __repr__ (self )-> str : 562return ( 563f"Job(id= { self . id !r } , exit_code= { self . exit_code !r } , " 564f"timed_out= { self . timed_out !r } , duration= { self . duration !r } , " 565f"bytes= { self . capture . seen } )" 566 ) 567 568 569async def _unstarted ()-> albedo_proc .Termination : 570return albedo_proc .Termination (None , (),True , ("process never started" ,)) 571 572 573def release (job :Job )-> None : 574"""Retire active work only when its group is gone.""" 575if ( 576job .termination is None 577or not job .termination .gone 578or active .pop (job .id ,None )is None 579 ): 580return 581retained .pop (job .id ,None ) 582retained [job .id ]= job 583while len (retained )> RETAINED_LIMIT : 584_ ,evicted = retained .popitem (last = False ) 585if jobs .get (evicted .id )is evicted : 586del jobs [evicted .id ] 587 588 589def forget (job :Job )-> None : 590"""Drop a finished job a plugin ran for its own purposes: it leaves `jobs` 591and the retained output channels, so internal work never shows up beside, 592or crowds out, the model's own jobs and cells.""" 593if jobs .get (job .id )is job : 594del jobs [job .id ] 595retained .pop (job .id ,None ) 596if forget_output is not None : 597forget_output (job .id ) 598 599 600def owe (job :Job )-> None : 601"""Queue a finished job's wake. One announcer carries every owed result, 602so jobs that finish during a turn wake the session once, not once each.""" 603global announcer 604owed [job .id ]= job 605if announcer is None or announcer .done (): 606announcer = loop .create_task (_announce ()) 607 608 609async def _announce ()-> None : 610"""Wake the session for every owed result, retrying while it runs. 611 612A busy session answers with a refusal rather than queueing, so the notice 613retries until the run ends, naming whatever is still owed then; a read or 614await in the meantime retires that job, which is also the creating-cell 615barrier: a cell that will await its own job suppresses the wake before any 616turn can start. 617""" 618while True : 619for job in [job for job in owed .values ()if not job ._unreported ]: 620del owed [job .id ] 621notify = host 622if not owed or notify is None : 623return 624batch = list (owed .values ()) 625try : 626await notify ("jobs.completed" ,notice ([job ._facts ()for job in batch ])) 627except Exception as error : 628if getattr (error ,"code" ,"" )== "busy" : 629await asyncio .sleep (NOTICE_RETRY ) 630continue 631for job in batch : 632job .capture .write (f"\n[completion notice not delivered: { error } \n" ) 633for job in batch : 634owed .pop (job .id ,None ) 635 636 637# A job on a remote.connect() host lives in that kernel's `jobs`, not the 638# model's, so its notice names the reference rem.run returned instead. 639REMOTE_READING = ( 640" a job on another host is read with .tail() on the reference rem.run" 641" returned, or await rem.output.read(id) on that connection." 642) 643 644 645def _name (job :dict [str ,object ])-> str : 646"""How a notice names a job: its `jobs` entry here, or a remote job's id.""" 647return repr (job ["id" ])if job ["host" ]else f"jobs[ { job [ 'id' ]!r } ]" 648 649 650def notice (facts :list [dict [str ,object ]])-> dict [str ,object ]: 651"""The wake turn's display text, model text, and the facts behind both.""" 652advice = ( 653" no user sent this message; use the result if the session's work needs" 654" it, otherwise acknowledge briefly and stay idle.</system-note>" 655 ) 656if len (facts )== 1 : 657 [job ]= facts 658reading = ( 659" its handle is the reference rem.run returned; its .tail() or" 660f" await rem.output.read( { job [ 'id' ]!r } ) on that connection reads its output." 661if job ["host" ] 662else f" its handle is jobs[ { job [ 'id' ]!r } ] in python; jobs[ { job [ 'id' ]!r } ].tail() or" 663f" output.read( { job [ 'id' ]!r } ) reads its output." 664 ) 665text = ( 666"<system-note>a background job finished with its result unread" 667f" { job [ 'where' ] } : { job [ 'summary' ] } , command: { job [ 'command' ] } ." 668+ reading 669+ advice 670 ) 671else : 672listed = "; " .join ( 673f" { _name ( job ) } { job [ 'where' ] } : { job [ 'summary' ] } , command: { job [ 'command' ] } " 674for job in facts 675 ) 676remote = any (job ["host" ]for job in facts ) 677text = ( 678f"<system-note> { len ( facts ) } background jobs finished with their results" 679f" unread: { listed } . jobs[id].tail() or output.read(id) reads one's" 680" output." + (REMOTE_READING if remote else "" )+ advice 681 ) 682display = "\n" .join ( 683f"job finished { job [ 'where' ] } ( { job [ 'summary' ] } ): { job [ 'command' ] } " 684for job in facts 685 ) 686return {"display" :display ,"text" :text ,"jobs" :facts } 687 688 689def run ( 690program :object , 691* args :object , 692cwd :str | os .PathLike [str ]| None = None , 693env :dict [str ,object ]| None = None , 694stdin :Data | Job | None = None , 695timeout :float = 300 , 696service :bool = False , 697)-> Job : 698"""Start one program, without a shell, and return its handle immediately: 699run("go", "test", "./...", cwd="cli"). It starts at once, at low priority. 700The timeout, in seconds and at most a day, counts wall time after the 701program starts. 702 703`env` adds to the kernel's environment. `stdin` is text or bytes to feed 704the program, a path to read, or another job, whose output streams in like 705a shell pipe (job.pipe(...) says the same). stderr joins stdout. 706 707Output stays on the handle: job.tail(lines=20) and job.head(lines=20) for 708its ends, output.read(job.id) for the retained whole. Jobs survive cell 709completion and each owns its process group. Only unfinished work counts 710against the concurrent quota; finished handles stay usable in `jobs` until 711newer ones displace them. 712 713A job that finishes with its result unread wakes the session by itself, so 714polling or trailing a handle is optional: leave it in a variable, end the 715cell, and the wake names the job when it lands. Awaiting the job, reading 716its result, or stopping it first means no wake. 717 718`nix` never copies a jj workspace into the store (a flake there is read 719from its commit), and `nix develop -c program ...` reuses a cached dev 720shell. `cargo` runs through mbx when it is installed, so every checkout 721shares compiled work; env={"ALBEDO_NO_MBX": "1"} runs plain cargo. 722 723Pass service=True for a program that is meant to keep running (a dev 724server, a file watcher): you are not waiting for it to finish, so it 725does not keep your prompt cache warm while you are idle. 726""" 727check_timeout (timeout ) 728if not (stdin is None or isinstance (stdin , (str ,bytes ,os .PathLike ,Job ))): 729raise TypeError ( 730f"stdin is text, bytes, a path, or a job on this kernel, not { type ( stdin ). __name__ } " 731 ) 732argv = albedo_shell .words ((program ,* args )) 733script = albedo_shell .shell_script (argv ) 734if script is not None : 735raise albedo_shell .refusal (f"` { os . path . basename ( argv [ 0 ]) } -c`" ,script = script ) 736environment = albedo_shims .prepend ( 737None 738if env is None 739else {** os .environ ,** {str (k ):str (v )for k ,v in env .items ()}} 740 ) 741directory = None if cwd is None else os .fsdecode (cwd ) 742if os .sep not in argv [0 ]: 743found = shutil .which (argv [0 ],path = (environment or os .environ ).get ("PATH" )) 744if found is None : 745if any (char .isspace ()for char in argv [0 ]): 746raise albedo_shell .refusal ( 747"a whole command line as the program" ,script = argv [0 ] 748 ) 749raise FileNotFoundError (f" { argv [ 0 ] } : no such program on PATH" ) 750return start ( 751argv ,timeout ,cwd = directory ,env = environment ,stdin = stdin ,service = service 752 ) 753 754 755def start ( 756argv :list [str ], 757timeout :float , 758* , 759cwd :str | None = None , 760env :dict [str ,str ]| None = None , 761stdin :Data | Job | None = None , 762traced :bool = True , 763service :bool = False , 764)-> Job : 765"""A job for argv as given, for run() and for plugins' own supervised work.""" 766if len (active )>= ACTIVE_LIMIT : 767raise RuntimeError (f" { ACTIVE_LIMIT } jobs are active; await or stop one first" ) 768return Job ( 769argv ,timeout ,cwd = cwd ,env = env ,stdin = stdin ,traced = traced ,service = service 770 ) 771 772 773async def close ()-> None : 774"""Kernel shutdown: end every live group inside one shared deadline.""" 775owned = list (active .values ()) 776# Never spawn a pending command just to shut it down. In-flight OS spawns, 777# however, must finish transferring ownership before the shared ladder. 778for job in owned : 779if not job ._starting : 780job .spawning .cancel () 781await asyncio .gather ( 782* (job ._spawn ()for job in owned if job .process is None ),return_exceptions = True 783 ) 784live = [(job ,group )for job in owned if (group := job .claim ())is not None ] 785if not live : 786return 787endings = await albedo_proc .terminate ( 788 [group for _ ,group in live ],SHUTDOWN_TERM ,SHUTDOWN_KILL 789 ) 790for (job ,_ ),ending in zip (live ,endings ): 791job .termination = ending 792release (job ) 793failures = [ending .report ()for ending in endings if not ending .gone ] 794if failures : 795send ({"type" :"cleanup" ,"module" :"run" ,"failures" :failures }) 796 797 798def setup (api :PythonApi )-> dict [str ,object ]: 799global loop ,capture_factory ,preview_limit ,jobs ,active ,retained ,send 800global host ,watch_output ,forget_output 801loop ,capture_factory ,preview_limit ,send = ( 802api .loop , 803api .capture , 804api .preview , 805api .send , 806 ) 807host ,watch_output ,forget_output = api .host ,api .watch_output ,api .forget_output 808 809async def stop_local_job (job_id :str )-> dict [str ,object ]| None : 810job = jobs .get (job_id ) 811if job is None : 812return None 813await job .stop () 814return {"ok" :True } 815 816jobs ,active ,retained = {}, {},OrderedDict () 817api .background_handle (Job ) 818api .on_shutdown (close ) 819if api .on_stop_job is not None : 820api .on_stop_job (stop_local_job ) 821return {"run" :run ,"jobs" :jobs }