diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-21 16:49:20 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-21 16:49:20 -0300 |
| commit | dddd556accea6b6ea7802cd3f622f8b3cf8eb43f (patch) | |
| tree | 64a1cc34d44f9be6ec266d852fdef6f5b842d004 /9harness/zmxify | |
| parent | b4db588dd5b92d647b661c2dc17b40925af92348 (diff) | |
| download | cloud9-dddd556accea6b6ea7802cd3f622f8b3cf8eb43f.tar.gz cloud9-dddd556accea6b6ea7802cd3f622f8b3cf8eb43f.zip | |
9harness: /active, and zmxify as a write to a file
The mirror answers what files exist. /active answers what is running:
one directory per live agent, normalized across harnesses, fields as
small text files, synthesized per request.
/active/claude/345104/{pid,cwd,session,via,name,status,title,
model,started,zmx,transcript,agents/}
This is /proc's shape, and deliberately: a directory per object named
by pid under a directory per harness, rather than a compound
`claude-345104` that would make you parse a name to recover a field
that is already the directory above it. There is no `updated` file —
that is the mtime of `transcript`, which stat already carries.
Each harness is asked in its own terms, and the route is reported in
`via` so a wrong guess is visible rather than silent. Claude Code
publishes sessions/<pid>.json itself, with procStart as a pid-reuse
guard, so nothing there is guessed. omp and dsh are found by the
transcript they hold open, omp falling back to the store named after
its cwd. codex's rollout file carries the session id in its *name*, so
its sqlite is never opened. hermes is the one gap and needs none: its
sessions live only in sqlite, and the only hermes processes that run
are the gateway and the dashboard, which are not sessions.
Liveness is /proc/<pid> plus a matching start time: a pid alone is not
an identity. The daemon never lists its own ancestry, so it cannot show
or act on the tree serving the request.
The write path, and why it is a file and not a ctl: writing a zmx
session name into an agent's `zmx` moves it there. The file means which
zmx session this agent lives in, and writing makes that true. A ctl
taking verbs is the ordinary Plan 9 spelling, and an executable script
served in the tree is the spelling zmx's own `attach` uses, but a
script that shells out to a local binary lies over a remote mount — it
would run against a session that is not on the client's machine. A
write is served where the authority is.
Every refusal comes before anything is destroyed: the name must be
zmx's label charset, unused by a live session, and the agent's session
must have resolved, because nothing is killed that has nowhere to come
back to. The command is fixed per harness and no client byte reaches
exec. It is off unless --allow-move: this is the one place the tree is
not read-only, and anything that can mount it could otherwise kill an
agent.
9harness/zmxify replaces the 307-line rc script. It parses no /proc,
opens no fd table and queries no database; it lists /active, offers the
rows to fzf and writes the chosen name. It no longer excludes the
caller's own session, which the old one had to: that script did the
killing itself, so killing its own parent lost the session it was
rescuing. The daemon completes the kill and the re-exec whether or not
the client is still connected — verified by hanging up immediately
after sending the write — so zmxifying the terminal you are sitting in
now works, which is the common case.
--proc DIR is the fixture seam: the scan, the liveness guard, the
exclusions and the ancestry rule are unit-tested against a fake process
tree, never the live one.
Suites: 87/87 root, 48/48 9ns, 26/26 9harness (+5 for the view),
60/60 9proc, 144/144 programs-test, 51+88 9ns integration, 44/0
9harness end-to-end (+16, including a real move against a fake harness
and a fake zmx), 29/0 9proc debug, 213/0 9ns adversarial, freestanding
green.
Diffstat (limited to '9harness/zmxify')
| -rwxr-xr-x | 9harness/zmxify | 176 |
1 files changed, 176 insertions, 0 deletions
diff --git a/9harness/zmxify b/9harness/zmxify new file mode 100755 index 0000000..5512ffa --- /dev/null +++ b/9harness/zmxify @@ -0,0 +1,176 @@ +#!/usr/lib/plan9/bin/rc +# zmxify - move a live harness session into a zmx session +# +# Lists the agents 9harness serves under /active, offers them to fzf, and +# writes the chosen zmx session name into that agent's `zmx` file. The +# daemon does the rest: it kills the harness and re-execs it under zmx +# with its session resumed. +# +# usage: zmxify [-n] [dir] +# dir only agents running there; all of them if omitted +# -n print the plan, write nothing +# +# This is the whole script now. Everything it used to work out for +# itself - which processes are harnesses, which stored session each one +# is writing, whether it already lives in zmx, and how to resume it - +# is a file under /active, resolved by the daemon and covered by its +# tests. Nothing here parses /proc, opens an fd table or queries a +# database, and nothing here kills anything: a name written into `zmx` +# is refused unless the session behind it was resolved first, so a move +# always has somewhere to come back to. + +path=(/usr/bin /bin $path) + +nl=' +' +tab=' ' +dry=() +here=() + +if(~ $1 -n){ + dry=y + shift +} +if(! ~ $#* 0){ + here=`$nl {readlink -f $1} + if(~ $#here 0 || ! test -d $here){ + echo 'zmxify: '^$1^': not a directory' >[1=2] + exit dir + } + shift +} +if(! ~ $#* 0){ + echo 'usage: zmxify [-n] [dir]' >[1=2] + exit usage +} + +mnt=$NINE_MOUNT +if(~ $#mnt 0) + mnt=/mnt/9p +act=$mnt/harness/active +if(! test -d $act){ + echo 'zmxify: no harness fs at '^$act >[1=2] + echo ' start it with: zmx run harness -d 9harness --allow-move' >[1=2] + echo ' and run this inside a 9ns mount (an interactive fish already is)' >[1=2] + exit nofs +} + +# A field that is absent (the harness does not publish it) and one that +# is present but empty (`zmx` on an agent outside zmx) both answer `-`: +# a null list would blow up the concatenations below. +fn field { # field dir name: its contents, or - + fv=() + if(test -e $1/$2) + fv=`$nl {cat $1/$2 >[2]/dev/null} + if(~ $#fv 0) + fv=- + echo $fv +} + +# An agent already under zmx has nowhere to go: that is what this moves +# things into. Its own session is *not* excluded - moving the terminal +# you typed this into is the common case, and it is safe, because the +# daemon does the killing and the re-exec. This script only has to get +# the write out; it may die the instant after, and the move still lands. +# Ancestry is worked out only to know whether we will survive to attach. +fn ancestry { # ancestry pid: it and every parent of it + aup=$1 + while(! ~ $aup 0 1){ + echo $aup + aup=`{sed -n 's/^PPid:[ ]*//p' /proc/$aup/status >[2]/dev/null} + if(~ $#aup 0) + aup=0 + } +} +mine=`$nl {ancestry $pid} + +lines=() +for(h in `{ls $act >[2]/dev/null}){ + for(d in `{ls $act/$h >[2]/dev/null}){ + a=$act/$h/$d + acwd=`$nl {field $a cwd} + asess=`$nl {field $a session} + azmx=`{field $a zmx} + take=() + if(~ $#here 0 || ~ $acwd $here) + take=y + # Already under zmx: there is nothing to move it into. + if(! ~ $azmx -) + take=() + # Its session did not resolve, so the daemon would refuse + # the write; offering it would only produce an error. + if(~ $asess -) + take=() + if(! ~ $#take 0){ + short=$acwd + if(! ~ $#home 0) + short=`$nl {echo -n $acwd | sed 's,^'^$home^',~,'} + avia=`{field $a via} + atitle=`$nl {field $a title} + lines=($lines $d^$tab^$h^$tab^$short^$tab^'via='^$avia^$tab^$asess^$tab^$"atitle) + } + } +} + +scope=(all directories) +if(! ~ $#here 0) + scope=$here +if(~ $#lines 0){ + echo 'zmxify: no movable harness session in '^$"scope >[1=2] + exit 0 +} + +fzfopts=(--no-multi --reverse --height=40% --delimiter=$tab --prompt='zmxify ' --header='move under zmx ['^$"scope^']'^$nl^'pid harness dir via session title') +sel=`$nl {{for(l in $lines) echo $l} | fzf $fzfopts} +if(~ $#sel 0){ + echo 'zmxify: nothing picked' >[1=2] + exit 0 +} + +pick=`{echo $sel | awk '{print $1}'} +harness=`{echo $sel | awk -F$tab '{print $2}'} +a=$act/$harness/$pick +if(! test -d $a){ + echo 'zmxify: '^$harness^' pid '^$pick^' is gone' >[1=2] + exit gone +} +pickcwd=`$nl {field $a cwd} + +# A zmx session name nobody is using yet. zmx posts every session into +# the registry, so the registry is what says which names are taken. +zbase=`{basename $pickcwd | tr -c 'A-Za-z0-9'^$nl -} +if(~ $#zbase 0) + zbase=session +zname=$harness^-^$zbase +reg=$XDG_RUNTIME_DIR^/9p/zmx +if(~ $#XDG_RUNTIME_DIR 0) + reg=/run/user/^`{id -u}^/9p/zmx +n=() +while(test -e $reg/$zname){ + n=($n x) + zname=$harness^-^$zbase^-^$#n +} + +if(! ~ $#dry 0){ + echo 'move ' $harness 'pid' $pick 'in' $pickcwd + echo 'session' `{field $a via} `{field $a session} + echo 'write ' $zname '>' $a/zmx + echo 'then ' zmx attach $zname + exit 0 +} + +echo 'zmxify: moving' $harness 'pid' $pick 'into zmx session' $zname +# Moving something we hang off means this shell goes with it. Say the +# attach line first, because there may be no `we` left to say it after. +if(~ $pick $mine) + echo 'zmxify: that is this terminal; it will drop. reattach with: zmx attach '^$zname +# The write returns when the new session is up, or fails having changed +# nothing it could avoid changing. The daemon owns the kill and the +# re-exec, so the move lands even if this process dies mid-write. +if(! echo $zname > $a/zmx){ + echo 'zmxify: the move was refused' >[1=2] + exit move +} +if(~ $pick $mine) + exit 0 +exec zmx attach $zname |
