samagotchi 0.37.0 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +72 -1
- data/README.md +1 -1
- data/docs/broadcast.md +5 -2
- data/docs/cli.md +2 -1
- data/docs/configuration.md +11 -3
- data/docs/desktop.md +24 -2
- data/docs/guardrails.md +2 -2
- data/docs/hooks.md +3 -2
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/engine.md +4 -2
- data/docs/internals/llm-context-bench.md +119 -0
- data/docs/internals/prompt-caching.md +3 -2
- data/docs/plugins.md +85 -32
- data/docs/testing.md +7 -0
- data/lib/samagotchi/broadcast/scope_card.rb +15 -2
- data/lib/samagotchi/broadcast/triage.rb +29 -12
- data/lib/samagotchi/broadcast/triage_log.rb +9 -4
- data/lib/samagotchi/broadcast_command.rb +4 -2
- data/lib/samagotchi/bundles/mcp/manifest.yml +5 -4
- data/lib/samagotchi/bundles/mcp/plugin.rb +424 -89
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +3 -2
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/config.rb +30 -4
- data/lib/samagotchi/context_note.rb +7 -3
- data/lib/samagotchi/context_status.rb +20 -2
- data/lib/samagotchi/desktop/macos/App.swift +5 -1
- data/lib/samagotchi/desktop/macos/Broadcast.swift +97 -0
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +12 -3
- data/lib/samagotchi/desktop/macos/Panel.swift +123 -26
- data/lib/samagotchi/desktop/macos.rb +27 -5
- data/lib/samagotchi/desktop_command.rb +1 -1
- data/lib/samagotchi/engine.rb +22 -7
- data/lib/samagotchi/guardrails/targets.rb +14 -2
- data/lib/samagotchi/host_registry.rb +6 -3
- data/lib/samagotchi/kernel_loop.rb +114 -14
- data/lib/samagotchi/llm/chat_loop.rb +39 -14
- data/lib/samagotchi/llm/turn_settings.rb +5 -4
- data/lib/samagotchi/llm_context_apply.rb +153 -0
- data/lib/samagotchi/llm_context_edit.rb +71 -0
- data/lib/samagotchi/llm_context_stale.rb +255 -0
- data/lib/samagotchi/llm_context_strategy.rb +133 -0
- data/lib/samagotchi/llm_context_view.rb +124 -0
- data/lib/samagotchi/log_subscriber.rb +2 -1
- data/lib/samagotchi/plugin/api.rb +5 -3
- data/lib/samagotchi/prompt.rb +4 -11
- data/lib/samagotchi/session_manager.rb +9 -1
- data/lib/samagotchi/session_metrics.rb +18 -1
- data/lib/samagotchi/terminal_ui/formatting.rb +4 -2
- data/lib/samagotchi/token_usage.rb +3 -0
- data/lib/samagotchi/tool_ids.rb +84 -0
- data/lib/samagotchi/tool_response.rb +72 -11
- data/lib/samagotchi/tool_runner.rb +18 -9
- data/lib/samagotchi/tools/context_read.rb +27 -10
- data/lib/samagotchi/update_command.rb +1 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/worker.rb +4 -0
- metadata +9 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 15442c0fe342d1193ff595b31c554f886076fc4da7dac938fe319ec784e053c6
|
|
4
|
+
data.tar.gz: 37e73e8590db29ba273346191954ba96b72de468a46663bc2f8820807d95278d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a7d252dbf3ef0eb6ea34aa61bd47e5da389bf17b5f0f4784c9c0e2bf69d22732e83db29b8652bce40ef7ac4a0593e48bc481eb63b9b86a2485ff4027dac1de5f
|
|
7
|
+
data.tar.gz: 50d569dc04ca76c690c9f4f91ffcec90d85b54445b07f63d2bab1fc6aed5a47d214d9b400faa9b3915783a10e9952613b1a0b0db6361accb514eb4aab7ad3e67
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,75 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.39.0] - 2026-10-07
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `llm_context.strategy` (default `none`), and per model or host `llm_context_strategy`. `stale` sends a file read
|
|
16
|
+
that a later read of the same lines superseded as a one-line stub; the session keeps the output. With
|
|
17
|
+
`llm_context.stale_edits: true` (opt-in, experimental) a read a later edit or write of the file superseded is
|
|
18
|
+
stubbed too; on the replay bench these cost about 5x the tokens they free in re-prefill, and 26% were needed again
|
|
19
|
+
(against a 6% base rate). No read of a file edited in the last `llm_context.protect_steps` (3) steps is stubbed.
|
|
20
|
+
`llm_context.apply` (and per model or host `llm_context_apply`) says when stubs reach the prompt: `payoff` (the
|
|
21
|
+
default) sends them at a request when they free at least the tail the server reads again, else at the end of a
|
|
22
|
+
turn the model answered, which in practice is usually where they go; `next_request` and `turn_end` are the
|
|
23
|
+
others. See [docs/configuration.md](docs/configuration.md).
|
|
24
|
+
- `/stats` shows the re-prefilled tokens for every session (what the server prefilled again of the previous
|
|
25
|
+
request's prompt, 128 tokens or more), and the log's `generation_completed` line has them as `reprefill=`.
|
|
26
|
+
- For development: `script/llm_context_bench.rb`, a replay benchmark for the LLM context strategies over a folder of
|
|
27
|
+
stored sessions (offline; `--live` asks a model, for picks). It isn't part of the gem. See
|
|
28
|
+
[docs/internals/llm-context-bench.md](docs/internals/llm-context-bench.md).
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- Answering a step-limit question with Continue and then sending a prompt runs the continue turn first, even when
|
|
33
|
+
both arrive while the worker is between turns (the prompt could run first and drop the offer). A session started
|
|
34
|
+
from another session's `execute` (`chi send`) no longer inherits that session's `SAMAGOTCHI_PARENT_SESSION` and
|
|
35
|
+
`SAMAGOTCHI_SESSION_MODEL`, so your `!chi context add` there attaches to it, not to the other one. `context_read`
|
|
36
|
+
pages within `max_tool_output_chars` whatever its header holds, so a page is never cut a second time behind its
|
|
37
|
+
"Pass offset". The mcp bundle (0.6.1): `mcp_call` with a mistyped `<server>/<tool>` waits only for that server's
|
|
38
|
+
start, and `find_mcp_tools` names a server with no `description:` or instructions by its tool count, not its first
|
|
39
|
+
tool names (a model called those without searching).
|
|
40
|
+
|
|
41
|
+
Update with `chi update` (mcp 0.6.1).
|
|
42
|
+
|
|
43
|
+
## [0.38.0] - 2026-10-07
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
|
|
47
|
+
- The desktop helper's panel has an **Everyone it concerns** row (⌘B): ⌘⏎ there runs `chi broadcast` with the note
|
|
48
|
+
(your line, then the selection; a first line like `shopfront/checkout` keeps it to that project), instead of a
|
|
49
|
+
note to the sessions you pick. The panel shows "broadcasting…", then the summary line, and stays open; `chi
|
|
50
|
+
broadcast log` has the details. It may take `broadcast.triage_deadline` + 10 s: after changing the deadline, run
|
|
51
|
+
`chi update`. Rebuilt by `chi update`. See [docs/desktop.md](docs/desktop.md#everyone-it-concerns).
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
|
|
55
|
+
- The mcp bundle (0.6.0) no longer declares every MCP tool to the model. It gets two fixed tools: `find_mcp_tools`
|
|
56
|
+
searches the servers' tools by keywords and answers the best 5 with their schemas, and `mcp_call` calls one as
|
|
57
|
+
`<server>/<tool>`. Requests get smaller (about half on a 30-tool server, in a spike on a local model), and the
|
|
58
|
+
model's tools no longer change when a server starts, refreshes or fails, so the prompt cache keeps; a tool turn
|
|
59
|
+
takes a few more steps (search, then call). A failed call answers the tool's schema, so a call can be fixed on the
|
|
60
|
+
next step. A server's new optional `description:` names it in the search tool, else the first sentence of its
|
|
61
|
+
instructions. Guardrail rules on `mcp_<server>_<tool>` names still match (`mcp_call` acts as that tool), and the
|
|
62
|
+
question names `<server>: <tool>`; approvals stored under the old names don't carry over. Hooks and loop-guard's
|
|
63
|
+
`ignore_tools` see `mcp_call`. `/mcp` lists the tools as `<server>/<tool>` and says what every request carries.
|
|
64
|
+
Needs chi 0.37.0. See [docs/plugins.md](docs/plugins.md#the-mcp-bundle).
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
|
|
68
|
+
- `chi broadcast`'s triage ends at most a second after `broadcast.triage_deadline` (requests that ignore the cancel
|
|
69
|
+
could hold it up to ~4 s past it), turns logprobs off for a host only when its 400 was about them, and two broadcasts
|
|
70
|
+
at once no longer both rotate the triage log (one failed, or the old lines were lost). A scope card's folder is right
|
|
71
|
+
under a symlinked path (`/tmp` on macOS read `../../../tmp/…`), and the summary line shortens a reason that quotes a
|
|
72
|
+
setting (`3 unchecked: no triage model: broadcast.triage_host_ref`).
|
|
73
|
+
- `max_tool_output_chars` caps the tool outputs the model gets back on the native loop too (a host without
|
|
74
|
+
`api: openai`): it fed each output whole, so one long result could fill the context. On both loops a cut output now
|
|
75
|
+
ends with `[cut: N of M chars; read it in parts]`, so the model knows it didn't get it all (the chat loop cut it
|
|
76
|
+
silently); the tool row shows the same text.
|
|
77
|
+
|
|
78
|
+
Update with `chi update` (mcp 0.6.0; it also rebuilds the desktop helper for the new row).
|
|
79
|
+
|
|
11
80
|
## [0.37.0] - 2026-10-07
|
|
12
81
|
|
|
13
82
|
### Added
|
|
@@ -1805,7 +1874,9 @@ and long-lived sessions.
|
|
|
1805
1874
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
1806
1875
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
1807
1876
|
|
|
1808
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
1877
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.39.0...HEAD
|
|
1878
|
+
[0.39.0]: https://github.com/dm1try/samagotchi/compare/v0.38.0...v0.39.0
|
|
1879
|
+
[0.38.0]: https://github.com/dm1try/samagotchi/compare/v0.37.0...v0.38.0
|
|
1809
1880
|
[0.37.0]: https://github.com/dm1try/samagotchi/compare/v0.36.0...v0.37.0
|
|
1810
1881
|
[0.36.0]: https://github.com/dm1try/samagotchi/compare/v0.35.0...v0.36.0
|
|
1811
1882
|
[0.35.0]: https://github.com/dm1try/samagotchi/compare/v0.34.0...v0.35.0
|
data/README.md
CHANGED
|
@@ -179,7 +179,7 @@ only tells you (`context.wake`). See [Attached context](docs/context.md).
|
|
|
179
179
|
`chi desktop install` builds a small native helper: select text in any app →
|
|
180
180
|
Services → **Send to chi** (or ⌃⌥⌘N with the clipboard) → pick sessions in a
|
|
181
181
|
Spotlight-like panel → ⏎ sends it quoted under your question as a message, ⌘⏎ as a
|
|
182
|
-
context note. It needs the
|
|
182
|
+
context note (on the "Everyone it concerns" row, a `chi broadcast`). It needs the
|
|
183
183
|
Command Line Tools. See [Desktop helper](docs/desktop.md).
|
|
184
184
|
|
|
185
185
|
### Guardrails
|
data/docs/broadcast.md
CHANGED
|
@@ -16,7 +16,9 @@ chi broadcast log # the last broadcasts and who got them
|
|
|
16
16
|
chi broadcast deliver b-7f3a1c9e c0ffee12 # one it skipped, after all
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
To tell one session or a few you picked, use `chi note ID...` instead.
|
|
19
|
+
To tell one session or a few you picked, use `chi note ID...` instead. From
|
|
20
|
+
any app, the desktop helper's **Everyone it concerns** row broadcasts the
|
|
21
|
+
selection: see [Desktop helper](desktop.md#everyone-it-concerns).
|
|
20
22
|
|
|
21
23
|
## Who it reaches
|
|
22
24
|
|
|
@@ -157,7 +159,8 @@ Each broadcast (not a `--dry-run`) leaves one line in
|
|
|
157
159
|
id, time, your text, the note's tags and each recipient's verdict (result,
|
|
158
160
|
reason, P(yes), who decided, the session's tags). Scope cards are not kept:
|
|
159
161
|
they quote your prompts. Past 2 MiB the file becomes `log.jsonl.1`, one old
|
|
160
|
-
file kept.
|
|
162
|
+
file kept (`log.jsonl.lock` keeps two broadcasts at once from both rotating
|
|
163
|
+
it). Sessions don't read it: a broadcast stays fire-and-forget.
|
|
161
164
|
|
|
162
165
|
```sh
|
|
163
166
|
chi broadcast log # the last 5 broadcasts
|
data/docs/cli.md
CHANGED
|
@@ -421,7 +421,8 @@ file and index line stay as they are; only this session doesn't see it.
|
|
|
421
421
|
`/help` lists the commands this session takes, the installed bundles' too, each
|
|
422
422
|
with a line on what it does. `/stats` shows the session's numbers: turns, tool
|
|
423
423
|
calls (by tool, with errors), iterations, tokens in/out summed over every
|
|
424
|
-
request (with the cached tokens, the cache writes and the reasoning tokens when the server
|
|
424
|
+
request (with the cached tokens, the cache writes, the re-prefilled tokens and the reasoning tokens when the server
|
|
425
|
+
reports them),
|
|
425
426
|
the decode speed, generation latency, cancellations, retries (the network's),
|
|
426
427
|
thinking cuts (generations a plugin such as loop-guard cut) and output cap hits
|
|
427
428
|
(generations that ended at the provider's cap), context used and window, the prompt profile and the model the server says it ran, and the
|
data/docs/configuration.md
CHANGED
|
@@ -535,8 +535,10 @@ cache:
|
|
|
535
535
|
key: session # off (default) or session
|
|
536
536
|
```
|
|
537
537
|
Each request's own counts are in the debug log's `generation_completed` line:
|
|
538
|
-
`prompt=` (its prompt tokens), `cached=` (read from the server's cache)
|
|
539
|
-
server reports writes)
|
|
538
|
+
`prompt=` (its prompt tokens), `cached=` (read from the server's cache), `cache_write=` (written to it, when the
|
|
539
|
+
server reports writes) and `reprefill=` (of the previous request's prompt on the same model, what the server
|
|
540
|
+
prefilled again: an `llm_context` edit's cache break, or anything else that rewrote the prompt's middle; only when the
|
|
541
|
+
server reports a cached count, and not on the first request after a warm-up); recap and side requests log theirs as `recap request_usage`.
|
|
540
542
|
|
|
541
543
|
## Llama Model Routing
|
|
542
544
|
|
|
@@ -915,6 +917,12 @@ described in their own sections.
|
|
|
915
917
|
| `context.status` | `true` | yes | Context-usage telemetry for the model. See [context telemetry](internals/context-telemetry.md). |
|
|
916
918
|
| `context.window_tokens` | server's, else 256000 | yes | Context window when the server doesn't report one. `models.<key>.window_tokens` and `hosts.<name>.window_tokens` come first; see "The context window". |
|
|
917
919
|
| `models.<key>.window_tokens`, `hosts.<name>.window_tokens` | none | | A model's or host's context window when the server and its model list report none. |
|
|
920
|
+
| `llm_context.strategy` | `none` | | What the model is sent of the conversation: `none` (everything, unchanged), or a list of layers (`stale`, `forget`; `"\|"`-separated or a YAML list). `stale`: a file read that a later read covering its lines superseded (a read that came back whole: not the head/tail preview of a big file, nor cut at `max_tool_output_chars`) is sent as a stub (`[read] lib/x.rb lines 1-200: superseded by a later read`). With `llm_context.stale_edits: true` (opt-in, experimental) so is a read a later successful edit or write of the file superseded (`superseded by a later edit`); on the replay bench these stubs cost about 5x the tokens they free in re-prefill, and 26% of them were needed again (against a 6% base rate). No read of a file edited or written in the last `llm_context.protect_steps` steps is stubbed. When stubs reach the prompt is `llm_context.apply`: `next_request` sends a read a later read superseded from the next request, and an edit-driven stub at the end of a turn the model answered; `turn_end` sends them all at the end of a turn the model answered (a turn that ran out of steps keeps them staged; on the native loop against a local llama.cpp the turn-end warm-up prefills the edited prompt, elsewhere the next turn's first request carries the cache break); `payoff` sends the batch at a request when the tokens it frees are at least the tail after its earliest stub (what the server reads again), or the context is in the top `context.status_thresholds` bucket (which needs `context.status` on), else at the end of a turn the model answered. A re-read's tail holds the later read, so in practice payoff usually waits for the turn's end. Each batch breaks the prompt cache from its earliest stub; `/stats` counts the re-prefilled tokens. The session keeps the outputs and the stubs it sent, so a `--resume` sends the same. `forget` isn't built yet: a list with it warns and is `none`; an unknown name warns and is `none`. `models.<key>.llm_context_strategy` and `hosts.<name>.llm_context_strategy` come first. |
|
|
921
|
+
| `models.<key>.llm_context_strategy`, `hosts.<name>.llm_context_strategy` | none | | A model's or host's `llm_context.strategy` (`none` or `stale` for now). |
|
|
922
|
+
| `llm_context.apply` | `payoff` | | When a layer's edits reach the prompt (each batch breaks the prompt cache from its earliest edit): `payoff`, `next_request` or `turn_end`. An unknown value warns and is `payoff`. `models.<key>.llm_context_apply` and `hosts.<name>.llm_context_apply` come first. |
|
|
923
|
+
| `models.<key>.llm_context_apply`, `hosts.<name>.llm_context_apply` | none | | A model's or host's `llm_context.apply`. |
|
|
924
|
+
| `llm_context.stale_edits` | `false` | | Opt-in, experimental: `stale` also stubs a read that a later successful edit or write of the file superseded. |
|
|
925
|
+
| `llm_context.protect_steps` | `3` | | `stale` never stubs a read of a file edited or written in the last N steps (tool batches; counted across the whole conversation, not only the turn). `0` turns the protection off. |
|
|
918
926
|
| `context.chars_per_token` | `4.0` | yes | Estimate ratio when the server reports no usage. |
|
|
919
927
|
| `context.status_thresholds` | `20,40,60,80` | yes | Percentages that trigger a status. |
|
|
920
928
|
| `context.status_cadence` | `0` | yes | Also every N rounds; `0` = thresholds only. |
|
|
@@ -924,7 +932,7 @@ described in their own sections.
|
|
|
924
932
|
| `thinking.level` | `default` | `--thinking` | `off`, `low`, `medium`, `high` or `default` for every model; the flag and env outrank the `models:`/`hosts:` entries, the file's value doesn't. See "Thinking". |
|
|
925
933
|
| `models.<key>.thinking`, `hosts.<name>.thinking` | none | | A model's or host's level. See "Thinking". |
|
|
926
934
|
| `hosts.<name>.remote` | by address | | `true`/`false`: treat the host as a remote provider or a local server. See "Remote or local". |
|
|
927
|
-
| `max_tool_output_chars` | `10000` | yes |
|
|
935
|
+
| `max_tool_output_chars` | `10000` | yes | Characters of each tool output kept in the conversation (both loops); a longer one is cut and ends with `[cut: N of M chars; read it in parts]`. A top-level key (see below). |
|
|
928
936
|
| `cache.warmup` | `auto` | | `auto`: after a turn, send the next turn's prompt (up to the next message) to a local llama.cpp host on the native loop, so the next turn prefills only its message; never a remote or `api: openai` host. `off`: never. See [prompt caching](internals/prompt-caching.md#the-turn-end-warm-up). |
|
|
929
937
|
| `cache.ttl` | `5m` | | How long Anthropic keeps the prompt-cache breakpoints of a Claude model on a remote host: `5m` (Anthropic's default) or `1h`. A 1h cache write costs 2× the input price (5m: 1.25×), a read the same 0.1×. See "Remote or local". |
|
|
930
938
|
| `cache.key` | `off` | | `session`: send the session id as `prompt_cache_key` to OpenAI's API and OpenRouter, which route and cache by it; no other host gets it. `off`: none. |
|
data/docs/desktop.md
CHANGED
|
@@ -38,6 +38,22 @@ note needs a session. After the send the panel shows `started <id>…` for 3 s.
|
|
|
38
38
|
the model the new session starts on (`chi self --model`: `default.model` from config.yml, or its env override; an
|
|
39
39
|
alias as the model it names).
|
|
40
40
|
|
|
41
|
+
### Everyone it concerns
|
|
42
|
+
|
|
43
|
+
The second row, **Everyone it concerns** (⌘B), shares a note with every session it may concern instead of the ones
|
|
44
|
+
you pick: ⌘⏎ (the **Broadcast** button) runs [`chi broadcast`](broadcast.md) with the line, then the text, on stdin.
|
|
45
|
+
chi picks the sessions (a shared ticket, PR or link, else a small triage model), so the preselected session doesn't
|
|
46
|
+
count: the row is ticked alone, like the new row. Write it the way you'd tell a colleague: a first line such as
|
|
47
|
+
`shopfront/checkout` keeps the note to that project's sessions, and the rest is the selection.
|
|
48
|
+
|
|
49
|
+
- The panel shows "broadcasting…", then the broadcast's id and chi's summary line (`b-7f3a1c9e: delivered 4 ·
|
|
50
|
+
skipped 2 · 1 unchecked: triage deadline`), and **stays open**: Esc closes. Which sessions got it and why:
|
|
51
|
+
`chi broadcast log` in a terminal.
|
|
52
|
+
- A broadcast is a text note: ⏎ and images beep. The source field and Send are hidden (its source is `broadcast`).
|
|
53
|
+
- It may take `broadcast.triage_deadline` (20 s) and 10 s more before the helper stops it ("the note may be partly
|
|
54
|
+
delivered"). The helper reads that from its launch file: after changing the deadline, run `chi update` (or
|
|
55
|
+
`chi desktop upgrade`).
|
|
56
|
+
|
|
41
57
|
### Choosing the model
|
|
42
58
|
|
|
43
59
|
Click the model on the new row, or press **⌘M**, to choose another one for the new session. The chooser takes the
|
|
@@ -179,7 +195,9 @@ exports. So `install` writes `~/Library/Application Support/Chi Helper/launch.js
|
|
|
179
195
|
- the absolute path of the Ruby running chi and of chi itself (the gem's wrapper, or a checkout's `bin/chi`);
|
|
180
196
|
- `LANG=en_US.UTF-8`;
|
|
181
197
|
- only these variables, and only when they are set: `XDG_CONFIG_HOME`, `XDG_STATE_HOME`, `GEM_HOME`, `GEM_PATH`,
|
|
182
|
-
`RUBYLIB`. No tokens and no `SAMAGOTCHI_*` settings go in
|
|
198
|
+
`RUBYLIB`. No tokens and no `SAMAGOTCHI_*` settings go in;
|
|
199
|
+
- the settings the panel needs from config.yml, as chi read them then: the `kitty:` section (when
|
|
200
|
+
`kitty.listen_on` is set) and `broadcast_timeout` (`broadcast.triage_deadline` + 10 s).
|
|
183
201
|
|
|
184
202
|
The file freezes the install shell's values. If you install with a temporary `XDG_STATE_HOME`, the helper keeps
|
|
185
203
|
using it. `status` prints the baked dirs.
|
|
@@ -194,10 +212,14 @@ The helper uses only these commands, so it could ship on its own later:
|
|
|
194
212
|
- `chi send [-m LINE] [--image PATH]... ID...` and `chi send --new --dir DIR [--model NAME] [-m LINE] [--image PATH]...`
|
|
195
213
|
with the text on stdin (or none), and `chi note [--source NAME] ID...` with the text on stdin → one line per session
|
|
196
214
|
on stdout; exit 0 means all sent or queued, 1 means some were refused or failed.
|
|
215
|
+
- `chi broadcast` with the text on stdin → its id first (`broadcast b-…`), a line per recipient, then the summary line
|
|
216
|
+
(`delivered N · skipped M…`); exit 0 when it ran, 1 when refused or a delivery failed.
|
|
197
217
|
- `chi self --model` → the model a new session starts on (the panel's hint).
|
|
198
218
|
- `chi models --format json` → the models the hosts offer (the new-session model picker).
|
|
199
219
|
|
|
200
|
-
Each call is stopped after 10 s, a send with images after 30 s
|
|
220
|
+
Each call is stopped after 10 s, a send with images after 30 s, a broadcast after the launch file's
|
|
221
|
+
`broadcast_timeout` (`broadcast.triage_deadline` + 10 s; 30 s when it has none). A stopped `chi note` or `chi
|
|
222
|
+
broadcast` says the note may be partly delivered.
|
|
201
223
|
|
|
202
224
|
## Troubleshooting
|
|
203
225
|
|
data/docs/guardrails.md
CHANGED
|
@@ -289,7 +289,7 @@ A tool name may be a glob, so one rule covers a plugin's tools (an MCP server's,
|
|
|
289
289
|
say): `tool: "mcp_*"` or `tool: ["mcp_{git,gh}_*", web_fetch]` (`*`, `?`, `[…]` and
|
|
290
290
|
`{a,b}`, matched with `File.fnmatch`). `/guardrails` lists the glob as given.
|
|
291
291
|
A plugin tool whose `targets:` name no command or path (an MCP tool) is asked
|
|
292
|
-
about with its arguments (`
|
|
292
|
+
about with its arguments (`x: sum: a=20 b=22`), and an approval of "this
|
|
293
293
|
call" is keyed by them.
|
|
294
294
|
|
|
295
295
|
A plugin tool that runs other tools for the model (a dispatcher such as
|
|
@@ -309,7 +309,7 @@ name.
|
|
|
309
309
|
|
|
310
310
|
```yaml
|
|
311
311
|
- id: mcp-ask
|
|
312
|
-
tool: "mcp_*" # every MCP
|
|
312
|
+
tool: "mcp_*" # every MCP call (mcp_call, acting as mcp_<server>_<tool>); not a find_mcp_tools search
|
|
313
313
|
verdict: ask
|
|
314
314
|
reason: an MCP server's tool
|
|
315
315
|
```
|
data/docs/hooks.md
CHANGED
|
@@ -56,7 +56,7 @@ The plugin class must respond to `#call(event)` — duck-typed, no base class re
|
|
|
56
56
|
| `:after_generation` | After LLM returns (both loops); not after a generation a hook cut (`stop_generation`) | `{ type: :after_generation, iteration: N, response: "...", messages: [...] (the conversation as sent) }` |
|
|
57
57
|
| `:generation_progress` | While the response streams, in batches (see [Watching the stream](#watching-the-stream)) | `{ type: :generation_progress, iteration: N, thinking: "..." (new since the last fire), text: "..." (new visible text), thinking_chars: N, text_chars: N (this generation so far), elapsed_ms: N }` |
|
|
58
58
|
| `:before_tool_call` | Before tool dispatch (and before `tool_call_started`) | `{ type: :before_tool_call, iteration: N, call: {...}, params: "...", guardrail: Verdict, context: {...}, targets: {...}, blocked: false, block_reason: nil }` |
|
|
59
|
-
| `:after_tool_call` | After tool execution | `{ type: :after_tool_call, iteration: N, tool: "read", output: "..." (capped at max_tool_output_chars), status: "ok" \| "error" \| "blocked" \| "stopped" }`; `stopped` is a `task_wait` the turn's Stop ended (the task runs on) or whose task was stopped |
|
|
59
|
+
| `:after_tool_call` | After tool execution | `{ type: :after_tool_call, iteration: N, tool: "read", output: "..." (the text the model gets: capped at max_tool_output_chars, a cut one ending in a [cut: N of M chars …] line), status: "ok" \| "error" \| "blocked" \| "stopped" }`; `stopped` is a `task_wait` the turn's Stop ended (the task runs on) or whose task was stopped |
|
|
60
60
|
| `:session_end` | After each turn `:after_turn` fires for, after it (turn-level lifecycle) | `{ type: :session_end, session_id: "..." }` |
|
|
61
61
|
|
|
62
62
|
Every event also carries the hook runtime (next section): `hook:` (the label
|
|
@@ -444,7 +444,8 @@ stripped of surrounding whitespace, except file text, an edit's
|
|
|
444
444
|
A `write`'s `content:` is the file text; an `edit` carries `old_text:` and
|
|
445
445
|
`new_text:` (its `content:` is `""`). A plugin or MCP tool's call has its
|
|
446
446
|
arguments whole on `args:` (string keys) instead, and its `content:` is those
|
|
447
|
-
arguments as JSON.
|
|
447
|
+
arguments as JSON. An MCP tool's call is the mcp bundle's `mcp_call`: `tool:
|
|
448
|
+
"mcp_call"`, with `args: {"tool" => "<server>/<tool>", "args" => {…}}`.
|
|
448
449
|
|
|
449
450
|
## Bundle Hooks (unified workflow bundle)
|
|
450
451
|
|
|
@@ -23,6 +23,6 @@ Behavior:
|
|
|
23
23
|
- `task_get` returns metadata and `output_path`; use `read` for output contents.
|
|
24
24
|
- `task_wait` accepts `timeout`, `tail_lines` (maximum 100), and `done_pattern` (a regular expression string).
|
|
25
25
|
- `task_create` accepts `env` as a JSON object string for deterministic overrides such as `PATH`; use an absolute interpreter path when that is simpler. Ruby/Bundler isolation variables remain protected.
|
|
26
|
-
- `execute` and `task_create` export chi's own markers into the command, set over the model's `env` (which can't set them: `Error: env key is reserved`): `SAMAGOTCHI_PARENT_SESSION` (the session's id, `chi` without one; a chi started there answers as a parent agent) and `SAMAGOTCHI_SESSION_MODEL` (the resolved ref the session runs on, read at each call so it follows `/model`; unset when unknown). `chi self` reads the latter for its `model` row.
|
|
26
|
+
- `execute` and `task_create` export chi's own markers into the command, set over the model's `env` (which can't set them: `Error: env key is reserved`): `SAMAGOTCHI_PARENT_SESSION` (the session's id, `chi` without one; a chi started there answers as a parent agent) and `SAMAGOTCHI_SESSION_MODEL` (the resolved ref the session runs on, read at each call so it follows `/model`; unset when unknown). `chi self` reads the latter for its `model` row. A session's worker never inherits them from whoever started it (a `chi send` run by another session's `execute`): its other commands (a user's `!cmd`, an MCP server, a context command) have none.
|
|
27
27
|
|
|
28
28
|
Work that needs a model, not a shell command, goes to a child chi session instead: `delegate` / `delegate_result` have the same shape (start, then wait) and return only the child's final reply; see [Sessions: Delegating](../sessions.md#delegating).
|
data/docs/internals/engine.md
CHANGED
|
@@ -192,7 +192,7 @@ One iteration with tool calls:
|
|
|
192
192
|
→ returns {output:, capped_output:, truncated:, activity:, images:, shown_params:,
|
|
193
193
|
shown_label:, diff:}
|
|
194
194
|
→ :tool_dispatch_completed {iteration, call_count}
|
|
195
|
-
→ native: ToolResponse.joined(runs), one entry with the
|
|
195
|
+
→ native: ToolResponse.joined(runs), one entry with the capped outputs
|
|
196
196
|
chat: ToolResponse.single(run, tool_call_id:) per call, with the capped output
|
|
197
197
|
```
|
|
198
198
|
|
|
@@ -207,7 +207,9 @@ One iteration with tool calls:
|
|
|
207
207
|
- For edit and write: diffs the file before and after, and refreshes a memory's index line
|
|
208
208
|
(`MemoryBundle::IndexSync`).
|
|
209
209
|
- Attaches returned images, at most `MAX_IMAGES_PER_RESULT` (4) per result.
|
|
210
|
-
- Scrubs bytes that aren't UTF-8, and caps the
|
|
210
|
+
- Scrubs bytes that aren't UTF-8, and caps the output at `max_tool_output_chars`: a longer one is cut and ends
|
|
211
|
+
with `[cut: N of M chars; read it in parts]` (N kept of M, the whole within the cap unless the cap is shorter
|
|
212
|
+
than that line). The model (both loops), the event and the after_tool_call hook get that text.
|
|
211
213
|
- Emits `:tool_call_completed` {iteration, call_count, call_index, tool, output, output_truncated, activity,
|
|
212
214
|
images, diff, waited_ms, view}, then fires `:after_tool_call` {iteration, tool, output, status}.
|
|
213
215
|
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# The llm_context replay benchmark
|
|
2
|
+
|
|
3
|
+
`script/llm_context_bench.rb` replays stored chi sessions offline and reports what an LLM context strategy
|
|
4
|
+
(`llm_context.strategy`) would have changed in them. Every strategy or policy change gets a number from it before it
|
|
5
|
+
becomes a default. It is a development tool: it lives under `script/`, isn't shipped with the gem, and no spec runs
|
|
6
|
+
it on real sessions (the specs use small synthetic ones).
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
ruby script/llm_context_bench.rb [SESSIONS_DIR] [options] # --help lists the options
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`SESSIONS_DIR` is a folder of `<session id>.json` files: an argument, else `$LLM_CONTEXT_BENCH_SESSIONS`, else chi's
|
|
13
|
+
own (`~/.local/state/samagotchi/sessions`). The benchmark only reads it. Sessions with fewer than two turns are
|
|
14
|
+
skipped; `--top N` keeps the N heaviest, `--session PREFIX` picks some.
|
|
15
|
+
|
|
16
|
+
It measures what chi would send: sessions load through `Session.from_h`, a native batch's joined result splits into
|
|
17
|
+
its runs as `LLMContextView` splits it (calls read by chi's `ToolCallParser`), every output is named by its
|
|
18
|
+
`ToolIds` id, and a strategy's edits are saved as `LLMContextEdit` records on copies of the entries and rendered
|
|
19
|
+
through `LLMContextView`. Tokens are chars/4 throughout, which undercounts code-heavy prompts by about 1.25x.
|
|
20
|
+
|
|
21
|
+
## What it reports
|
|
22
|
+
|
|
23
|
+
**The profile (none, every session):** the resent tokens by category (tool outputs, call arguments, the system
|
|
24
|
+
prompt, prose, user messages, context notes, a native session's inline thinking; stored thinking apart, since chi
|
|
25
|
+
never resends it), tool outputs by tool and size, turns, how much of a turn's output a later turn refers to, and
|
|
26
|
+
repeat reads.
|
|
27
|
+
|
|
28
|
+
**A row per strategy × model × policy**, summed over the cases. A case is the end of a turn that has a next turn,
|
|
29
|
+
scored at the next turn's first request. Cases are named `<first 8 of the session id>_t<turn>`; they come from a
|
|
30
|
+
`--cases FILE` (one name per line), else from the picks given, else every turn with at least `--min-turn-tool`
|
|
31
|
+
(3000) tokens of its own tool output.
|
|
32
|
+
|
|
33
|
+
Each case says how its turn ends: `answer` (the model's final prose, no calls), `tool_result` (it stopped mid-task on
|
|
34
|
+
a tool's output), `turn_note` (chi's note on a turn that ended without an answer), `tool_call` (calls never
|
|
35
|
+
answered) or `empty`. The report's second line tallies them, `--per-case` has an `ends` column, and the JSON has
|
|
36
|
+
`case_ends` (name => ending) and `ends` on each case. `--ends answer|any` picks which turns make cases: by default the
|
|
37
|
+
turn-size rule keeps only `answer` ends, the point where a forget-at-turn-end offer would come (the layout check
|
|
38
|
+
found a model behaves differently mid-task), and named cases (`--cases`, picks) are kept as named. `--ends answer`
|
|
39
|
+
drops named cases that end otherwise and says how many; `--ends any` takes every turn.
|
|
40
|
+
|
|
41
|
+
| Column | Meaning |
|
|
42
|
+
|---|---|
|
|
43
|
+
| forgot/outputs | outputs the strategy edits / outputs in context at the case |
|
|
44
|
+
| freed/tool | what the prompt loses, stubs counted / the tool output tokens in context |
|
|
45
|
+
| wrong strict (base) | edited outputs a later step needs: from the step the edit reaches through the end of the next turn, a call re-reads the file or re-runs the command. In brackets the base rate: the share of all outputs in context that are needed, what a random pick scores |
|
|
46
|
+
| wrong loose (base) | the same, counting a mention of the output's path or of an identifier it brought in |
|
|
47
|
+
| 1-step | edited outputs the step the edit reaches re-reads (a read of the file, not an edit) or re-runs. Offline that step is the session's own, made without the stub, so it is a floor, not the stub's effect |
|
|
48
|
+
| re-prefilled | what the server prefills again because an edit broke its prompt cache: from the earliest changed entry to the end of what the previous request left cached. A case counts the breaks of its own window only (after the previous turn's case point), so a strategy that edits as the session goes isn't counted twice |
|
|
49
|
+
| breaks | the requests of the case's window that an edit first reaches: one prompt-cache break each, a batch's edits together |
|
|
50
|
+
|
|
51
|
+
The proxies are the context-edit spike's (lexical and lenient: a basename mention counts).
|
|
52
|
+
|
|
53
|
+
## Strategies
|
|
54
|
+
|
|
55
|
+
- `none`: nothing changes. The base.
|
|
56
|
+
- `forget_all`: at the turn's end, forget every output in context with an empty note. The spike's base rate; about
|
|
57
|
+
what a model told "free context now" does.
|
|
58
|
+
- `stale`: chi's own stale layer (`Samagotchi::LLMContextStale`) as chi runs it: each read a later read covering its
|
|
59
|
+
lines superseded is stubbed from the request after that read (chi applies it at the next request), so a case's
|
|
60
|
+
numbers count every stub since the session began. An edit or write of the file supersedes nothing, nor does a read
|
|
61
|
+
that came back as a preview or cut. A relative
|
|
62
|
+
path is taken against the session's working directory.
|
|
63
|
+
- `stale_edits_next_request`, `stale_edits_turn_end`, `stale_edits_payoff`: the stale layer with edit-driven stubs
|
|
64
|
+
under an `llm_context.apply` rule, as chi runs it (`Samagotchi::LLMContextApply`, `llm_context.protect_steps`'
|
|
65
|
+
default), over each session from its start: at each request, and at the end of each turn that ends with the
|
|
66
|
+
model's answer (those stubs reach the next turn's first request, the case point). The top context bucket isn't
|
|
67
|
+
modelled (the bench has no window), so `payoff` applies at a request only when the freed tokens are at least the
|
|
68
|
+
tail. A turn-end batch's re-prefill is counted at the next turn's first request even where chi's warm-up would
|
|
69
|
+
prefill it while the user reads.
|
|
70
|
+
- `forget_outputs` (P4): a slot. It prints "not built yet" until its phase adds it to
|
|
71
|
+
`LLMContextBench::Strategies`; a strategy answers `#plans(kase)` with `Plan`s of `PlannedEdit`s (the output id, the
|
|
72
|
+
edit kind, the note, the request it first reaches), and the scorer and the report take it as is.
|
|
73
|
+
- `--picks LABEL=DIR`: model picks recorded as responses, one file per pick, `<case>.<variant>.json` (the spike's
|
|
74
|
+
`out/<model>/` files, or what `--live` saves). The variant is the row's policy, the label its model.
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
ruby script/llm_context_bench.rb "$SESSIONS" --strategy none,forget_all,stale --per-case
|
|
78
|
+
ruby script/llm_context_bench.rb "$SESSIONS" --strategy none,stale,stale_edits_turn_end,stale_edits_payoff --ends any
|
|
79
|
+
ruby script/llm_context_bench.rb "$SESSIONS" --picks deepseek=picks/deepseek --picks splash=picks/splash --json
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Live picks (the D7 tool-name A/B)
|
|
83
|
+
|
|
84
|
+
The one part that calls a model, and only with `--live MODEL`: at each case it asks the model which of its tool
|
|
85
|
+
outputs to forget, through chi's own chat client (the model must be on an `api: openai` host), and saves each
|
|
86
|
+
answer under `--out DIR` for `--picks` to score. A saved pick is never asked for again. The request is the
|
|
87
|
+
session's conversation up to the turn's end (thinking dropped, every tool result led by its id `[#tN]`), chi's
|
|
88
|
+
built-in chat tools plus the forget tool, and the policy's tail line; a model that doesn't call the tool is asked
|
|
89
|
+
once more with it forced (`--no-force` asks once).
|
|
90
|
+
|
|
91
|
+
A pick record holds the answer (`unforced`), the forced one when it took that (`forced`) and why it was forced
|
|
92
|
+
(`forced_because`: `no_call`, or `length` when the unforced reply was cut off at `max_tokens`, finish reason
|
|
93
|
+
`length`, before it could call anything), how it was asked (`bench`), and `cost`: what the server reported for the
|
|
94
|
+
pick's requests, summed (OpenRouter's `usage.cost`, asked for with `usage.include`; absent when the server sends
|
|
95
|
+
none). The run's last line counts the picks written, the forced ones, those forced after a cut, the errors, and sums
|
|
96
|
+
the cost.
|
|
97
|
+
|
|
98
|
+
A request that fails is saved as an error record and the run goes on to the next case, except a payment error: a
|
|
99
|
+
402, or an error that mentions credits or balance, stops the run at once (exit 1), saves nothing for that case, and
|
|
100
|
+
says how many picks it wrote before it. A rerun with the same `--out` asks only for the rest.
|
|
101
|
+
|
|
102
|
+
- `--tool-name forget_outputs|forget_llm_context`: the name under test; the description stays the same.
|
|
103
|
+
- `--policy subtask|soft|now`: `subtask` offers the tool and puts the plan's policy line in its description;
|
|
104
|
+
`soft` and `now` are the spike's selective and "free context now" lines.
|
|
105
|
+
- `--layout tail_system|tail_user|boundary`: where the tail line goes. `tail_system` (the default) is a system
|
|
106
|
+
message after the case's last entry; `tail_user` the same line as a user message; `boundary` a system message
|
|
107
|
+
worded as a stopping point. A layout other than the default names its picks `<policy>_<layout>`.
|
|
108
|
+
- `--samples N`, and `--dry-run` to count the requests and prompt tokens (and the cases' endings) without calling
|
|
109
|
+
anything.
|
|
110
|
+
|
|
111
|
+
Case selection matters here: at a turn that ends with the model's answer the forget offer reads as a real turn end,
|
|
112
|
+
while mid-task (a `tool_result` or `turn_note` end) a model mostly carries on with the task, or, worded as a
|
|
113
|
+
stopping point, forgets at the base rate. A forget-at-turn-end test should run on `answer` cases, the default for
|
|
114
|
+
the turn-size rule; give `--ends answer` with a `--cases` file to hold one to that too.
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
ruby script/llm_context_bench.rb "$SESSIONS" --cases cases.txt --live openrouter:deepseek/deepseek-v4.1-flash \
|
|
118
|
+
--tool-name forget_llm_context --policy subtask --out picks/deepseek --dry-run
|
|
119
|
+
```
|
|
@@ -110,10 +110,11 @@ is prefilled once. The native and the chat loop use the same plan.
|
|
|
110
110
|
## Reading the numbers
|
|
111
111
|
|
|
112
112
|
- The debug log's `generation_completed` line: `prompt=` (the request's prompt tokens), `cached=` (read from the
|
|
113
|
-
server's cache)
|
|
113
|
+
server's cache), `cache_write=` (written to it, where the provider reports writes) and `reprefill=` (of the last
|
|
114
|
+
request's prompt on the same model, what was prefilled again). The `http` line of a
|
|
114
115
|
remote Claude request shows `cache=on` (or `cache=1h`) when it carried breakpoints.
|
|
115
116
|
- The warm-up logs `model warmup` with `cached=`, `prefilled=` and `prefill_ms=`.
|
|
116
|
-
- `/stats`: "cached N (P%)"
|
|
117
|
+
- `/stats`: "cached N (P%)", "cache writes" and "re-prefilled" for the session.
|
|
117
118
|
- A healthy session: from the second request on `cached=` is close to `prompt=`; a second fresh session in the
|
|
118
119
|
same project reads everything up to the volatile tail.
|
|
119
120
|
|