Commit Graph
10 Commits
Author SHA1 Message Date
iris-aiandClaude Opus 5 ac476ab0c9 Give llama.cpp sessions tools, web search and a model picker
A llama session was a chat box: no tools, a fixed model, no permission
mode, and a model name drawn as the path the file sits at. It now runs the
agent loop itself, which is what the pieces below all hang off.

Tools are `llama-server`'s own (`--tools all`), which that server both
publishes and runs -- `GET /tools` for the definitions, `POST /tools` to
call one. Web search is Exa's MCP server, reached from this backend rather
than from the machine serving the model: that is what llama.cpp's own web
UI does, and it puts the search on the machine with a route out instead of
the one with the GPU. `llama-server`'s `--mcp-servers-json` can only spawn
local commands, so using it would have meant a Node bridge on every
machine that serves a model.

Driving the loop is what makes the permission gate ours. Two modes,
`manual` and `bypassPermissions`, which is what the mechanism has: the web
UI asks before every call and remembers the tools you say "always" to. The
allowances fold back out of the transcript's own answers, so they survive
a restart and a model change without being stored anywhere else.

Also here, because tools made each of them matter:

- **Loading is a state.** A 12 GB model takes twenty seconds to reach
  memory and refuses everything until it has; the session used to report
  `running` for that whole time, and a message sent meanwhile came back as
  an error. It is `loading` now, and the message waits.
- **The model can be changed.** A `llama-server` holds one model, so this
  stops it and starts another. The conversation survives because it was
  never in the server.
- **Models are named, not pathed.** `general.name` read out of the file
  itself -- over ssh too, in the round trip the spawn was already making.
  Where two models share a name the file name breaks the tie.
- **`-np 1`, and the MTP draft head where the file has one.** Measured on
  the 27B here: 41.5 tok/s plain, 61.4 with `--spec-type draft-mtp` at one
  slot, and 28 with it at four -- speculating against a split KV cache is
  worse than not speculating. The flag is conditional because asking for a
  head that is not there makes `llama-server` exit.
- **A refusal says what to do.** Tool results are thousands of tokens, so
  an overrun context is now ordinary; it was "http status: 400" and is now
  the server's own "exceeds the available context size, try increasing it".

`GET /machines/{id}/models` is gone: the provider models route answers the
same question, and two answers to one question is how a picker comes to
offer a model the spawn screen does not.

Verified end to end against real models: a tool call asked and allowed, an
Exa search, a shell command, a 27B loaded while a message waited on it, a
model switch mid-session, a second message queued behind a running turn,
and the whole of it again on a session running over ssh.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 08:11:49 -04:00
iris 7d9df5d572 Rename setups and add provider reauthentication 2026-09-12 22:56:43 -04:00
iris 7ee88dfd9c Make model and permission choices provider-specific 2026-09-08 00:08:09 -04:00
iris 6a0202b1b5 Add Codex JSON sessions and usage limits 2026-09-07 23:29:15 -04:00
irisandClaude Opus 5 74c07d687a End subagents on the CLI's own task lifecycle, and detect a limit two ways
Subagents were showing "running" long after they had finished. Measured
against 2.1.237 by running a session that launched one Task agent and
reading its stdout: a subagent's lines carry no `stream_event` at all --
they are whole `user`/`assistant` lines with a null `stop_reason` -- and no
`result` line is sent for one. So `ends_a_turn`, which watches for a raw
`message_delta` saying `end_turn`, could never fire for a subagent, and
nothing finished one until its session's process exited.

