import os,sys,pathlib,subprocess,json,datetime,time,socket,sqlite3,shlex,traceback,signal,shutil
ROOT=pathlib.Path('/tmp/lead-28108-r1/evidence-capture')
side=sys.argv[1]
checkout=pathlib.Path('/home/ubuntu/wt/pr-28108'+('-before' if side=='before' else ''))
cwd=checkout/'apps/server'
stamp=datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%SZ')
run=ROOT/f'{side}-{stamp}';run.mkdir()
transcript=ROOT/f'runtime-{side}-{stamp}.log'
f=transcript.open('w',buffering=1)
def log(label,obj=''):
 line=f'{datetime.datetime.now(datetime.timezone.utc).isoformat()} {label} '+(json.dumps(obj,ensure_ascii=False) if not isinstance(obj,str) else obj)
 f.write(line+'\n');print(line,flush=True)
log('ENV',dict(side=side,sha=subprocess.check_output(['git','rev-parse','HEAD'],cwd=checkout,text=True).strip(),node=subprocess.check_output(['node','--version'],text=True).strip(),codex=subprocess.check_output(['codex','--version'],text=True).strip(),machine='fs-11',os=subprocess.check_output(['uname','-srmo'],text=True).strip(),model='gpt-5.6-sol',run=str(run)))
home=run/'home';userdata=home/'userdata';userdata.mkdir(parents=True)
project=run/'project';project.mkdir()
(project/'README.md').write_text('Synthetic T3 provider-settings evidence fixture.\n')
subprocess.run(['git','init','-q',str(project)],check=True)
subprocess.run(['git','-C',str(project),'add','README.md'],check=True)
subprocess.run(['git','-C',str(project),'-c','user.name=Evidence Fixture','-c','user.email=evidence@example.invalid','commit','-qm','Synthetic fixture seed'],check=True)
wrappers=run/'bin';wrappers.mkdir();wrapperlog=run/'wrapper.log'
real=shutil.which('codex')
for name in ['codex-a','codex-b']:
 script=wrappers/name
 script.write_text('#!/bin/sh\n'+f'printf "%s wrapper={name} pid=%s ppid=%s CODEX_HOME=%s T3_PROOF_MARKER=%s args=" "$(date -u +%Y-%m-%dT%H:%M:%S.%NZ)" "$$" "$PPID" "${{CODEX_HOME-<unset>}}" "${{T3_PROOF_MARKER-<unset>}}" >> '+shlex.quote(str(wrapperlog))+'\n'+f'printf " <%s>" "$@" >> {shlex.quote(str(wrapperlog))}\n'+f'printf "\\n" >> {shlex.quote(str(wrapperlog))}\n'+f'exec {shlex.quote(real)} "$@"\n')
 script.chmod(0o755)
settings={'defaultRuntimeMode':'full-access','providerInstances':{'codex':{'driver':'codex','enabled':True,'config':{'binaryPath':str(wrappers/'codex-a')}}}}
settingsfile=userdata/'settings.json';settingsfile.write_text(json.dumps(settings,indent=2))
(run/'selected-seed.json').write_text(json.dumps(settings,indent=2))
log('SEED',settings)
sock=socket.socket();sock.bind(('127.0.0.1',0));port=sock.getsockname()[1];sock.close()
serverlog=(run/'server-private.log').open('w')
cmd=['node','src/bin.ts','serve','--base-dir',str(home),'--port',str(port),'--host','127.0.0.1',str(project)]
log('SERVER SPAWN',dict(cwd=str(cwd),command=cmd,env={'T3CODE_DEV_SEED':'0'}))
server=subprocess.Popen(cmd,cwd=cwd,env={**os.environ,'T3CODE_DEV_SEED':'0'},stdout=serverlog,stderr=subprocess.STDOUT)
log('SERVER PID',server.pid)
(run/'server.pid').write_text(str(server.pid))
tracked={}
def procstat(pid):
 try:
  raw=pathlib.Path(f'/proc/{pid}/stat').read_text();parts=raw[raw.rindex(')')+2:].split();return int(parts[1]),parts[19]
 except (OSError,ValueError):return None
