summaryrefslogtreecommitdiff
path: root/9agents/test
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-25 16:40:49 -0300
committerGabriel Schneider <[email protected]>2026-09-25 17:52:33 -0300
commit1c1b192d4ef59199a7196229d56901b1f6512678 (patch)
tree9f6e4c1efcf877d1231aa58975ccd923ad4f2f2e /9agents/test
parent73602127d15d10a1932b6fe916bd18a608054980 (diff)
downloadcloud9-main.tar.gz
cloud9-main.zip
9agents: /active/<h>/<pid>/chat, the conversation as one file per messageHEADmain
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')
-rwxr-xr-x9agents/test/e2e.sh74
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