okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
8a09b71
main
1"""The prompt-cache warmer: an idle session whose children or background jobs 2still run re-sends its last request with a tiny output budget before the 3provider cache would expire. 4 5Catches: pings that rebuild the request instead of repeating the one the turn 6sent (which would compact or summarize), ping request rows that do not carry 7the turn's prefix identity, pings that commit or publish anything, warming 8that never stops at its budget, warming that continues after the waking work 9finished, a session let go cold while its job runs on a remote host its 10kernel cannot list, warming kept up for a job that was started as a service, 11a parent let go cold while its child sits between turns on a build of its 12own, and a submit that cannot get through while a ping holds the session. 13None of this is observable from the transcript alone; the provider and the 14request rows are the witnesses. 15""" 16 17import json 18import os 19import sqlite3 20from datetime import datetime 21import threading 22import time 23import unittest 24 25from harness import ( 26LOOPBACK_REMOTE , 27Albedo , 28alive , 29Provider , 30exclusive , 31operation_id , 32python , 33release_fifo , 34text , 35) 36 37 38def milliseconds (timestamp ): 39return round ( 40datetime .fromisoformat (timestamp .replace ("Z" ,"+00:00" )).timestamp ()* 1000 41 ) 42 43 44# A prefix big enough to matter: 4096 cached tokens against the 1024 floor. 45PARENT_USAGE = { 46"input_tokens" :6000 , 47"output_tokens" :4 , 48"input_tokens_details" : {"cached_tokens" :4096 }, 49} 50CHILD_USAGE = { 51"input_tokens" :300 , 52"output_tokens" :4 , 53"input_tokens_details" : {"cached_tokens" :100 }, 54} 55 56TASK = "hold the fort until released" 57JOB_PROMPT = "wait for the job" 58CHILD_JOB_TASK = "start the build and report back" 59 60 61# A local cache-table override matching the fixture host: a short refresh TTL, 62# bought at 1.0x and read at 0.3x, so pings come 2.7s apart and at most 63# floor(1.0 / 0.3) - 1 = 2 of them pay for themselves. A tick later than the 64# 2s jitter floor (the 0.3s margin is under the 5s landing time) is dropped. 65TTL_SECONDS = 3 66# Longer than a ping interval by more than a loaded machine's jitter: a ping 67# that was due would have gone out within it. 68QUIET_SECONDS = 2 * TTL_SECONDS 69CACHE_TTL = { 70"version" :1 , 71"entries" : [ 72 { 73"id" :"fixture" , 74"match" : {"host" :"127.0.0.1" }, 75"policy" :"refresh" , 76"clock" :"request" , 77"tiers" : [{"seconds" :TTL_SECONDS ,"write" :1.0 }], 78"read" :0.3 , 79"evidence" :"measured" , 80 } 81 ], 82} 83 84 85# Long enough to outlast a slow daemon restart: pings 10s apart, the latest 5s 86# past that, and floor(1.0 / 0.1) - 1 = 9 of them. A 1s idle or unload sweep 87# comes well before the first one. 88RESTART_TTL = { 89** CACHE_TTL , 90"entries" : [ 91 { 92** CACHE_TTL ["entries" ][0 ], 93"tiers" : [{"seconds" :20 ,"write" :1.0 }], 94"read" :0.1 , 95 } 96 ], 97} 98# Past a restarted session's first ping, had there been one. 99RESTART_QUIET_SECONDS = 12 100 101 102def release (gate ): 103wait_for (lambda :release_fifo (gate )) 104 105 106def wait_for (predicate ,timeout = 30 ): 107deadline = time .monotonic ()+ timeout 108while time .monotonic ()< deadline : 109value = predicate () 110if value : 111return value 112time .sleep (0.05 ) 113raise AssertionError ("warming never arrived" ) 114 115 116class WarmTest (unittest .TestCase ): 117def setUp (self ): 118# The child's turn blocks here until the test releases it, so the 119# parent sits idle on work that will wake it. 120self .gate = threading .Event () 121self .job_code = "" 122self .provider = Provider (self .reply ) 123providers = { 124"orchestrator" : { 125"baseUrl" :self .provider .url + "/parent/v1" , 126"apiKey" :"key" , 127"model" :"fixture-model" , 128"protocol" :"responses" , 129 }, 130"scout" : { 131"baseUrl" :self .provider .url + "/child/v1" , 132"apiKey" :"key" , 133"model" :"scout-model" , 134"protocol" :"responses" , 135 }, 136 } 137 138def prepare (app ): 139 (app .home / "cache-ttl.json" ).write_text (json .dumps (CACHE_TTL )) 140app .write_extensions ( 141 {"enabled" : {"warm" :True },"warm" : {"minCachedTokens" :1024 }} 142 ) 143 144self .app = Albedo ( 145self .provider ,protocol = "responses" ,providers = providers ,prepare = prepare 146 ) 147self .app .__enter__ () 148self .addCleanup (self .provider .close ) 149self .addCleanup (self .app .__exit__ ,None ,None ,None ) 150# A failed test must not leave the scout's request blocked. 151self .addCleanup (self .gate .set ) 152 153def reply (self ,request ): 154sent = json .dumps (request ) 155if TASK in sent : 156self .gate .wait (timeout = 90 ) 157return text ("scout finished" ,usage = CHILD_USAGE ) 158if CHILD_JOB_TASK in sent : 159if "function_call_output" not in sent : 160return python (self .job_code ) 161return text ("build started" ,usage = CHILD_USAGE ) 162if JOB_PROMPT in sent and "function_call_output" not in sent : 163return python (self .job_code ) 164return text ("orchestrator reply" ,usage = PARENT_USAGE ) 165 166def api (self ,path ,body = None ,method = None ): 167with self .app .api (path ,body ,method = method )as response : 168return json .load (response ) 169 170def requests (self ,fragment ): 171return [ 172record ["request" ] 173for record in self .provider .requests 174if fragment in record ["path" ] 175 ] 176 177def pings (self ,session_requests ): 178return [ 179request for request in session_requests if "max_output_tokens" in request 180 ] 181 182def rows (self ,session ): 183with self .app .api (f"/sessions/ { session } /context?view=requests" )as response : 184return json .load (response )["items" ] 185 186def history (self ,session ): 187items ,after = [],0 188while True : 189with self .app .api ( 190f"/sessions/ { session } /history?after= { after } &limit=100" 191 )as response : 192page = json .load (response ) 193items .extend (page ["items" ]) 194if page ["newer" ]is None : 195return items 196after = items [- 1 ]["position" ] 197 198def spawn (self ,parent ,task ): 199"""The parent's scout, started on `task`.""" 200child = operation_id () 201self .app .api ( 202f"/sessions/ { child } " , 203 { 204"kind" :"child" , 205"parent_id" :parent , 206"address" :"scout" , 207"name" :"scout" , 208"initial_input_id" :operation_id (), 209"task" :task , 210"model" :"scout/scout-model" , 211 }, 212method = "PUT" , 213headers = {"If-None-Match" :"*" }, 214 ).close () 215return child 216 217def swarm (self ,parent = None ): 218"""A parent whose turn has run while its scout is still working.""" 219parent = parent or self .app .session () 220child = self .spawn (parent ,TASK ) 221wait_for (lambda :self .requests ("/child/" )or None ) 222self .app .prompt (parent ,"run the swarm" ).close () 223self .app .idle (parent ) 224return parent ,child 225 226def job_gate (self ,service ,remote = False ): 227"""The fifo a job started by `self.job_code` runs until. A remote job 228runs on a loopback host: the parent's kernel cannot list it, only hear 229its remote kernel count it.""" 230gate = self .app .workspace / "job-release" 231os .mkfifo (gate ) 232program = f"open( { str ( gate )!r } , 'rb').read(1)" 233flag = ", service=True" if service else "" 234start = f"run(sys.executable, '-c', { program !r } { flag } )" 235if remote : 236# The cell ends once the remote kernel has started the job. 237self .job_code = ( 238LOOPBACK_REMOTE 239+ f"""import asyncio, sys 240job = rem. { start } 241for _ in range(200): 242if job.id is not None: 243break 244await asyncio.sleep(0.05) 245job.id""" 246 ) 247else : 248self .job_code = f"import sys\njob = { start } \njob.id" 249# A failed test must not leave the job blocked. 250self .addCleanup (release_fifo ,gate ) 251return gate 252 253def job (self ,service ,remote = False ): 254"""A parent whose turn started a job that runs until released, and 255then went idle waiting on it.""" 256gate = self .job_gate (service ,remote ) 257parent = self .app .session () 258self .app .prompt (parent ,JOB_PROMPT ).close () 259self .app .idle (parent ) 260return parent ,gate 261 262def child_job (self ): 263"""A parent woken by its scout's report that a build it started is 264running, idle again with the scout idle between turns on that job.""" 265gate = self .job_gate (service = False ) 266parent = self .app .session () 267child = self .spawn (parent ,CHILD_JOB_TASK ) 268wait_for (lambda :self .requests ("/parent/" )or None ) 269self .app .idle (child ) 270self .app .idle (parent ) 271return parent ,child ,gate 272 273def build_finished (self ,gate ): 274"""Release the scout's build and wait for its end to wake the scout.""" 275release (gate ) 276wait_for ( 277lambda :any ( 278"job finished" in json .dumps (request ) 279for request in self .requests ("/child/" ) 280 ) 281 ) 282 283def turns (self ,fragment ): 284return [ 285request 286for request in self .requests (fragment ) 287if "max_output_tokens" not in request 288 ] 289 290def assert_no_pings (self ,parent ,child ): 291time .sleep (QUIET_SECONDS ) 292self .assertEqual (len (self .requests ("/parent/" )),1 ) 293self .assertEqual ([row ["kind" ]for row in self .rows (parent )], ["turn" ]) 294 295# exclusive: writes daemon-wide cache TTL and warmer settings 296@exclusive 297def test_the_warmer_is_on_unless_disabled (self ): 298settings = json .loads ((self .app .home / "extensions.json" ).read_text ()) 299del settings ["enabled" ]["warm" ] 300 (self .app .home / "extensions.json" ).write_text (json .dumps (settings )) 301parent ,child = self .swarm () 302wait_for (lambda :self .pings (self .requests ("/parent/" ))or None ) 303self .gate .set () 304self .app .idle (child ) 305self .app .idle (parent ) 306 307# exclusive: writes daemon-wide cache TTL and warmer settings 308@exclusive 309def test_the_warmer_is_off_when_disabled_globally (self ): 310settings = json .loads ((self .app .home / "extensions.json" ).read_text ()) 311settings ["enabled" ]["warm" ]= False 312 (self .app .home / "extensions.json" ).write_text (json .dumps (settings )) 313self .assert_no_pings (* self .swarm ()) 314 315# exclusive: writes daemon-wide cache TTL and warmer settings 316@exclusive 317def test_a_session_that_disables_the_warmer_is_not_pinged (self ): 318parent = self .app .session () 319with self .app .api (f"/sessions/ { parent } ?view=configuration" )as response : 320json .load (response ) 321revision = response .headers ["ETag" ] 322with self .app .api ( 323f"/sessions/ { parent } ?view=configuration" , 324 {"selection" : {"extensions" : {"warm" :False }}}, 325method = "PATCH" , 326headers = {"If-Match" :revision }, 327 )as response : 328self .assertFalse ( 329json .load (response )["session" ]["selection" ]["effective" ]["extensions" ][ 330"warm" 331 ] 332 ) 333self .assert_no_pings (* self .swarm (parent )) 334 335# exclusive: writes daemon-wide cache TTL and warmer settings 336@exclusive 337def test_pings_repeat_the_last_request_and_stop_at_the_budget (self ): 338parent ,child = self .swarm () 339before = self .history (parent ) 340 341def two_pings (): 342found = self .pings (self .requests ("/parent/" )) 343return found if len (found )== 2 else None 344 345pings = wait_for (two_pings ) 346turn = self .requests ("/parent/" )[0 ] 347for ping in pings : 348# A ping is the turn's request as sent, with only the output 349# budget lowered — the Responses protocol's minimum. 350self .assertEqual (ping , {** turn ,"max_output_tokens" :16 }) 351 352# The request rows carry the same prefix identity and cache marks as 353# the turn they repeat, and never a transcript row of their own. 354rows = self .rows (parent ) 355self .assertEqual ( 356 [row ["kind" ]for row in rows ], ["turn" ,"background" ,"background" ] 357 ) 358head = rows [0 ] 359for row in rows [1 :]: 360self .assertEqual (row ["head_hash" ],head ["head_hash" ]) 361self .assertEqual (row ["input_count" ],head ["input_count" ]) 362self .assertEqual (row ["replaced_input_count" ],head ["replaced_input_count" ]) 363self .assertEqual (row ["projection_hash" ],head ["projection_hash" ]) 364self .assertEqual (row ["cache_marks" ],head ["cache_marks" ]) 365self .assertEqual (row ["cached_tokens" ]["observed" ],4096 ) 366self .assertEqual (row ["transcript_positions" ], []) 367self .assertEqual (row ["outcome" ],"completed" ) 368self .assertEqual (row ["provider_profile" ],"orchestrator" ) 369self .assertEqual (row ["model" ],"fixture-model" ) 370# A ping paid 4096 cached tokens: the hit the TTL model predicted. 371# Each ping is due an interval after the send before it, timed from 372# the moment the warmer sent it, which its request row stamps a little 373# later: rows apart from the turn's are bounded from the turn's. 374interval = TTL_SECONDS * 900 375self .assertGreaterEqual ( 376milliseconds (rows [1 ]["started_at" ]), 377milliseconds (rows [0 ]["started_at" ])+ interval , 378 ) 379self .assertGreaterEqual ( 380milliseconds (rows [2 ]["started_at" ]), 381milliseconds (rows [0 ]["started_at" ])+ 2 * interval , 382 ) 383 384# The transcript is untouched by warming. 385self .assertEqual (self .history (parent ),before ) 386 387# The budget is spent: no third ping past another interval. 388time .sleep (QUIET_SECONDS ) 389self .assertEqual (len (self .pings (self .requests ("/parent/" ))),2 ) 390 391# Once the scout finishes, nothing new is scheduled either. 392self .gate .set () 393self .app .idle (child ) 394self .app .idle (parent ) 395time .sleep (QUIET_SECONDS ) 396self .assertEqual (len (self .pings (self .requests ("/parent/" ))),2 ) 397# The scout's answer woke the parent with an ordinary turn. 398turns = self .turns ("/parent/" ) 399self .assertEqual (len (turns ),2 ) 400self .assertIn ("scout finished" ,json .dumps (turns [1 ])) 401 402# exclusive: writes daemon-wide cache TTL and warmer settings 403@exclusive 404def test_a_ping_restarts_the_cached_counts_fade (self ): 405parent ,child = self .swarm () 406 407def fade (): 408with self .app .api (f"/sessions/ { parent } ?tail=0" )as response : 409return json .load (response )["usage" ]["cache_fade" ] 410 411# The fixture's clock counts the TTL from the send's start; nothing is 412# left after it. 413turn = self .rows (parent )[0 ] 414expires = milliseconds (turn ["started_at" ])+ TTL_SECONDS * 1000 415self .assertEqual ( 416 [(milliseconds (step ["at" ]),step ["cached_tokens" ])for step in fade ()], 417 [(expires ,0 )], 418 ) 419 420# A ping resends the prefix, so the provider's clock starts over from 421# it: the fade moves to the ping's send, which the session times from 422# just before the request row does. 423ping = wait_for ( 424lambda :next ( 425 (row for row in self .rows (parent )if row ["kind" ]== "background" ),None 426 ) 427 ) 428moved = wait_for ( 429lambda :next ( 430 ( 431milliseconds (step ["at" ]) 432for step in fade () 433if milliseconds (step ["at" ])!= expires 434 ), 435None , 436 ) 437 ) 438self .assertLessEqual ( 439moved ,milliseconds (ping ["started_at" ])+ TTL_SECONDS * 1000 440 ) 441self .assertGreater ( 442moved ,milliseconds (ping ["started_at" ])+ TTL_SECONDS * 1000 - 500 443 ) 444self .gate .set () 445self .app .idle (child ) 446self .app .idle (parent ) 447 448# exclusive: writes daemon-wide cache TTL and warmer settings 449@exclusive 450def test_a_submit_during_warming_answers_and_warming_ends_with_the_children (self ): 451parent ,child = self .swarm () 452 453first = wait_for (lambda :self .pings (self .requests ("/parent/" ))or None ) 454turn = self .requests ("/parent/" )[0 ] 455self .assertEqual (first [0 ], {** turn ,"max_output_tokens" :16 }) 456 457# A real submit while warming gets through and is answered normally. 458self .app .prompt (parent ,"real work now" ).close () 459self .app .idle (parent ) 460self .assertIn ( 461"orchestrator reply" , 462 [ 463part ["text" ] 464for entry in self .history (parent ) 465for part in entry ["content" ] 466if part ["kind" ]== "text" 467 ], 468 ) 469 470# The work that kept the parent warm is done; warming ends with it. 471self .gate .set () 472self .app .idle (child ) 473self .app .idle (parent ) 474settled = len (self .pings (self .requests ("/parent/" ))) 475time .sleep (QUIET_SECONDS ) 476self .assertEqual (len (self .pings (self .requests ("/parent/" ))),settled ) 477# Every ping that did go out repeated a turn's request, budget aside. 478turns = self .turns ("/parent/" ) 479for ping in self .pings (self .requests ("/parent/" )): 480self .assertIn ( 481ping , [{** repeated ,"max_output_tokens" :16 }for repeated in turns ] 482 ) 483 484def assert_warm_until_the_job_ends (self ,remote ): 485parent ,gate = self .job (service = False ,remote = remote ) 486pings = wait_for (lambda :self .pings (self .requests ("/parent/" ))or None ) 487turn = self .turns ("/parent/" )[- 1 ] 488self .assertEqual (pings [0 ], {** turn ,"max_output_tokens" :16 }) 489 490# The job's end wakes the parent with an ordinary turn, and warming 491# ends with the work that kept it up. 492release (gate ) 493wait_for ( 494lambda :any ( 495"background job finished" in json .dumps (request ) 496for request in self .requests ("/parent/" ) 497 ) 498 ) 499self .app .idle (parent ) 500settled = len (self .pings (self .requests ("/parent/" ))) 501time .sleep (QUIET_SECONDS ) 502self .assertEqual (len (self .pings (self .requests ("/parent/" ))),settled ) 503 504def assert_a_service_is_not_warmed_for (self ,remote ): 505parent ,_ = self .job (service = True ,remote = remote ) 506time .sleep (QUIET_SECONDS ) 507self .assertEqual (len (self .requests ("/parent/" )),2 ) 508self .assertEqual ([row ["kind" ]for row in self .rows (parent )], ["turn" ,"turn" ]) 509 510# exclusive: writes daemon-wide cache TTL and warmer settings 511@exclusive 512def test_a_running_job_keeps_the_cache_warm_until_it_finishes (self ): 513self .assert_warm_until_the_job_ends (remote = False ) 514 515# exclusive: writes daemon-wide cache TTL and warmer settings 516@exclusive 517def test_a_remote_job_keeps_the_cache_warm_until_it_finishes (self ): 518self .assert_warm_until_the_job_ends (remote = True ) 519 520# exclusive: writes daemon-wide cache TTL and warmer settings 521@exclusive 522def test_a_service_job_does_not_keep_the_cache_warm (self ): 523self .assert_a_service_is_not_warmed_for (remote = False ) 524 525# exclusive: writes daemon-wide cache TTL and warmer settings 526@exclusive 527def test_a_remote_service_job_does_not_keep_the_cache_warm (self ): 528self .assert_a_service_is_not_warmed_for (remote = True ) 529 530# exclusive: writes daemon-wide cache TTL and warmer settings 531@exclusive 532def test_a_child_waiting_on_its_job_keeps_the_parent_warm (self ): 533parent ,child ,gate = self .child_job () 534# Neither session is in a turn, yet the scout's build will wake it, 535# and its report the parent. 536pings = wait_for (lambda :self .pings (self .requests ("/parent/" ))or None ) 537turns = self .turns ("/parent/" ) 538self .assertEqual (pings [0 ], {** turns [- 1 ],"max_output_tokens" :16 }) 539 540# The build's end wakes the scout, and warming ends with the work that 541# kept it up. 542self .build_finished (gate ) 543self .app .idle (child ) 544self .app .idle (parent ) 545settled = len (self .pings (self .requests ("/parent/" ))) 546time .sleep (QUIET_SECONDS ) 547self .assertEqual (len (self .pings (self .requests ("/parent/" ))),settled ) 548 549def restarted_pings (self ,wait ): 550"""The parent's pings after a restart while it idles on `wait`: a 551job, a remote job, a service, a child, or a child idle on a job.""" 552 (self .app .home / "cache-ttl.json" ).write_text (json .dumps (RESTART_TTL )) 553if wait == "child" : 554parent ,gate = self .swarm () 555elif wait == "child job" : 556parent ,_ ,gate = self .child_job () 557else : 558parent ,gate = self .job ( 559service = wait == "service" ,remote = wait == "remote job" 560 ) 561turn = self .turns ("/parent/" )[- 1 ] 562self .app .restart () 563before = len (self .provider .requests ) 564return ( 565parent , 566gate , 567turn , 568lambda :self .pings ( 569 [ 570record ["request" ] 571for record in self .provider .requests [before :] 572if "/parent/" in record ["path" ] 573 ] 574 ), 575 ) 576 577def assert_a_restart_keeps_warming_for (self ,wait ): 578parent ,gate ,turn ,pings = self .restarted_pings (wait ) 579# The warmer's call lived in memory only; the restarted session 580# rebuilds it from the transcript, exactly as the turn sent it. 581ping = wait_for (pings )[0 ] 582self .assertEqual (ping , {** turn ,"max_output_tokens" :16 }) 583# The provider answers before the ping's request row is written. 584rows = wait_for ( 585lambda : (rows := self .rows (parent ))[- 1 ]["kind" ]== "background" and rows 586 ) 587head = [row for row in rows if row ["kind" ]== "turn" ][- 1 ] 588self .assertEqual (rows [- 1 ]["head_hash" ],head ["head_hash" ]) 589self .assertEqual (rows [- 1 ]["input_count" ],head ["input_count" ]) 590 591release (gate ) 592wait_for ( 593lambda :any ( 594"background job finished" in json .dumps (request ) 595for request in self .requests ("/parent/" ) 596 ) 597 ) 598self .app .idle (parent ) 599 600# exclusive: writes daemon-wide cache TTL and warmer settings, restarts 601@exclusive 602def test_a_restart_keeps_warming_a_session_waiting_on_a_job (self ): 603self .assert_a_restart_keeps_warming_for ("job" ) 604 605# exclusive: writes daemon-wide cache TTL and warmer settings, restarts 606@exclusive 607def test_a_restart_keeps_warming_a_session_waiting_on_a_remote_job (self ): 608# The reattached kernel's hello carries its remote jobs' count. 609self .assert_a_restart_keeps_warming_for ("remote job" ) 610 611# exclusive: shortens the daemon's idle and unload limits, restarts 612@exclusive 613def test_a_parent_waiting_on_its_child_keeps_warming_once_released (self ): 614 (self .app .home / "cache-ttl.json" ).write_text (json .dumps (RESTART_TTL )) 615self .app .restart ( 616prepare = lambda app :app .env .update ( 617ALBEDO_IDLE_SECONDS = "1" ,ALBEDO_UNLOAD_SECONDS = "1" 618 ) 619 ) 620parent ,child = self .swarm () 621with sqlite3 .connect (self .app .home / "albedo.sqlite" )as database : 622 (kernel ,)= database .execute ( 623"SELECT pid FROM kernel_links WHERE session=?" , (parent ,) 624 ).fetchone () 625# The idle sweep takes the kernel long before the first ping is due, 626# and the unload sweep would stop the session with its warmer. 627wait_for (lambda :not alive (kernel )) 628self .assertEqual (self .pings (self .requests ("/parent/" )), []) 629wait_for (lambda :self .pings (self .requests ("/parent/" )),timeout = 40 ) 630self .gate .set () 631self .app .idle (child ) 632self .app .idle (parent ) 633 634# exclusive: writes daemon-wide cache TTL and warmer settings, restarts 635@exclusive 636def test_a_restart_keeps_warming_a_parent_waiting_on_its_child (self ): 637parent ,child ,turn ,pings = self .restarted_pings ("child" ) 638# The restart resumes the scout mid-turn, and its parent with it. 639self .assertEqual (wait_for (pings )[0 ], {** turn ,"max_output_tokens" :16 }) 640self .gate .set () 641self .app .idle (child ) 642self .app .idle (parent ) 643 644# exclusive: writes daemon-wide cache TTL and warmer settings, restarts 645@exclusive 646def test_a_restart_keeps_warming_a_parent_whose_child_waits_on_a_job (self ): 647parent ,gate ,turn ,pings = self .restarted_pings ("child job" ) 648# The scout's kernel comes back running its build, which brings back 649# the parent waiting on its report. 650self .assertEqual (wait_for (pings )[0 ], {** turn ,"max_output_tokens" :16 }) 651self .build_finished (gate ) 652self .app .idle (parent ) 653 654# exclusive: writes daemon-wide cache TTL and warmer settings, restarts 655@exclusive 656def test_a_restart_does_not_warm_a_session_with_only_a_service (self ): 657_ ,_ ,_ ,pings = self .restarted_pings ("service" ) 658time .sleep (RESTART_QUIET_SECONDS ) 659self .assertEqual (pings (), [])