serverstart=procstat(server.pid)
def snapshot(label):
 log('SNAPSHOT BEGIN',label)
 parents={}
 for p in pathlib.Path('/proc').iterdir():
  if p.name.isdigit():
   st=procstat(int(p.name))
   if st:parents[int(p.name)]=st
 descendants={server.pid}
 for i in range(20):
  new={pid for pid,(ppid,start) in parents.items() if ppid in descendants}
  if new.issubset(descendants):break
  descendants.update(new)
 for pid in sorted(descendants-{server.pid}):
  try:
   argv=pathlib.Path(f'/proc/{pid}/cmdline').read_bytes().replace(b'\0',b' ').decode(errors='replace').strip()
   if 'codex' not in argv or 'app-server' not in argv:continue
   tracked[pid]=parents[pid][1]
   ps=subprocess.check_output(['ps','-p',str(pid),'-o','pid=,ppid=,lstart=,args='],text=True).strip()
   env={x.split('=',1)[0]:x.split('=',1)[1] for x in pathlib.Path(f'/proc/{pid}/environ').read_bytes().decode(errors='replace').split('\0') if x.startswith('CODEX_HOME=') or x.startswith('T3_PROOF_MARKER=')}
   log('PROCESS',dict(ps=ps,env=env))
  except (OSError,subprocess.SubprocessError):pass
 log('WRAPPER LOG',wrapperlog.read_text() if wrapperlog.exists() else '<none>')
 log('SNAPSHOT END',label)
def cli(args,timeout=240,check=True):
 c=['node','src/bin.ts',*args]
 log('DRIVER COMMAND',dict(cwd=str(cwd),argv=c))
 p=subprocess.run(c,cwd=cwd,stdout=subprocess.PIPE,stderr=subprocess.STDOUT,text=True,timeout=timeout,env={**os.environ,'T3CODE_DEV_SEED':'0'})
 for line in p.stdout.splitlines():log('CLI OUTPUT',line)
 log('CLI EXIT',p.returncode)
 if check and p.returncode:raise RuntimeError('CLI failure '+str(p.returncode))
 result=None
 for line in p.stdout.splitlines():
  try:result=json.loads(line)
  except ValueError:pass
 return result
DB=userdata/'statev2.sqlite'
def rows(table,where='',params=()):
 if not DB.exists():return []
 with sqlite3.connect(f'file:{DB}?mode=ro',uri=True) as db:
  try:return [json.loads(r[0]) for r in db.execute('SELECT payload_json FROM orchestration_v2_projection_'+table+' '+where,params)]
  except sqlite3.OperationalError:return []
def projection(label,thread=None):
 out={}
 for table in ['threads','provider_threads','provider_turns','runs','turn_items']:
  vals=rows(table,'WHERE thread_id = ?' if thread else '',(thread,) if thread else ())
  if table=='turn_items':
   vals=[v for v in vals if v.get('type') not in ['user_message','assistant_message']]
  out[table]=vals
 log('PERSISTED PROJECTION '+label,out)
 return out
def until(label,pred,timeout=90):
 deadline=time.monotonic()+timeout
 while time.monotonic()<deadline:
  result=pred()
  if result:
   log('OBSERVED '+label,result);return result
  if server.poll() is not None:raise RuntimeError('Server exited')
  time.sleep(.2)
 log('UNVERIFIED '+label,'condition not observed');return None
