remove stale IDEAS_CONTROLLER.md (dead /home/lkn/mnt/ollie/session/ refs)

This commit is contained in:
Ollie Agent 2026-07-31 08:37:26 +02:00
parent 5021dedabe
commit 5ce4477316
1 changed files with 0 additions and 302 deletions

View File

@ -1,302 +0,0 @@
# IDEAS: Fit-for-Purpose Session Controller
Ollie's 9P session filesystem exposes enough observable state that an external
controller process can enforce production-grade loop discipline without touching
the agent loop, the model, or any prompt. The controller is a first-class citizen
of the design — it reads structured synthetic files and actuates via `ctl` writes.
## Background
Production agent systems avoid "smart shell script with amnesia" behavior by
applying explicit control policies:
- A **planner/executor split** where one component sets the next bounded action
and another performs it.
- **Per-step budgets**: max tool calls before replanning, max context tokens
before compaction, max output bytes admitted back into context, max turns
without net state change.
- **Structured tool results**: the model sees a normalized observation record
(status, key findings, changed files, exit condition) instead of raw terminal
output.
Ollie's system prompt enforces the planner/executor split at the prompt layer
(mandatory `PLAN:` checklist, persisted to `s/$SID/plan`). The per-step budgets
and output normalization are not enforced mechanically — they are the gap that
an external controller closes.
## Observable Surfaces
All paths are relative to `$OLLIE/session/$SID/agent/0/`.
| Path | Mode | Signal class | Description |
|---|---|---|---|
| `state` | read | state | `idle` / `thinking` / `calling:<tool>` |
| `statewait` | blocking read | state | Blocks until state changes — the event pump |
| `ctxsz` | read | budget | Current tokens / window ceiling as `N/M` |
| `ctxszwait` | blocking read | budget | Blocks until context size changes — fires after every tool call |
| `usage` | read | budget | Token usage counters (input / output / total) |
| `plan` | r/w | stall | Markdown checklist; `[x]` count delta = net progress signal |
| `ctl` | write | actuator | `stop` / `kill` / `compact` / `clear` — controller's actuator |
| `prompt` | write | actuator | Inject a prompt into the session |
| `fifo.in` | write | actuator | Queue a prompt to execute after the current turn |
| `chat` | read | stall | Full chat history — tail from `offset` to measure response size |
| `offset` | read | stall | Byte offset of last user prompt |
**Key principle**: use the `*wait` blocking-read variants, not polling. A
controller that polls `state` every second wastes cycles and can miss fast
transitions. Block on `statewait`; wake only when state actually changes.
## Signal Taxonomy
### State signals
Derived from `statewait`. Fires on every `idle → thinking → calling:tool → idle`
transition. Gives turn boundaries without polling.
### Context budget
Derived from `ctxszwait` and `usage`. Fires after each tool call, not just at
idle. Enforce a ceiling as a fraction of the window before the model starts
silently degrading from context loss.
### Output byte budget
Computed from `offset` + `chat`. After each turn transitions to idle, measure
`wc -c` on the chat file from the saved offset. A large delta with no
corresponding `plan` checkbox change is raw terminal sludge.
### Stall detection
Computed from the `[x]` line count in `plan` across consecutive idle events.
Count idle turns where the completed-task count didn't change. That's the
no-progress counter.
## Controller Types
Each controller is a small independent process. They compose via process
supervision — no shared framework, no IPC, just the filesystem.
### budget-ctl
Watches `ctxszwait`. When context crosses a threshold, writes `compact` to `ctl`.
When a step ceiling is crossed (counted as `calling:*` → `idle` transitions),
writes a queued replan prompt to `fifo.in`.
- **Watches**: `ctxszwait`, `usage`
- **Actuates**: `ctl ← compact`, `fifo.in ← replan prompt`
- **Implementation**: ~30 lines of `sh` + `awk`
### stall-ctl
Blocks on `statewait` to get idle events. Reads the plan file on each idle,
diffs the `[x]` count. After N idle turns with no delta, writes `stop` and logs
the stall.
- **Watches**: `statewait`, `plan`
- **Actuates**: `ctl ← stop`
- **Implementation**: ~40 lines of `sh`
### output-ctl
After each idle, computes the chat delta from `offset`. If the raw response
exceeds `MAX_BYTES`, injects a normalization prompt:
> Compress your last tool output into a structured observation: status, key
> findings, changed files, next action.
- **Watches**: `statewait`, `offset`, `chat`
- **Actuates**: `prompt ← normalize instruction`
- **Implementation**: ~50 lines of `sh`
### turn-auditor
Accumulates tool-call events from state transitions (`calling:<tool>` → `idle`).
Counts parallel vs sequential calls, flags turns where the agent re-read a file
it already read, emits a structured turn trace for post-hoc review. Observes
only — does not actuate.
- **Watches**: `statewait` (all states)
- **Actuates**: write to log file
- **Implementation**: Go or Python; passive observer
## Controller Loop Design
The controller state machine per session:
```
WATCHING → block on statewait
→ wake on "idle"
IDLE_FIRED → read ctxsz, plan; compute chat delta
EVALUATING → check all policies: budget, stall, output size
ACTING → write to ctl, prompt, or fifo.in (at most one action per idle)
WATCHING → block on statewait again
```
For multi-signal controllers, fan out checks in parallel after each idle event,
then apply a priority ordering before actuating (budget > stall > output) to
avoid conflicting writes to `ctl`.
## Reference Sketches
### stall-ctl
```sh
#!/bin/sh
# stall-ctl: kill session after N idle turns with no plan progress
# Usage: stall-ctl --sid SID [--no-progress-turns N]
set -e
SID="" MAX_STALL=3
while [ $# -gt 0 ]; do
case $1 in
--sid) SID=$2; shift 2 ;;
--no-progress-turns) MAX_STALL=$2; shift 2 ;;
*) shift ;;
esac
done
[ -z "$SID" ] && { echo "stall-ctl: --sid required" >&2; exit 1; }
CTL="$OLLIE/session/$SID"
stall_count=0
prev_checked=-1
while true; do
# Block until state changes
new_state=$(cat "$CTL/statewait")
[ "$new_state" != "idle" ] && continue
# Count completed tasks in the plan checklist
checked=$(grep -c '^- \[x\]' "$CTL/plan" 2>/dev/null || echo 0)
if [ "$prev_checked" -ge 0 ] && [ "$checked" -eq "$prev_checked" ]; then
stall_count=$((stall_count + 1))
if [ "$stall_count" -ge "$MAX_STALL" ]; then
echo "stall-ctl: $SID stalled for $stall_count turns, stopping"
echo "stop" > "$CTL/ctl"
exit 0
fi
else
stall_count=0
fi
prev_checked=$checked
done
```
### budget-ctl
```sh
#!/bin/sh
# budget-ctl: compact when context exceeds pct of window; replan after steps-max
# Usage: budget-ctl --sid SID [--ctx-pct N] [--steps-max N] [--replan-prompt FILE]
set -e
SID="" CTX_PCT=75 STEPS_MAX=0 REPLAN_PROMPT=""
while [ $# -gt 0 ]; do
case $1 in
--sid) SID=$2; shift 2 ;;
--ctx-pct) CTX_PCT=$2; shift 2 ;;
--steps-max) STEPS_MAX=$2; shift 2 ;;
--replan-prompt) REPLAN_PROMPT=$2; shift 2 ;;
*) shift ;;
esac
done
[ -z "$SID" ] && { echo "budget-ctl: --sid required" >&2; exit 1; }
CTL="$OLLIE/session/$SID"
step=0
while true; do
# Wake on any ctx change (fires after every tool call, not just idle)
raw=$(cat "$CTL/ctxszwait") # e.g. "12847/65536"
used=$(echo "$raw" | cut -d/ -f1)
total=$(echo "$raw" | cut -d/ -f2)
pct=$(awk "BEGIN{print int($used*100/$total)}")
if [ "$pct" -ge "$CTX_PCT" ]; then
echo "budget-ctl: ctx $pct% >= threshold, compacting"
echo "compact" > "$CTL/ctl"
fi
# Count steps: calling:* → idle counts as one step
state=$(cat "$CTL/state")
case $state in idle) step=$((step+1)) ;; esac
if [ $STEPS_MAX -gt 0 ] && [ $step -ge $STEPS_MAX ]; then
echo "budget-ctl: step limit reached, injecting replan"
cat "$REPLAN_PROMPT" > "$CTL/prompt"
step=0
fi
done
```
### output-ctl
```sh
#!/bin/sh
# output-ctl: inject summarize prompt when turn output exceeds MAX_BYTES
# Usage: output-ctl --sid SID [--max-bytes N]
set -e
SID="" MAX_BYTES=4096
while [ $# -gt 0 ]; do
case $1 in
--sid) SID=$2; shift 2 ;;
--max-bytes) MAX_BYTES=$2; shift 2 ;;
*) shift ;;
esac
done
[ -z "$SID" ] && { echo "output-ctl: --sid required" >&2; exit 1; }
CTL="$OLLIE/session/$SID"
prev_offset=0
while true; do
new_state=$(cat "$CTL/statewait")
[ "$new_state" != "idle" ] && continue
cur_offset=$(cat "$CTL/offset")
chat_size=$(wc -c < "$CTL/chat")
delta=$((chat_size - prev_offset))
if [ "$delta" -gt "$MAX_BYTES" ]; then
echo "output-ctl: response ${delta}B > ${MAX_BYTES}B, injecting normalize"
printf 'Compress your last tool output into a structured observation: '\
'status, key findings, changed files, next action.' > "$CTL/prompt"
fi
prev_offset=$cur_offset
done
```
## Composition
All three controllers watch the same session directory as independent processes.
They share no state. Compose them per task type in a `mkfile` recipe:
```sh
# Start a coding session with full controller suite
SID=$(ollie session new cwd=$PWD agent=coding)
CTL="$OLLIE/session/$SID"
budget-ctl --sid "$SID" --ctx-pct 75 --steps-max 12 \
--replan-prompt "$OLLIE_CFG_PATH/prompts/replan.md" &
stall-ctl --sid "$SID" --no-progress-turns 3 &
output-ctl --sid "$SID" --max-bytes 4096 &
echo "$TASK" > "$CTL/prompt"
wait
```
Each controller process exits when it writes `kill`/`stop` to `ctl` or when the
session dies. No daemon, no framework, no shared memory.
## Gap Closure
| Control gap | Closed by | Signal used |
|---|---|---|
| Max tool calls before replan | `budget-ctl` step counter | `statewait` → count `calling:*` → `idle` |
| Max context before compact | `budget-ctl` ctx ceiling | `ctxszwait` → threshold → `compact` |
| Max output bytes in context | `output-ctl` | `offset` + `chat` delta |
| Stall detection | `stall-ctl` | `plan` checkbox delta across idle events |
| Structured tool results | `output-ctl` normalize prompt | Reactive: normalizes after the fact |
| Hard mid-turn enforcement | Not closeable externally | Controller acts at turn boundaries only |
## Limitations
The controller acts at turn boundaries — it sees output after it is already in
context. If a tool produces 100KB of stdout, `output-ctl` can inject a
normalization prompt on the next turn, but the current context window already
contains the raw output. Fully preventive output filtering would require a proxy
at the tool execution layer, not the session filesystem layer.
The planner/executor discipline (mandatory `PLAN:` before tool calls) remains
prompt-layer only. The controller can stop a session after a bad turn but cannot
prevent a bad action mid-turn.