What the CLI does send is a task lifecycle, as top-level `system` lines:
`task_started` (with the tool_use id), `task_progress`, `task_updated`
(status, naming the task only) and `task_notification` (tool id, status, and
the agent's own summary). `translate_task` keeps the task -> tool mapping,
records the summary as the subagent's closing text -- the run showed its
child lines stop at its last tool_result, so without this a finished
subagent reads as stopping mid-tool -- and ends it. A `completed` update is
deliberately not the end, since its notification carries the summary; any
other terminal status is, because the failure to avoid is a subagent nothing
ever finishes. `ends_a_turn` stays as a second detector and must never be
the only one again. Verified by replaying the captured stream through the
server as a fake CLI: running, prompt, Bash call, output, report, exited.

`finish_all` now reads the directory rather than the live map, which is what
clears the ones already stuck: a subagent left running by an earlier run of
the server is exactly the one this process never touched, so it read
"running" again every time its session was started.

Auto-resume gets the same treatment on its own single point of failure. The
only thing that scheduled a resume was the CLI's error sentence at the end
of a failed turn; the CLI also sends `rate_limit_event` lines saying where
the account stands, and this server ignored them entirely. Both are read
now. Anything that is not an `allowed...` status counts as refused and is
logged if unfamiliar -- being wrong that way costs one question to the usage
meter, which is still what decides whether anything is sent, and being wrong
the other way is the feature silently not existing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 13:29:23 -04:00
irisandClaude Opus 5 4821a02bd3 Default thinking level for new sessions, and move the rigs out of AGENTS.md
`Config::default_effort` is what a session starts at when nothing chose one,
applied in `spawn_session` rather than filled in by the spawn screen so it
holds for an import and a bare API call too. It is set by the spawn screen's
own picker, whose label says so: one control, where new sessions are made,
rather than a settings page for a single value. Not on a provider, because
providers are discovered and the next rediscovery would erase it; not on the
phone, because a second device would then spawn at a level nobody there
chose. `GET`/`POST /defaults` carry it as a struct, so the permission mode --
still hardcoded to `auto` on the spawn screen -- can move there later without
a second route.

Only drivers that read a level are given one: an echo session was storing a
`--effort` it never passes to anything, which is a config file answering a
question about itself wrongly.

Separately, `AGENTS.md` is 35 KB sent with every request in this repo, and 12
KB of it was rigs and reference measurements that only matter once you are
running one. Those are the `ai-app-rigs` skill now -- the same text, still the
only copy, read when the work touches it. 35,198 -> 20,813 chars.

Verified on the emulator against the sandbox: the spawn screen pre-fills from
the server, picking `low` spawned a session at `low` and left `/defaults` set
to it, and an echo session spawned afterwards took no level at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 21:42:05 -04:00
iris 3c0214ece8 Merge branch 'main' of git.arirex.me:iris/ai-app
# Conflicts:
#	AGENTS.md
#	PLAN.md
#	app/androidApp/src/main/kotlin/com/example/aiapp/SessionUsageBar.kt
#	app/androidApp/src/main/kotlin/com/example/aiapp/SpawnScreen.kt
#	server/src/config.rs
#	server/src/main.rs
#	server/src/routes.rs
#	server/src/session/echo.rs
#	server/src/session/llama.rs
#	server/src/session/transport.rs
#	server/src/ssh.rs
#	server/src/usage.rs
2026-09-04 17:56:50 -04:00
irisandClaude Opus 5 127b25e60a Meter a session by its provider, and let llama.cpp run over ssh
The rate-limit bar answered a question about an account, and picked the
answer by machine. One machine runs echo, the Claude CLI and a local
model side by side, so every echo session on it drew the CLI's five-hour
window: a quota that session cannot spend and could never run down. A
session now names its meter (`usageProvider`, from
`DriverKind::usage_provider`, which `usage::providers_for` reads too so
the two lists cannot disagree), and the phone matches on machine *and*
provider. Nothing meters echo or llama, and nothing at all is drawn --
including while the first fetch is out, since "checking" under a session
that turns out to meter nothing is a row the screen then withdraws.

Echo gets a meter it can be *told* about instead: `/usage 42`,
`/usage 95 20`, `/usage 42 never`, `/usage notloggedin`,
`/usage unreachable`, `/usage failed`, `/usage off`. Those states cost
real quota to arrange, which is why none of them had been looked at.

And llama.cpp runs wherever a setup says, which was the last of phase 5.
`Transport::reserve_port` is the second half of what a transport is --
"run this" plus "reach this port" -- returning the port the server binds
there and the port that reaches it here, and `Launch::reaching` puts the
`-L` tunnel on the connection that already carries the command. Three
things that came out of building it:

- A forwarded launch gets a pty and every other one keeps `-T`. Killing
  the ssh client ends a CLI by closing the stdin it reads; llama-server
  never reads its stdin, so the same kill left it running on the far
  machine with the model loaded -- one orphan per stopped session.
- The model is looked for on the machine that will serve it, at that
  machine's own models directory, so `GET /setups/{id}/models` is what
  the spawn screen offers rather than the backend's own downloads.
- The readiness poll watches the process, not only the port: a model
  that will not load exits in a second and would otherwise have been
  reported as "gave up after 300s". The failure carries the log's tail.

Exercised end to end against this VM over ssh to itself: spawn, load,
answer, outlive a backend restart, be adopted, answer again, and stop --
with both the ssh client and the far llama-server gone afterwards. The
local path, the Claude bar and the spawn screen checked on the emulator.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 17:45:32 -04:00
irisandClaude Opus 5 edc39c7371 Thin the app's comments
The same pass the server had, on the Kotlin side: comments restating what
the code says are gone, and the ones recording a measurement, a constraint
or an incident are kept but cut to a few lines each. 6540 comment lines to
5674, and 920 lines off the app.

Two doc comments had drifted onto the item above the one they describe --
`contextAfter`'s onto `sessionWorking` in Events.kt, and `UsageMonitor`'s
equivalent on the server was fixed in the previous commit. Each is back on
its own item, which is the only non-comment line this diff moves.

The comments are reflowed to the column limit at their own indentation:
several were written wide, and ktfmt re-wrapped them into lines holding a
single orphan word. `/tmp` script, not kept -- ktfmt is idempotent over the
result, which is the check.

Left alone deliberately: this codebase's remaining comment density is high
because the comments carry things the code cannot say -- what a null means,
what a number was measured against, which bug a guard exists for. Of the
238 one-line doc comments in the app, five were pure restatement of the
name and were removed; the rest each say something the signature does not.

ktfmtFormat, compileDebugKotlin, lintDebug and testDebugUnitTest pass;
cargo test (127), clippy --all-targets and fmt still clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 16:20:16 -04:00
iris b172c464ea ai-app: a phone interface to Claude Code and llama.cpp sessions
A Rust backend that owns the sessions and an Android app that reads them.
The server spawns and adopts CLI processes, normalises everything they emit
into one event model, keeps the transcript, and serves it over pinned TLS on
a WireGuard interface; the phone streams that, replies, sends images, and
imports conversations the machine already has.

`AGENTS.md` is the working guide -- what runs where, what has been measured,
and the faults that were expensive to find. `PLAN.md` is the design record.

History before this point was squashed away. It was a personal project's
running commentary and carried a name and a couple of machine paths that
have no business in a public repository; the tree is what mattered and the
tree is here.
2026-08-31 20:29:07 -04:00