def wrapper_count():return len(wrapperlog.read_text().splitlines()) if wrapperlog.exists() else 0
def setcfg(label,config=None,environment=None,globalchange=None,equal=False):
 global settings
 old_count=wrapper_count()
 if config:settings['providerInstances']['codex']['config'].update(config)
 if environment is not None:settings['providerInstances']['codex']['environment']=environment
 if globalchange:settings.update(globalchange)
 # Preserve server-written nonsecret/default fields, mutate only chosen fixture fields.
 try:current=json.loads(settingsfile.read_text())
 except (OSError,ValueError):current={}
 current.update(settings)
 tmp=userdata/'settings-evidence-write.json';tmp.write_text(json.dumps(current,indent=2));tmp.replace(settingsfile)
 log('SETTINGS WRITE '+label,settings)
 if not equal and not globalchange:
  until('settings rebuild launched status probe '+label,lambda: wrapperlog.read_text().splitlines()[old_count:] if wrapper_count()>old_count else None,60)
  # Probe completion distinguishes status-only processes from persistent serving processes.
  newlines=wrapperlog.read_text().splitlines()[old_count:]
  probe_pids=[]
  import re
  for line in newlines:
   m=re.search(r' pid=(\d+)',line)
   if m and '<app-server>' in line:probe_pids.append(int(m.group(1)))
  if probe_pids:until('status probe finished '+label,lambda: {'exitedProbePids':probe_pids} if all(procstat(p) is None for p in probe_pids) else None,60)
 else:
  # An equal instance must not spawn a probe; allow the watcher to consume the write.
  log('WATCHER CONTROL WINDOW',dict(seconds=3,reason='equal/unrelated file write; no provider rebuild expected'))
  time.sleep(3)
 snapshot('settings reconciled '+label)
def start(label,prompt,wait=True,check=True):
 log('ROW',label)
 args=['thread','start','--base-dir',str(home),'--provider','codex','--model','gpt-5.6-sol','--json']
 if wait:args+=['--wait']
 result=cli([*args,str(project),prompt],check=check)
 snapshot(label+' after start')
 if result and result.get('threadId'):projection(label,result['threadId'])
 return result

def send(label,thread,prompt,wait=True,check=True):
 log('ROW',label)
 args=['thread','send','--base-dir',str(home),'--json']
 if wait:args+=['--wait']
 result=cli([*args,thread,prompt],check=check)
 snapshot(label+' after send');projection(label,thread)
 return result
