diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-25 16:40:49 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-25 17:52:33 -0300 |
| commit | 1c1b192d4ef59199a7196229d56901b1f6512678 (patch) | |
| tree | 9f6e4c1efcf877d1231aa58975ccd923ad4f2f2e /9agents/test/e2e.sh | |
| parent | 73602127d15d10a1932b6fe916bd18a608054980 (diff) | |
| download | cloud9-main.tar.gz cloud9-main.zip | |
Each message of a claude or codex agent's transcript is a file named
<id>-<kind>: the id is its position in the transcript from 0, as 8
digits so the names sort; the kind is user, assistant, thinking, system
or agent, and a tool call is named after its tool (00000002-bash) and
its result after the call (00000003-bash-result). A file is the message
as text (a tool call: its name, then its input); its mtime is when it
was said.
chat.zig indexes a transcript incrementally, keyed by (dev, ino) and
checked by birth time and a hash of its first and last indexed bytes, so
a file rewritten in place is indexed again. It holds offsets, never
bytes: every read reads the file, and a read resumes where the last one
stopped so a long message is not decoded from its start each time. The
indexes and the runner live in lazily backed mappings (NORESERVE,
NOHUGEPAGE), and the fid table is 32768 so a kernel mount can hold every
message of a long chat.
A 9agents started inside a user namespace (from a 9ns-wrapped terminal)
cannot read /proc/<pid>/fd of processes outside it, so the fd route
never resolved codex there; /active now also finds a codex session
through the thread writer locks it holds (/proc/locks), via = lock.
Fixes to /active found on the way: a fifo in place of a transcript or
record no longer blocks the daemon; a transcript is opened from its
pinned root a component at a time, so a symlinked directory or a
sessionId with .. cannot reach outside it; slot generations are 24 bits,
so a stale node id cannot come to name another agent; listing a harness
rescans; an agent that had not resolved yet is asked again.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Diffstat (limited to '9agents/test/e2e.sh')
| -rwxr-xr-x | 9agents/test/e2e.sh | 74 |
1 files changed, 74 insertions, 0 deletions
diff --git a/9agents/test/e2e.sh b/9agents/test/e2e.sh index fa41bef..3413285 100755 --- a/9agents/test/e2e.sh +++ b/9agents/test/e2e.sh @@ -242,6 +242,19 @@ AGENT_START=$(sed 's/.*) //' "/proc/$AGENT/stat" | awk '{print $20}') AGENT_CWD=$(readlink "/proc/$AGENT/cwd") printf '{"pid":%d,"sessionId":"sess-e2e","cwd":"%s","name":"e2e-agent","status":"idle","procStart":"%s"}\n' \ "$AGENT" "$AGENT_CWD" "$AGENT_START" > "$ACT/home/.claude/sessions/$AGENT.json" +# Its transcript, for the chat. Claude Code's store is named after the cwd, +# every byte that is not a letter or a digit dashed; it has to exist before +# the scan that finds the agent, as it does for a real one. +SLUG=$(printf %s "$AGENT_CWD" | sed 's/[^A-Za-z0-9]/-/g') +CT="$ACT/home/.claude/projects/$SLUG/sess-e2e.jsonl" +mkdir -p "$(dirname "$CT")" +cat > "$CT" <<'CEOF' +{"type":"user","message":{"role":"user","content":"list the files"},"timestamp":"2026-09-24T12:51:26.691Z"} +{"type":"assistant","message":{"content":[{"type":"tool_use","id":"t1","name":"Bash","input":{"command":"ls"}}]}} +{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"t1","content":"a\nb"}]}} +{"type":"attachment","attachment":{"type":"reminder"}} +{"type":"assistant","message":{"content":[{"type":"text","text":"two files: \"a\" and \"b\""}]}} +CEOF act_roots() { echo --root "claude=$ACT/home/.claude" --root "codex=$ACT/home/.codex" \ @@ -257,6 +270,67 @@ expect_eq "its session comes from the harness's own record" "sess-e2e" "$(p9 "$A expect_eq "the route taken is named" "registry" "$(p9 "$ACT/ro.sock" read "/active/claude/$AGENT/via")" expect_eq "the harness's own name is served" "e2e-agent" "$(p9 "$ACT/ro.sock" read "/active/claude/$AGENT/name")" expect_eq "the harness's own status is served" "idle" "$(p9 "$ACT/ro.sock" read "/active/claude/$AGENT/status")" + +# The chat: the transcript as one file per message, named <id>-<kind>. +CHAT="/active/claude/$AGENT/chat" +expect_contains "the agent has a chat" "chat" "$(p9 "$ACT/ro.sock" ls "/active/claude/$AGENT")" +expect_eq "the chat lists its messages by id and kind, tools by name" "00000000-user 00000001-bash 00000002-bash-result 00000003-assistant" \ + "$(p9 "$ACT/ro.sock" ls "$CHAT" | tr '\n' ' ' | sed 's/ $//')" +expect_eq "a message reads as its text" "list the files" "$(p9 "$ACT/ro.sock" read "$CHAT/00000000-user")" +expect_eq "a tool call reads as its name, then its input" 'Bash +{"command":"ls"}' "$(p9 "$ACT/ro.sock" read "$CHAT/00000001-bash")" +expect_eq "escapes are undone" 'two files: "a" and "b"' "$(p9 "$ACT/ro.sock" read "$CHAT/00000003-assistant")" +# The length a stat reports must be what a read returns, or a kernel +# mount would cut the file short. +expect_eq "a message reads its whole length" "4" "$(p9 "$ACT/ro.sock" read "$CHAT/00000002-bash-result" | wc -c)" +expect_eq "a message's length is what reading it returns" "4" \ + "$(p9 "$ACT/ro.sock" ls -l "$CHAT/00000002-bash-result" | awk '{print $6}')" +p9 "$ACT/ro.sock" read "$CHAT/00000000-assistant" >/dev/null 2>&1 \ + && fail "a name with the wrong kind is no file" || pass "a name with the wrong kind is no file" +echo '{"type":"user","message":{"content":"thanks"}}' >> "$CT" +expect_eq "a message written later takes the next id" "00000004-user" "$(p9 "$ACT/ro.sock" ls "$CHAT" | tail -1)" +expect_eq "and reads at once" "thanks" "$(p9 "$ACT/ro.sock" read "$CHAT/00000004-user")" + +# Through the kernel: a mount holds a fid for every inode it caches, and a +# long chat is thousands of them. Every message must stat, read and match. +if [ -n "${NS:-}" ]; then + for i in $(seq 1 1000); do printf '{"type":"user","message":{"content":"m%d"}}\n' "$i"; done >> "$CT" + KOUT=$("$NS" --unix "$ACT/ro.sock" --name a -- sh -c ' + C=$NINE_MOUNT/active/claude/'"$AGENT"'/chat + echo "files=$(ls "$C" | wc -l)" + ls -l "$C" >/dev/null 2>&1 && echo "lsl=ok" || echo "lsl=failed" + echo "read=$(cat "$C"/* 2>/dev/null | wc -c)" + echo "stat=$(stat -c %s "$C"/* | paste -sd+ | bc)"' 2>&1) + expect_contains "a 1005-message chat lists through a kernel mount" "files=1005" "$KOUT" + expect_contains "and every message stats there" "lsl=ok" "$KOUT" + expect_eq "and reads there exactly as long as it stats" \ + "$(echo "$KOUT" | sed -n 's/^stat=//p')" "$(echo "$KOUT" | sed -n 's/^read=//p')" +else + echo "SKIP: kernel-mount chat check needs 9ns" +fi + +# A codex agent: found by the rollout it holds open, which the fake holds +# as its stdin. +RO="$ACT/home/.codex/sessions/2026/09/21/rollout-2026-09-21T10-00-00-01a0bcd2-5653-7cc0-aa36-676f6467a72a.jsonl" +mkdir -p "$(dirname "$RO")" +cat > "$RO" <<'REOF' +{"timestamp":"2026-09-21T10:00:00.000Z","type":"session_meta","payload":{"id":"01a0bcd2-5653-7cc0-aa36-676f6467a72a"}} +{"timestamp":"2026-09-21T10:00:01.000Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"hi codex"}]}} +{"timestamp":"2026-09-21T10:00:01.000Z","type":"event_msg","payload":{"type":"user_message","message":"hi codex"}} +{"timestamp":"2026-09-21T10:00:02.000Z","type":"response_item","payload":{"type":"function_call","name":"shell","arguments":"{\"command\":[\"ls\"]}","call_id":"c1"}} +{"timestamp":"2026-09-21T10:00:03.000Z","type":"response_item","payload":{"type":"function_call_output","call_id":"c1","output":"x\n"}} +REOF +cp /bin/sleep "$ACT/bin/codex" +"$ACT/bin/codex" 600 < "$RO" & +CODEX=$! +PIDS+=("$CODEX") +sleep 0.3 +expect_eq "a codex agent's chat comes from the rollout it holds" "00000000-user 00000001-shell 00000002-shell-result" \ + "$(p9 "$ACT/ro.sock" ls "/active/codex/$CODEX/chat" | tr '\n' ' ' | sed 's/ $//')" +expect_eq "its tool call reads as name and arguments" 'shell +{"command":["ls"]}' "$(p9 "$ACT/ro.sock" read "/active/codex/$CODEX/chat/00000001-shell")" +kill "$CODEX" 2>/dev/null + if echo "nope" | p9 "$ACT/ro.sock" write "/active/claude/$AGENT/zmx" 2>/dev/null; then fail "without --allow-move the tree stays read-only" "the write was accepted" else |
