From 05517c384a155395abb986f0acae9d20b750ed7b Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Tue, 4 Aug 2026 16:57:01 +0200 Subject: [PATCH] o: simplify path resolution, remove auto-session/agent - Remove auto-resolution of session/agent (was slow, unexpected) - Path classification determines required context level immediately - Errors are fast and actionable with clear fix instructions - Root-level paths work without any context - o env now just prints export statements without requiring context --- data/scripts/o | 388 +++++++++++++++++++++++++++---------------------- 1 file changed, 213 insertions(+), 175 deletions(-) diff --git a/data/scripts/o b/data/scripts/o index a397106..28b13d5 100755 --- a/data/scripts/o +++ b/data/scripts/o @@ -1,106 +1,155 @@ #!/bin/bash # o — unified CLI for the ollie 9P namespace. # +# Path resolution: +# Paths are resolved based on context: +# - Absolute paths (starting with session/, agents, models, etc.) → used as-is +# - Relative paths with $session set but no $agent → session/$session/ +# - Relative paths with both $session and $agent → session/$session/agent/$agent/ +# - No context set → path used as-is (root-relative) +# # Subcommands: -# read Read a file from the active session/agent +# read Read a file +# write [data] Write to a file (args or stdin) +# readloop Read in a loop (re-reads after each return) # ls [path] List namespace entries # ctl Send a control command (stop, compact, kill, ...) -# prompt Interactive REPL for submitting prompts -# tail Tail chat output from the active agent +# prompt Interactive REPL for submitting prompts (requires agent) +# tail Tail chat output (requires agent) +# tui Launch tmux TUI (requires agent) # env Print resolved session/agent environment +# stop Interrupt running agent (requires agent) +# kill Kill session (requires session) # # Environment: # session Session name (set via 'eval $(o env)') # agent Agent name -# -# If session/agent are not set and there is exactly one session, it is -# selected automatically. If multiple exist, lists them and exits. set -euo pipefail -# ── Resolve session and agent ──────────────────────────────────── +# ── Path classification ───────────────────────────────────────── -resolve_session() { +# Root-level entries (no context needed) +ROOT_ENTRIES="session elevate agents backends models help ctl eventwait complete generate route aliases tools" + +# Session-level files (exist directly under session/{name}/) +SESSION_FILES="env paused connected ctl name id elevate" + +# Agent-level files (exist under session/{name}/agent/{name}/) +AGENT_FILES="prompt prompt.prev fifo.in fifo.out chat connection state statewait log offset plan cfg ctl cwd usage cost ctxsz models systemprompt context tail tools name id proc" + +is_root_path() { + local path="$1" + local first="${path%%/*}" + for entry in $ROOT_ENTRIES; do + [[ "$first" == "$entry" ]] && return 0 + done + return 1 +} + +is_session_file() { + local name="$1" + for f in $SESSION_FILES; do + [[ "$name" == "$f" ]] && return 0 + done + [[ "$name" == "agent" || "$name" == agent/* ]] && return 0 + return 1 +} + +is_agent_file() { + local name="$1" + local base="${name%%/*}" + for f in $AGENT_FILES; do + [[ "$base" == "$f" ]] && return 0 + done + return 1 +} + +# ── Path resolution with validation ───────────────────────────── + +# Resolve a path based on current context, with meaningful errors. +# Returns 0 on success (path on stdout), 1 on error (message on stderr). +resolve_path() { + local path="$1" + + # Root-level paths used as-is + if is_root_path "$path"; then + echo "$path" + return 0 + fi + + # Get context local s="${session:-}" - if [[ -n "$s" ]]; then - echo "$s" - return + local a="${agent:-}" + + # Determine what level this path requires + local base="${path%%/*}" + + # Check if it's an agent-level file + if is_agent_file "$base"; then + if [[ -z "$s" ]]; then + echo "error: '$path' requires session context" >&2 + echo " export session= or use full path: session//agent//$path" >&2 + return 1 + fi + if [[ -z "$a" ]]; then + echo "error: '$path' requires agent context" >&2 + echo " export agent= or use full path: session/$s/agent//$path" >&2 + return 1 + fi + echo "session/$s/agent/$a/$path" + return 0 fi - # Count sessions from idx - local idx - idx=$(ollie-9p read session/idx 2>/dev/null || true) - if [[ -z "$idx" ]]; then - return + + # Check if it's a session-level file/dir + if is_session_file "$base"; then + if [[ -z "$s" ]]; then + echo "error: '$path' requires session context" >&2 + echo " export session= or use full path: session//$path" >&2 + return 1 + fi + echo "session/$s/$path" + return 0 fi - local names - names=$(echo "$idx" | cut -f1 | sort -u) - local count - count=$(echo "$names" | wc -l) - if [[ $count -eq 1 ]]; then - echo "$names" + + # Unknown relative path — try with whatever context we have + if [[ -n "$s" && -n "$a" ]]; then + echo "session/$s/agent/$a/$path" + elif [[ -n "$s" ]]; then + echo "session/$s/$path" else - echo "error: multiple sessions active, set one:" >&2 - echo "$names" | sed 's/^/ /' >&2 - echo "" >&2 - echo " export session= OR eval \$(o env )" >&2 - exit 1 + echo "$path" fi + return 0 } -resolve_agent() { - local s="$1" - local a="${agent:-}" - if [[ -n "$a" ]]; then - echo "$a" - return - fi - if [[ -z "$s" ]]; then - return - fi - # List agents in session - local agents - agents=$(ollie-9p ls "session/$s/agent" 2>/dev/null | grep -v '^new$' || true) - if [[ -z "$agents" ]]; then - return - fi - local count - count=$(echo "$agents" | wc -l) - if [[ $count -eq 1 ]]; then - echo "$agents" - else - echo "error: multiple agents in session '$s', set one:" >&2 - echo "$agents" | sed 's/^/ /' >&2 - echo "" >&2 - echo " export agent= OR eval \$(o env $s )" >&2 - exit 1 - fi -} +# ── Context requirements for specific commands ────────────────── require_session() { - local s - s=$(resolve_session) + local s="${session:-}" if [[ -z "$s" ]]; then - echo "error: no session active (start one, or export session=)" >&2 - exit 1 + echo "error: no session set" >&2 + echo " export session=" >&2 + return 1 fi echo "$s" } require_agent() { local s="$1" - local a - a=$(resolve_agent "$s") + local a="${agent:-}" if [[ -z "$a" ]]; then - echo "error: no agent set (export agent=)" >&2 - exit 1 + echo "error: no agent set" >&2 + echo " export agent=" >&2 + return 1 fi echo "$a" } agent_path() { local s a - s=$(require_session) - a=$(require_agent "$s") + s=$(require_session) || return 1 + a=$(require_agent "$s") || return 1 echo "session/$s/agent/$a" } @@ -108,64 +157,62 @@ agent_path() { cmd_read() { if [[ $# -lt 1 ]]; then - echo "usage: o read [session [agent]]" >&2 + echo "usage: o read " >&2 exit 1 fi - local file="$1"; shift - [[ $# -ge 1 ]] && export session="${1}" && shift - [[ $# -ge 1 ]] && export agent="${1}" && shift local path - path=$(agent_path) - ollie-9p read "$path/$file" + path=$(resolve_path "$1") || exit 1 + ollie-9p read "$path" } cmd_write() { if [[ $# -lt 1 ]]; then - echo "usage: o write [data...]" >&2 - echo " writes stdin (or args) to session/\$s/agent/\$a/" >&2 + echo "usage: o write [data...]" >&2 exit 1 fi local file="$1"; shift local path - path=$(agent_path) + path=$(resolve_path "$file") || exit 1 if [[ $# -gt 0 ]]; then - printf '%s\n' "$*" | ollie-9p write "$path/$file" + printf '%s\n' "$*" | ollie-9p write "$path" else - ollie-9p write "$path/$file" + ollie-9p write "$path" fi } cmd_ls() { - local s - s=$(resolve_session) || true if [[ $# -ge 1 ]]; then - local target="$1"; shift - if [[ "$target" == session/* || "$target" == elevate/* ]]; then - ollie-9p ls "$target" - elif [[ -n "$s" ]]; then - local a - a=$(resolve_agent "$s") || true - if [[ -n "$a" ]]; then - ollie-9p ls "session/$s/agent/$a/$target" - else - ollie-9p ls "session/$s/$target" - fi - else - ollie-9p ls "$target" - fi - elif [[ -n "$s" ]]; then - local a - a=$(resolve_agent "$s") || true - if [[ -n "$a" ]]; then - ollie-9p ls "session/$s/agent/$a" - else - ollie-9p ls "session/$s" - fi + local path + path=$(resolve_path "$1") || exit 1 + ollie-9p ls "$path" else - ollie-9p ls session + # No arg: list based on context level + local s="${session:-}" + local a="${agent:-}" + + if [[ -n "$s" && -n "$a" ]]; then + ollie-9p ls "session/$s/agent/$a" + elif [[ -n "$s" ]]; then + ollie-9p ls "session/$s" + else + ollie-9p ls "" + fi fi } +cmd_readloop() { + if [[ $# -lt 1 ]]; then + echo "usage: o readloop " >&2 + exit 1 + fi + local file="$1"; shift + local path + path=$(resolve_path "$file") || exit 1 + while true; do + ollie-9p read "$path" || break + done +} + cmd_ctl() { if [[ $# -lt 1 ]]; then echo "usage: o ctl [args...]" >&2 @@ -173,15 +220,16 @@ cmd_ctl() { exit 1 fi local path - path=$(agent_path) + path=$(agent_path) || exit 1 echo "$*" | ollie-9p write "$path/ctl" } cmd_prompt() { - local path s a - path=$(agent_path) - s=$(require_session) - a=$(require_agent "$s") + local s a path + s=$(require_session) || exit 1 + a=$(require_agent "$s") || exit 1 + path="session/$s/agent/$a" + echo "[$s/$a]" echo "Enter prompts (blank line sends, Ctrl+D exits)." echo "Slash commands: /stop /compact /kill /clear /model /backend /cwd /help" @@ -196,7 +244,6 @@ cmd_prompt() { prompt_str=" " fi if ! IFS= read -e -r -p "$prompt_str" line; then - # EOF — send remaining buffer if any if [[ -n "$buf" ]]; then history -s "$buf" printf '%s\n' "$buf" | ollie-9p write "$path/prompt" @@ -204,7 +251,6 @@ cmd_prompt() { printf "\n" break fi - # Slash commands (only when buffer is empty — first line) if [[ -z "$buf" && "$line" == /* ]]; then history -s "$line" local cmd="${line#/}" @@ -230,7 +276,6 @@ cmd_prompt() { continue fi if [[ -z "$line" && -n "$buf" ]]; then - # Blank line sends accumulated buffer history -s "$buf" printf '%s\n' "$buf" | ollie-9p write "$path/prompt" buf="" @@ -248,88 +293,68 @@ cmd_prompt() { cmd_tail() { local path - path=$(agent_path) + path=$(agent_path) || exit 1 exec ollie-9p read "$path/chat" } -cmd_readloop() { - if [[ $# -lt 1 ]]; then - echo "usage: o readloop " >&2 - echo " read in a loop — re-reads after each return" >&2 - exit 1 - fi - local file="$1"; shift - local path - path=$(agent_path) - while true; do - ollie-9p read "$path/$file" || break - done -} - cmd_env() { - # Accept optional positional args to override - [[ $# -ge 1 ]] && export session="${1}" && shift - [[ $# -ge 1 ]] && export agent="${1}" && shift - - local s a - s=$(require_session) - a=$(require_agent "$s") - echo "export session=$s" - echo "export agent=$a" + # Accept optional positional args to set context + if [[ $# -ge 1 ]]; then + echo "export session=$1" + shift + elif [[ -n "${session:-}" ]]; then + echo "export session=$session" + fi + if [[ $# -ge 1 ]]; then + echo "export agent=$1" + elif [[ -n "${agent:-}" ]]; then + echo "export agent=$agent" + fi } cmd_stop() { local path - path=$(agent_path) + path=$(agent_path) || exit 1 echo "stop" | ollie-9p write "$path/ctl" } cmd_kill() { local s - s=$(require_session) + s=$(require_session) || exit 1 echo "kill" | ollie-9p write "session/$s/ctl" } cmd_tui() { local s a - s=$(require_session) - a=$(require_agent "$s") + s=$(require_session) || exit 1 + a=$(require_agent "$s") || exit 1 local tmux_session="o-${s}" - # Subcommand: o tui exit if [[ "${1:-}" == "exit" ]]; then tmux kill-session -t "$tmux_session" 2>/dev/null && echo "[killed $tmux_session]" || echo "no tui session: $tmux_session" return fi - # Kill existing tmux session if present tmux kill-session -t "$tmux_session" 2>/dev/null || true - # Create new detached tmux session - # Pane 0: chat tail (left, large) tmux new-session -d -s "$tmux_session" -c "$HOME" -x 200 -y 50 \ -e "session=$s" -e "agent=$a" tmux send-keys -t "$tmux_session" "o tail" Enter - # Pane 1: state monitor (top-right) tmux split-window -t "$tmux_session" -h -l 40 \ -e "session=$s" -e "agent=$a" tmux send-keys -t "$tmux_session" "o readloop statewait" Enter - # Pane 2: spare shell (bottom-right, below state) tmux split-window -t "$tmux_session" -v \ -e "session=$s" -e "agent=$a" - # Pane 3: prompt REPL (bottom, full width) tmux split-window -t "$tmux_session:0.0" -v -l 8 \ -e "session=$s" -e "agent=$a" tmux send-keys -t "$tmux_session" "o prompt" Enter - # Focus the prompt pane tmux select-pane -t "$tmux_session:0.3" - # Attach exec tmux attach -t "$tmux_session" } @@ -337,27 +362,40 @@ cmd_help() { cat <<'EOF' usage: o [args...] -commands: - read Read a file from the active agent's namespace - write [data] Write to a file (args or stdin) - readloop Read in a loop (re-reads after each return) - ls [path] List namespace entries (defaults to agent dir) - ctl Send control command (stop, compact, kill, ...) - stop Interrupt the running agent - kill Kill the active session - prompt Interactive multi-line prompt REPL - tail Stream chat output - tui Launch tmux layout (chat + prompt + state + shell) - tui exit Kill the tmux TUI session - env [session] [agent] Print session/agent env vars (eval-able) - help Show this help +Path resolution: + - Root paths (session/*, agents, models, etc.) → used as-is + - Agent files (chat, state, prompt, etc.) → require $session + $agent + - Session files (env, ctl, name, etc.) → require $session -environment: - session Active session name - agent Active agent name +Commands: + read Read a file + write [data] Write to a file (args or stdin) + readloop Read in a loop (re-reads after return) + ls [path] List namespace entries -If only one session/agent exists, it is auto-selected. -If multiple exist, you must choose: eval $(o env [agent]) + ctl Send control command to agent + stop Interrupt running agent + kill Kill active session + + prompt Interactive multi-line prompt REPL + tail Stream chat output + tui Launch tmux layout + tui exit Kill tmux TUI session + + env [session] [agent] Print export statements for context + help Show this help + +Environment: + session Session name + agent Agent name + +Examples: + o ls # list at current context level + o ls session # list all sessions + o read models # read root-level file + export session=foo agent=bar # set context + o read chat # read agent's chat + o readloop statewait # blocks until agent state changes EOF } @@ -370,17 +408,17 @@ fi subcmd="$1"; shift case "$subcmd" in - read) cmd_read "$@" ;; - write) cmd_write "$@" ;; + read) cmd_read "$@" ;; + write) cmd_write "$@" ;; readloop) cmd_readloop "$@" ;; - ls) cmd_ls "$@" ;; - ctl) cmd_ctl "$@" ;; - stop) cmd_stop "$@" ;; - kill) cmd_kill "$@" ;; - prompt) cmd_prompt "$@" ;; - tail) cmd_tail "$@" ;; - tui) cmd_tui "$@" ;; - env) cmd_env "$@" ;; + ls) cmd_ls "$@" ;; + ctl) cmd_ctl "$@" ;; + stop) cmd_stop "$@" ;; + kill) cmd_kill "$@" ;; + prompt) cmd_prompt "$@" ;; + tail) cmd_tail "$@" ;; + tui) cmd_tui "$@" ;; + env) cmd_env "$@" ;; help|-h|--help) cmd_help ;; *) echo "unknown command: $subcmd" >&2