try:
 until('server listening',lambda: (lambda s: s.connect_ex(('127.0.0.1',port))==0)(socket.socket()),120)
 cli(['project','add','--base-dir',str(home),str(project)])
 a=start('F01 cold open','Remember this synthetic marker for later in this conversation: saffron-orbit-28108. Reply with exactly: one')
 t1=a['threadId']
 send('F02 existing reuse',t1,'Reply with exactly: reuse-existing')
 sibling=start('F02 sibling reuse','Remember this synthetic older sibling marker: cobalt-reef-28108. Reply with exactly: sibling-one')
 t2=sibling['threadId']
 setcfg('F03 equal instance config',equal=True)
 send('F03 equal settings reuse',t1,'Reply with exactly: equal-ok')
 setcfg('F03 unrelated global setting',globalchange={'threadTitleInstructions':'Use short titles for synthetic evidence.'})
 start('F03 unrelated reuse','Reply with exactly: unrelated-ok')
 setcfg('F04 binary A to B',config={'binaryPath':str(wrappers/'codex-b')})
 send('F04 F08 existing-thread first resume',t1,'What synthetic marker did I ask you to remember earlier? Reply with only that marker.')
 send('F08 older sibling revisit',t2,'What synthetic marker did I ask you to remember earlier? Reply with only that marker.')
 setcfg('F05 binary B to A',config={'binaryPath':str(wrappers/'codex-a')})
 start('F05 new-thread first','Reply with exactly: new-first-ok')
 setcfg('F06 launch args and environment',config={'launchArgs':'-c model_reasoning_summary=none'},environment=[{'name':'T3_PROOF_MARKER','value':'fixture-F06','sensitive':False}])
 send('F06 existing-thread first',t1,'Reply with exactly: args-existing-ok')
 start('F06 new-thread followup','Reply with exactly: args-new-ok')
 # Busy turn overlap must be established by a persisted running tool item, not a delay.
 busy=start('F09 root starts','Run the shell command sleep 90 and wait for it to finish. Do not run it in the background. When it finishes reply with exactly: long-done',wait=False)
 tx=busy['threadId']
 running=until('F09 root tool running',lambda: [v for v in rows('turn_items','WHERE thread_id = ?',(tx,)) if v.get('status')=='running' and v.get('type') not in ['assistant_message','user_message']],90)
 projection('F09 before settings while busy',tx);snapshot('F09 before settings while busy')
 setcfg('F09 busy binary A to B',config={'binaryPath':str(wrappers/'codex-b')})
 start('F09 sibling while root running','Reply with exactly: busy-sibling-ok')
 projection('F09 after sibling root still running',tx)
 until('F09 root terminal',lambda: [v for v in rows('runs','WHERE thread_id = ?',(tx,)) if v.get('status') in ['completed','failed','interrupted','cancelled']],150)
 cli(['thread','show','--base-dir',str(home),'--json',tx]);snapshot('F09 root finished')
 send('F09 idle open after busy completion',tx,'Reply with exactly: after-busy-ok')
 # Try real manual compaction once. A fast finish is explicitly inconclusive for overlap.
 compact=send('F11 attempt manual compact',t1,'/compact',wait=False,check=False)
 observed=until('F11 compaction in flight',lambda: [v for v in rows('turn_items','WHERE thread_id = ?',(t1,)) if v.get('type')=='compaction' and v.get('status')=='running'],12)
 if observed:
  setcfg('F11 compaction settings transition',config={'binaryPath':str(wrappers/'codex-a')})
  start('F11 sibling attempt','Reply with exactly: compact-sibling-ok')
 else:log('F11 LIVE LIMIT','Could not establish in-flight compaction overlap; focused T-compact is a pinning-only test substitute.')
 cli(['thread','show','--base-dir',str(home),'--json',t1],check=False)
 snapshot('F11 final attempt')
 # Native Codex retained work attempt: untracked shell children are not accepted as proof.
 bg=start('F14 tracked background attempt','Use a tracked background shell task to run sleep 60. Let it continue after your root turn ends and reply immediately with exactly: background-launched. Do not wait for the task. If your tools cannot launch a tracked background task, reply exactly: background-unavailable.',check=False)
 if bg and bg.get('threadId'):
  projection('F14 root terminal background evidence',bg['threadId']);snapshot('F14 root terminal')
 log('F14 LIMIT','Root completion alone or an untracked sleep is not proof. Inspect projected pending tool/subagent events; no direct settings+pending-true deterministic test exists.')
 # Home switch is new-thread-first and last, so no cross-home native migration is implied.
 scratchhome=run/'codex-home-b';scratchhome.mkdir(mode=0o700)
 for name in ['auth.json','config.toml']:
  source=pathlib.Path('/home/ubuntu/.codex')/name
  if source.is_file():shutil.copyfile(source,scratchhome/name);(scratchhome/name).chmod(0o600)
 log('F07 PRIVATE SEED','Copied only auth.json/config.toml where available into scratch CODEX_HOME; contents omitted and excluded from uploads.')
 setcfg('F07 homePath normal to scratch B',config={'homePath':str(scratchhome)})
 start('F07 new-thread first home switch','Reply with exactly: home-switch-ok',check=False)
 snapshot('F07 final serving process')
 log('CAPTURE FINISHED','Primary live matrix complete; F11/F14 limits require explicit reporting.')
except Exception as e:
 log('CAPTURE ERROR',repr(e));log('TRACEBACK',traceback.format_exc())
finally:
 snapshot('before server shutdown')
 log('SERVER STOP',dict(pid=server.pid,signal='SIGTERM',source='captured spawn PID'))
 if server.poll() is None:server.terminate()
 try:server.wait(timeout=20)
 except subprocess.TimeoutExpired:
  log('SERVER STOP ESCALATE',server.pid);server.kill();server.wait(timeout=10)
 # Wrappers and snapshots record identity; clean up only captured child PIDs with same start time.
 if wrapperlog.exists():
  import re
  for line in wrapperlog.read_text().splitlines():
   m=re.search(r' pid=(\d+)',line)
   if m:
    pid=int(m.group(1));st=procstat(pid)
    if st and pid not in tracked:log('CHILD NOT KILLED','Untracked surviving wrapper PID '+str(pid))
 for pid,starttime in tracked.items():
  st=procstat(pid)
  if st and st[1]==starttime:
   log('CAPTURED CHILD STOP',dict(pid=pid,signal='SIGTERM'));os.kill(pid,signal.SIGTERM)
 log('SERVER EXIT',server.returncode)
 snapshot('after server shutdown')
 f.close()
 print('TRANSCRIPT '+str(transcript),flush=True)
