#!/usr/bin/env python3
"""Drive real T3 CLI/Claude wake recovery; run seed, then record the browser while running reproduce.
Arguments: seed|reproduce CHECKOUT DISPOSABLE_HOME SERVER_LOG OUTPUT_DIRECTORY.
CLAUDE_MODEL selects the locally available inexpensive Claude model (kept out of public output).
The home must be a fresh 256 MiB tmpfs; start T3 with T3CODE_DEV_SEED=0.
"""
import datetime,json,os,pathlib,sqlite3,subprocess,sys,time
phase,checkout,home,server_log,out=sys.argv[1:]
out=pathlib.Path(out);out.mkdir(parents=True,exist_ok=True)
def stamp(message):
 line=datetime.datetime.now(datetime.timezone.utc).isoformat(timespec='seconds')+' '+message
 print(line,flush=True)
 with open(out/'runtime.txt','a') as f:f.write(line+'\n')
def cli(*args):
 p=subprocess.run(['node','apps/server/src/bin.ts',*args,'--base-dir',home],cwd=checkout,text=True,capture_output=True)
 if p.returncode:raise RuntimeError('T3 CLI failed (see private raw output)')
 return p.stdout
if phase=='seed':
 workspace=out/'workspace';workspace.mkdir()
 subprocess.run(['git','init','-q',str(workspace)],check=True)
 subprocess.run(['git','-C',str(workspace),'-c','user.name=Evidence','-c','user.email=evidence@example.invalid','commit','--allow-empty','-qm','Initial'],check=True)
 cli('project','add',str(workspace),'--title','Wake recovery')
 data=json.loads(cli('thread','start',str(workspace),'Reply only Ready.','--provider','claudeAgent','--model',os.environ['CLAUDE_MODEL'],'--title','Wake recovery','--json','--wait'))
 (out/'thread.json').write_text(json.dumps(data));stamp('Fresh synthetic thread is ready.')
else:
 tid=json.loads((out/'thread.json').read_text())['threadId']
 def send(label,seconds,report):
  prompt=f'Use the Bash tool to start exactly this command with run_in_background=true: sleep {seconds}; printf "{report}\\n". Do not wait for it, poll it, or read its output. End your turn immediately after starting it, saying only "Background job {label} started." When a task notification arrives later, report its output verbatim.'
  stamp(f'Start background job {label}: sleep {seconds}, then print {report}.')
  data=json.loads(cli('thread','send',tid,prompt,'--wait','--json'))
  (out/f'{label}-turn.json').write_text(json.dumps(data));stamp('Turn ended: '+str(data['reply']))
 def processes():
  found=[]
  for p in pathlib.Path('/proc').iterdir():
   if not p.name.isdigit():continue
   try:
    a=(p/'cmdline').read_bytes().split(b'\0')
    if b'/usr/local/bin/claude' in a and b'--output-format' in a and any(home.encode() in v for v in a):found.append(p.name)
   except OSError:pass
  return found
 send('A',60,'REPORT_A_RECOVERED');original=processes()
 c=sqlite3.connect(home+'/userdata/statev2.sqlite');c.execute('PRAGMA wal_checkpoint(TRUNCATE)').fetchone();c.close()
 try:
  with open(home+'/filler','wb',buffering=0) as f:
   while True:f.write(bytes(4096))
 except OSError as e:
  if e.errno!=28:raise
 stamp('Database filesystem filled; available bytes: '+str(os.statvfs(home).f_bavail*os.statvfs(home).f_frsize))
 deadline=time.monotonic()+150
 while True:
  log=pathlib.Path(server_log).read_text()
  if 'provider-continuation.dispatch-failed' in log and 'database or disk is full' in log:break
  if time.monotonic()>deadline:raise RuntimeError('No disk-full continuation failure observed')
  time.sleep(.5)
 stamp('Real server logged provider-continuation.dispatch-failed: database or disk is full.')
 # Let the failed wake finish emitting before restoring space.
 time.sleep(12)
 os.unlink(home+'/filler');stamp('Space freed; T3 and Claude are not restarted.')
 stamp('Same Claude process still running: '+str(bool(original) and original==processes()))
 send('B',30,'REPORT_B_DELIVERED')
 time.sleep(65)
 data=json.loads(cli('thread','show',tid,'--messages','20','--json'))
 (out/'final-private.json').write_text(json.dumps(data))
 messages=[m['text'] for m in data['messages'] if m['role']=='assistant']
 stamp('Final run count: '+str(data['thread']['runCount']))
 for message in messages:stamp('Assistant: '+message.replace('\n',' '))
 stamp('Same Claude process at finish: '+str(bool(original) and original==processes()))
 stamp('Observation complete.')
