← Commit history

Recognize Codex queue auto-start by exact client message identity

John Lauer ·47c8ec5a4b · ·parent eb59956
3 files changed +26−3
docs/codex-supervisor.md+1−1
@@ -23,4 +23,4 @@ On **any** user pause or cancellation, the AI must run `pause` immediately befor  State and receipts live in `<run>/supervisor/{status,state,pause}.json` and `events.jsonl`. A process PID alone is not proof it is alive: check the last check time and the process. A `start-acknowledged` receipt is not proof the model completed subsequent work. Verify the next turn in the same conversation. No engineering gate, board or acceptance limit is changed by this helper. -Validation: ten policy/dispatch tests cover active/approval states, explicit pause, failed/interrupted turns, completion, malformed guards, queue interference, duplicate suppression, uncertain dispatch, state races and no-progress. Live metadata discovery was verified against Codex 0.154.0-alpha.6.2. End-to-end automatic continuation remains unverified until an actual completed turn is followed by a receipt and new turn in that same thread. Source availability, local activation and a packaged release are separate claims.+Validation: eleven policy/dispatch tests cover active/approval states, explicit pause, failed/interrupted turns, completion, malformed guards, queue interference, duplicate suppression, uncertain dispatch, state races and no-progress. Live metadata discovery was verified against Codex 0.154.0-alpha.6.2. Live continuation was observed in the same electronic-load thread: queue/add auto-started its next turn, carrying the exact client message ID. The initial observer incorrectly classified that as a race; it now attributes its own auto-start by client ID, covered by an additional regression test. Source availability, local activation and a packaged release are separate claims.
tools/continuation/supervisor.py+10
@@ -155,6 +155,16 @@ def check(run, directory, thread, state):         write(directory/'state.json', state)         # A newer human turn may have started meanwhile; remove only our item.         latest = client.rpc('thread/read', {'threadId': thread, 'includeTurns': False})['thread']['status']+        # queue/add may auto-start immediately in the IDE. Attribute activity+        # by the exact client ID, not by timing or an assumption of a race.+        if latest['type'] == 'active' and not (directory/'pause.json').exists():+            recent = client.rpc('thread/turns/list', {'threadId': thread, 'limit': 1,+                                'sortDirection': 'desc', 'itemsView': 'summary'})['data']+            if recent and any(item.get('type') == 'userMessage'+                              and item.get('clientId') == state['dispatchId']+                              for item in recent[0].get('items', [])):+                state.update(dispatch='auto-start-verified', startedTurn=recent[0]['id'])+                return 'continuation-dispatched', state         if latest['type'] != 'idle' or (directory/'pause.json').exists():             client.rpc('thread/queue/delete', {'threadId': thread, 'queuedSubmissionId': queued['id']})             state['dispatch'] = 'removed-after-state-change'
tools/continuation/test_supervisor.py+15−2
@@ -39,6 +39,7 @@ class FakeClient:     race=False     read_count=0     fail=False+    auto=False     def __init__(self,*args): pass     def close(self): pass     def rpc(self,method,params):@@ -46,7 +47,11 @@ class FakeClient:         if method=='thread/read':             type(self).read_count+=1             return {'thread':{'status':{'type':'active' if self.race and self.read_count>=3 else 'idle'}}}-        if method=='thread/turns/list':return {'data':[{'id':'t','status':'completed'}]}+        if method=='thread/turns/list':+            if self.auto and self.read_count>=3:+                sent=next(p for m,p in self.calls if m=='thread/queue/add')+                return {'data':[{'id':'own-next-turn','status':'inProgress','items':[{'type':'userMessage','clientId':sent['clientUserMessageId']}]}]}+            return {'data':[{'id':'t','status':'completed'}]}         if method=='thread/queue/list':return {'data':[]}         if method=='thread/queue/add':             if self.fail:raise TimeoutError('uncertain dispatch')@@ -55,7 +60,7 @@ class FakeClient:  class DispatchTests(unittest.TestCase):     def setUp(self):-        FakeClient.calls=[];FakeClient.race=False;FakeClient.read_count=0;FakeClient.fail=False+        FakeClient.calls=[];FakeClient.race=False;FakeClient.read_count=0;FakeClient.fail=False;FakeClient.auto=False     def run_check(self, path, state):         with patch.object(s,'discover',return_value=({'socket':'test'},{})), patch.object(s,'Codex',FakeClient), patch.object(s,'guard',return_value={'decision':'continue','mayStop':False}), patch.object(s,'progress_hash',return_value='same'):             return s.check(path,path,'exact-thread',state)@@ -82,6 +87,14 @@ class DispatchTests(unittest.TestCase):             self.assertEqual(action,'paused-dispatch-race')             self.assertIn(('thread/queue/delete',{'threadId':'exact-thread','queuedSubmissionId':'q'}),FakeClient.calls)             self.assertFalse(any(m=='thread/queue/start' for m,_ in FakeClient.calls))+    def test_auto_start_is_recognized_without_deleting_or_restarting(self):+        with tempfile.TemporaryDirectory() as d:+            FakeClient.race=True;FakeClient.auto=True+            action,state=self.run_check(Path(d),{})+            self.assertEqual(action,'continuation-dispatched')+            self.assertEqual(state['dispatch'],'auto-start-verified')+            self.assertEqual(state['startedTurn'],'own-next-turn')+            self.assertFalse(any(m in ['thread/queue/start','thread/queue/delete'] for m,_ in FakeClient.calls))     def test_pause_and_no_progress(self):         with tempfile.TemporaryDirectory() as d:             p=Path(d);s.write(p/'pause.json',{})