samagotchi 0.75.0 → 0.77.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/docs/architecture.md +11 -4
- data/docs/cli.md +13 -1
- data/docs/configuration.md +4 -3
- data/docs/guardrails.md +9 -0
- data/docs/internals/anthropic.md +3 -1
- data/docs/internals/bundles.md +20 -7
- data/docs/internals/context-telemetry.md +22 -0
- data/docs/internals/engine.md +15 -4
- data/docs/internals/prompt-caching.md +2 -1
- data/docs/memory.md +34 -0
- data/docs/plugins.md +43 -1
- data/docs/releasing.md +6 -2
- data/docs/sessions.md +3 -1
- data/lib/samagotchi/answer_command.rb +5 -5
- data/lib/samagotchi/bridge.rb +3 -3
- data/lib/samagotchi/bundle_command.rb +52 -39
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/codex-peer/plugin.rb +19 -2
- data/lib/samagotchi/bundles/core/manifest.yml +2 -2
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +12 -10
- data/lib/samagotchi/bundles/source-links/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/child_reports.rb +1 -1
- data/lib/samagotchi/desktop/macos.rb +1 -2
- data/lib/samagotchi/effective_model.rb +5 -0
- data/lib/samagotchi/engine.rb +105 -16
- data/lib/samagotchi/guardrails/parent_approvals.rb +4 -4
- data/lib/samagotchi/guardrails/parent_continue.rb +2 -2
- data/lib/samagotchi/kernel_loop.rb +13 -10
- data/lib/samagotchi/llm_context_override.rb +8 -6
- data/lib/samagotchi/memory_bundle/asset_installer.rb +34 -5
- data/lib/samagotchi/memory_bundle/builder.rb +13 -14
- data/lib/samagotchi/memory_bundle/installer.rb +122 -5
- data/lib/samagotchi/memory_bundle/manifest.rb +20 -1
- data/lib/samagotchi/memory_bundle/memory_files.rb +24 -6
- data/lib/samagotchi/memory_bundle/profile.rb +1 -1
- data/lib/samagotchi/memory_bundle/shipped_update.rb +2 -1
- data/lib/samagotchi/memory_bundle/source.rb +66 -3
- data/lib/samagotchi/memory_bundle/system_bundle.rb +4 -2
- data/lib/samagotchi/message_parts.rb +272 -0
- data/lib/samagotchi/model_price.rb +4 -3
- data/lib/samagotchi/parent_report.rb +30 -22
- data/lib/samagotchi/paths.rb +15 -1
- data/lib/samagotchi/pending_question.rb +49 -0
- data/lib/samagotchi/question_desk.rb +39 -32
- data/lib/samagotchi/relay.rb +8 -9
- data/lib/samagotchi/relay_verifier.rb +2 -2
- data/lib/samagotchi/relay_watcher.rb +2 -2
- data/lib/samagotchi/reply_wait.rb +4 -4
- data/lib/samagotchi/row_refs.rb +1 -1
- data/lib/samagotchi/session.rb +22 -14
- data/lib/samagotchi/session_commands.rb +5 -0
- data/lib/samagotchi/session_manager.rb +1 -2
- data/lib/samagotchi/session_metrics/model_segment.rb +109 -0
- data/lib/samagotchi/session_metrics/totals.rb +127 -0
- data/lib/samagotchi/session_metrics.rb +118 -83
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +3 -2
- data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -2
- data/lib/samagotchi/terminal_ui/formatting.rb +24 -10
- data/lib/samagotchi/terminal_ui/model_segments_text.rb +118 -0
- data/lib/samagotchi/terminal_ui/status_row.rb +3 -1
- data/lib/samagotchi/terminal_ui.rb +5 -3
- data/lib/samagotchi/tool_output_cut.rb +1 -1
- data/lib/samagotchi/tool_response.rb +13 -13
- data/lib/samagotchi/tool_row_fields.rb +1 -1
- data/lib/samagotchi/tool_runner.rb +16 -9
- data/lib/samagotchi/toolkit.rb +9 -8
- data/lib/samagotchi/toolkit_command.rb +2 -2
- data/lib/samagotchi/tools/builtins.rb +1 -5
- data/lib/samagotchi/tools/delegate_relay.rb +7 -7
- data/lib/samagotchi/tools/delegate_wait.rb +1 -1
- data/lib/samagotchi/tools/web_fetch.rb +3 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +5 -2
- data/lib/samagotchi/web/public/app.js +21 -58
- data/lib/samagotchi/web/public/chains.js +1 -1
- data/lib/samagotchi/web/public/composer.js +9 -5
- data/lib/samagotchi/web/public/ctx.js +84 -11
- data/lib/samagotchi/web/public/index.html +32 -36
- data/lib/samagotchi/web/public/info_bar_model.js +2 -1
- data/lib/samagotchi/web/public/markdown_place.js +53 -0
- data/lib/samagotchi/web/public/question_cards.js +1 -1
- data/lib/samagotchi/web/public/session_state.js +3 -0
- data/lib/samagotchi/web/public/stage_slots.js +3 -3
- data/lib/samagotchi/web/public/stream_handlers.js +38 -3
- data/lib/samagotchi/web/public/timing.js +11 -2
- data/lib/samagotchi/web/public/toasts.js +69 -0
- metadata +8 -2
- data/lib/samagotchi/web/message_parts.rb +0 -274
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 433cf999c039d529cdf878223e798ed00b23efc0a35393c48111c0b1f7b597a7
|
|
4
|
+
data.tar.gz: eb13e65818fc0b2efaa04d28315d9693856ff77861d5643d668702fe80cdeca1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 58012cc677a1b2c14e358dabed798856f88a8a74f59dff1606f08dca297ae78d5f89d69c392b2254523308996a67a86ff828205e07a7eac7ea0b7adf99961d79
|
|
7
|
+
data.tar.gz: 7332bff82c9060f9ad9a77063891ced098572ce28861fbe2a65ab9486076478d40f1731fec13f41ec9dc4a15fc60e14ac023b40255c0356993bfe22969817986
|
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.77.0] - 2026-10-17
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- A session's worker starts about 80 ms faster: it loads the HTML parser (Nokogiri) only when `web_fetch` first reads
|
|
16
|
+
an HTML page.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Web: toasts stack (up to three) instead of replacing one another, so a later notice no longer hides a sticky error
|
|
21
|
+
such as "Can't send: …"; each one has its own timer and ✕.
|
|
22
|
+
- After `/model`, a session's card drops the old model's context % right away (the new window's %, or none while it
|
|
23
|
+
is unknown), in every tab and after a reload, instead of keeping it until the next turn ends.
|
|
24
|
+
- Web: the context chip and its tooltip update as soon as a generation starts, not at its first chunk.
|
|
25
|
+
- Web: a model ref longer than 40 characters is cut in the middle in the ctx tooltip's per-model rows, so it no
|
|
26
|
+
longer stretches the tooltip.
|
|
27
|
+
- REPL (`--no-shared`): the status row drops the old context % after `/model` when the new window is unknown.
|
|
28
|
+
- source-links: a URL in the terminal answer no longer takes in trailing punctuation (`.`, `,`, `)` …) or an unmatched
|
|
29
|
+
`]`, the same as in the web.
|
|
30
|
+
- `chi bundle build` skips a symlinked memory (`notes.md is a symlink (not included)`) instead of copying the file it
|
|
31
|
+
links to into the bundle.
|
|
32
|
+
|
|
33
|
+
Update with `chi update` (the system bundle 0.77.0, `source-links` 0.5.1), then restart `chi web`.
|
|
34
|
+
|
|
35
|
+
## [0.76.0] - 2026-10-17
|
|
36
|
+
|
|
37
|
+
### Security
|
|
38
|
+
|
|
39
|
+
- A bundle can no longer replace an installed one by taking its name. `chi bundle install` over a bundle from another
|
|
40
|
+
source is refused (`--force` too): `chi bundle upgrade <new source>` switches the source and says
|
|
41
|
+
`Source changes: <old> → <new>`. A name chi ships (`guardrails`, `samagotchi-system`, …, in any letter case)
|
|
42
|
+
installs only from chi's own copy unless `--force`, and a name that differs from an installed one only in case is
|
|
43
|
+
refused.
|
|
44
|
+
- A bundle's symlinked memory is no longer installed: a tar, zip or git repo whose `notes.md` linked to another file
|
|
45
|
+
(`~/.ssh/…`, an API key) copied that file into the memories the model reads. It is skipped with
|
|
46
|
+
`notes.md is a symlink (not installed)`; a linked hook or guardrail rule is skipped too, and a linked plugin or
|
|
47
|
+
script fails the install.
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- `/stats`, `chi sessions stats` and the web's ctx tooltip split tokens, cost and speed per model after a `/model`
|
|
52
|
+
switch: a row per run of turns on one model, each switch's prefill under it, and a total.
|
|
53
|
+
- Docs: [chi-nah](https://github.com/dm1try/chi-nah), an external guard bundle, in docs/guardrails.md.
|
|
54
|
+
|
|
55
|
+
### Changed
|
|
56
|
+
|
|
57
|
+
- A bundle installed from a git URL is named by its manifest's `name:` (it took the repo's name), so a URL install and
|
|
58
|
+
a local copy are one bundle. One installed by URL before this under a different repo name: `chi bundle uninstall
|
|
59
|
+
<repo name>`, then install it again (chi says so).
|
|
60
|
+
- The `core` and `dev` profiles require the chi their members need (0.60.0 and 0.72.0).
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
|
|
64
|
+
- After `/model` the ctx % no longer shows the old model's window: it uses the new model's window at once, or shows
|
|
65
|
+
`ctx ?` (tooltip `Context window unknown until the next request`) until the window is known. The same for a session
|
|
66
|
+
resumed or woken on another model.
|
|
67
|
+
- `/stats` no longer counts a `/model` to the same model on another host as a re-prefill.
|
|
68
|
+
- Web: switching sessions while a command or a restored prompt re-reads no longer draws it in the session just
|
|
69
|
+
opened; a command's answer no longer clears another session's draft; such failures are a toast, not an alert.
|
|
70
|
+
- `chi bundle install|upgrade`: an invalid or missing `manifest.yml` fails with one clear line (no backtrace, no
|
|
71
|
+
"falling back to install"); a manifest `name:` that is a path is refused; a plain re-install keeps the record of
|
|
72
|
+
files the bundle stopped shipping, so the upgrade it suggests trashes them; `chi bundle upgrade` with no source
|
|
73
|
+
prints the full usage line.
|
|
74
|
+
- codex-peer: a send no longer hangs when `codex queue` exits but leaves a process holding its output; it stops
|
|
75
|
+
after 10 s and says the message may already be queued.
|
|
76
|
+
|
|
77
|
+
Update with `chi update` (the system bundle 0.76.0, `core` 0.3.1, `dev` 0.5.1, `codex-peer` 0.3.2), then restart
|
|
78
|
+
`chi web`.
|
|
79
|
+
|
|
11
80
|
## [0.75.0] - 2026-10-17
|
|
12
81
|
|
|
13
82
|
### Changed
|
|
@@ -3176,7 +3245,9 @@ and long-lived sessions.
|
|
|
3176
3245
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
3177
3246
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
3178
3247
|
|
|
3179
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
3248
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.77.0...HEAD
|
|
3249
|
+
[0.77.0]: https://github.com/dm1try/samagotchi/compare/v0.76.0...v0.77.0
|
|
3250
|
+
[0.76.0]: https://github.com/dm1try/samagotchi/compare/v0.75.0...v0.76.0
|
|
3180
3251
|
[0.75.0]: https://github.com/dm1try/samagotchi/compare/v0.74.0...v0.75.0
|
|
3181
3252
|
[0.74.0]: https://github.com/dm1try/samagotchi/compare/v0.73.0...v0.74.0
|
|
3182
3253
|
[0.73.0]: https://github.com/dm1try/samagotchi/compare/v0.72.0...v0.73.0
|
data/docs/architecture.md
CHANGED
|
@@ -327,10 +327,16 @@ which `SessionMetrics#persist` rewrites after each turn; the web server merges t
|
|
|
327
327
|
The totals' `tokens` block adds the cached, cache-write and reasoning sums, the reported cost (`cost_sum`) and the
|
|
328
328
|
estimated one (`cost_estimate_sum`, from `hosts.<name>.models` prices; 0 for none), the decode time and tokens
|
|
329
329
|
behind the average speed (`avg_decode_tps`) and the last speeds (`last_decode_tps`,
|
|
330
|
-
`last_prefill_tps`, `tps_source`: `server` or `estimate`).
|
|
330
|
+
`last_prefill_tps`, `tps_source`: `server` or `estimate`). `model_segments` (always present) splits those sums
|
|
331
|
+
by model after a `/model` switch: one row per run of consecutive turns on one model (`SessionMetrics::ModelSegment`,
|
|
332
|
+
keyed on the turn records' `model_ref`), in order, with its turns, token and cost sums, speeds and the switch's prefill
|
|
333
|
+
(`prefill_tokens`, `prefill_cost_estimate`: the sums of its turn records' `switch_prefill_tokens` and
|
|
334
|
+
`switch_prefill_cost_estimate`, [sessions.md](sessions.md)); `[]` with one model, the oldest rows folded into one (`folded_segments`)
|
|
335
|
+
past 6; a running turn counts in its model's row (`ended_at` null). The rows add up to `tokens`. They are derived from
|
|
336
|
+
the records, never read back. Each `generation_completed` event
|
|
331
337
|
carries the generation's `speed` (`{decode_tps, source}`, null without one), its own prompt-cache
|
|
332
338
|
counts (`prompt_tokens`, `cached_tokens`, `cache_write_tokens`: null when the server reported no writes) and
|
|
333
|
-
those running `tokens`: the Engine closes the generation in `SessionMetrics#finish_generation` before relaying
|
|
339
|
+
those running `tokens` and `model_segments`: the Engine closes the generation in `SessionMetrics#finish_generation` before relaying
|
|
334
340
|
it, so the web's info bar updates per generation without parsing chunk payloads.
|
|
335
341
|
|
|
336
342
|
The page's frequent reads of `GET /api/sessions/:id` stay light too:
|
|
@@ -504,6 +510,7 @@ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/
|
|
|
504
510
|
|---|---|---|
|
|
505
511
|
| Worker | `worker.rb` (`worker_idle_exit.rb`, `worker_inbound.rb`, `worker_signals.rb`) | A session's background process: builds the `Engine` and `Bridge`, runs queued turns, exits when idle. A spawned one traps TERM, HUP and INT (`WorkerSignals`, installed by `run_session_loop` when `SAMAGOTCHI_WORKER_PROCESS=1`): the running turn stops as a Stop with reason `signal`, is saved, and the worker leaves (`:signal`, exit 0; a new worker only for input already queued). A running turn is checkpointed at its iteration boundaries (`TurnCheckpoint`, the `interrupted_turn` mark), and the next worker settles a turn its predecessor died in (`TurnCheckpoint.settle`). The session is saved from the turn's thread, Bridge threads and a signal's thread; `Session#save` takes a per-session lock around its snapshot and write, so an older snapshot never lands over a newer one. |
|
|
506
512
|
| Config | `config.rb` | Every setting as an `Entry`: env, config.yml and CLI layers ([configuration.md](configuration.md)). |
|
|
513
|
+
| Paths | `paths.rb` (`Paths`) | The XDG base dirs chi's files live under, read from the env on every call: `Paths.state_home` (`$XDG_STATE_HOME`, else `~/.local/state`) and `Paths.config_home` (`$XDG_CONFIG_HOME`, else `~/.config`), both defaults under `Dir.home`; a blank variable counts as unset, a relative one is made absolute from the current directory. `Paths.tilde` shows a path with `~` for the home folder. `SessionManager` hands a spawned worker the spawner's `XDG_STATE_HOME` / `XDG_CONFIG_HOME`, when set, made absolute, so a relative one isn't read from the session's folder. |
|
|
507
514
|
| MemoryBundle | `memory_bundle.rb`, `memory_bundle/{installer,builder,installed_bundle,plugin_ref,bundle_hook}.rb` (`Installer`, `Builder`, `InstalledBundle`, `PluginRef`, `BundleHook`) | Installing, building and reading bundles of memories, hooks and plugins. |
|
|
508
515
|
| Plugin API | `plugin/api.rb` (`Plugin::Api`), `plugin/tool_spec.rb` (`Plugin::ToolSpec`: a checked tool declaration, how it is registered), `plugin/loader.rb` | What a bundle's plugin registers: tools, commands, annotation actions, hooks, services, toolkits, peer sources ([plugins.md](plugins.md)). |
|
|
509
516
|
| Peer bundles' base | `plugin/peer.rb` (`Plugin::Peer`) | What claude-peer and codex-peer share (the send tool, the command, the cards, payloads); each bundle's subclass sets its strings and transport ([plugins.md](plugins.md#the-claude-peer-bundle)). |
|
|
@@ -515,8 +522,8 @@ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/
|
|
|
515
522
|
| Idle work | `idle_scheduler.rb` (`IdleScheduler`), `idle_recap.rb` (`IdleRecap`), `idle_reminders.rb` | One poller for an idle session's recap and reminders. |
|
|
516
523
|
| Attached context | `context_sources.rb` (`ContextSources`), `context_poller.rb`, `context_absorber.rb`, `peer_sources.rb` (`PeerSources`: the sources peer bundles declare) | External sources a session keeps fresh, and the peer loop's wakes ([context.md](context.md)). |
|
|
517
524
|
| LLM context strategies | `llm_context_strategy.rb`, `llm_context_view.rb`, `llm_context_stale.rb`, `llm_context_forget.rb` | What a request sends of the history (none, stale, forget) ([internals/llm-context-forget.md](internals/llm-context-forget.md)). |
|
|
518
|
-
| Session chains | `session_continue.rb` (`SessionContinue`), `session_chain.rb` (`SessionChain`) | A session that continues an earlier one. |
|
|
519
|
-
| Analytics | `analytics_file.rb` (`AnalyticsFile`) | A session's `analytics.json` (`SessionMetrics#persist`). |
|
|
525
|
+
| Session chains | `session_continue.rb` (`SessionContinue`), `session_chain.rb` (`SessionChain`) | A session that continues an earlier one. `SessionChain.opener_for` gives the text web `continue →` prefills in the new link: the previous link's own first typed message when that link continues one itself, else nil (the 201's `opener`; not saved) ([sessions.md](sessions.md)). |
|
|
526
|
+
| Analytics | `analytics_file.rb` (`AnalyticsFile`), `session_metrics/totals.rb` (`SessionMetrics::Totals`), `session_metrics/model_segment.rb` (`SessionMetrics::ModelSegment`) | A session's `analytics.json` (`SessionMetrics#persist`); the sums over its turn records (`Totals`: tokens, cost, speeds, kept as running counters) and their split per model after a `/model` switch (`ModelSegment`: a run of turns on one model, the `model_segments` rows). |
|
|
520
527
|
| Images / vision | `image_store.rb` (`ImageStore`), `vision_support.rb`, `vision_context.rb` | Stored prompt images and whether a model takes them. |
|
|
521
528
|
| Toolkits | `toolkit.rb` (`Toolkit`), `toolkit_binding.rb` (`ToolkitBinding`, the Engine's seam), `toolkit_command.rb` (`/toolkit`), `tools/registry.rb` (`Registry#toolkit`, `#left_out_names`), `web/public/toolkit_chip.js` | Which registered tools a session's prompts declare: saved in the session, resolved at a turn's start (`Engine#apply_toolkit!`), named in the prompt's not-loaded line, a kit's prompt text after the appended files; `chi.toolkit` and `ctx.use_toolkit` for plugins ([configuration.md](configuration.md#toolkits)). |
|
|
522
529
|
| Tool index | `tools/tool_index.rb` (`Tools::ToolIndex`) | `find_tools` and `call_tool`: the tools a session knows but doesn't declare one by one (an mcp bundle's servers through `chi.tool_source`, the built-ins its toolkit leaves out), searched and called by name ([plugins.md](plugins.md)). |
|
data/docs/cli.md
CHANGED
|
@@ -469,7 +469,17 @@ prompt (last, server)`); for other servers it is estimated from the first
|
|
|
469
469
|
streamed chunk to the end and marked `~` (`~64 tok/s out (last, estimate)`).
|
|
470
470
|
Generations under 8 tokens or 100 ms get no speed of their own, nor does a
|
|
471
471
|
reply without server token counts or a non-streamed one. A delegated child
|
|
472
|
-
keeps its own numbers: a parent's tokens and cost leave them out.
|
|
472
|
+
keeps its own numbers: a parent's tokens and cost leave them out.
|
|
473
|
+
|
|
474
|
+
After a `/model` switch (or a hook's) the tokens line becomes a `models:` block, a row per run of turns on one
|
|
475
|
+
model, in order (A → B → A is three rows): the model (`host:model` as asked), when (`10:43–now` while it runs; with
|
|
476
|
+
the day when the rows span several), turns, tokens in/out, its average speed and cost. Under the row of the model
|
|
477
|
+
switched to, `└ switch prefill 58.0k uncached prompt ≈$0.01` says what its first request prefilled on the new model
|
|
478
|
+
(the prompt less the cached reads; `≈` an estimate from the new model's configured price, left out without one). A
|
|
479
|
+
`total` row ends the block. The speed line names the model of its last speed, and the cost is from `each model's host
|
|
480
|
+
prices`. Past 6 rows the oldest fold into one `N earlier segments` row; a session's turns from before chi recorded
|
|
481
|
+
their model are one `earlier turns` row. One model: the output is unchanged. `chi sessions stats` prints the same,
|
|
482
|
+
also offline (from the saved records). `/recap` is in
|
|
473
483
|
[Session recap](#session-recap), `/model` and `/models` in [Runtime Model
|
|
474
484
|
Switch](#runtime-model-switch-assist-mode).
|
|
475
485
|
|
|
@@ -889,6 +899,8 @@ In interactive assist mode, you can switch the request model without restarting:
|
|
|
889
899
|
Notes:
|
|
890
900
|
|
|
891
901
|
- The switch updates the request `model` field, routes to the matching host (`HostRegistry#resolve`), and resolves the prompt profile again (config, the server's chat template, the name; see "Prompt profile" in configuration.md).
|
|
902
|
+
- The ctx % follows the switch at once: the last turn's count against the new model's window (the web's info bar, the attached TUI's status row, a client that joins later), recounted at the next generation. When the new window can't be resolved the web shows `ctx ?` and the attached TUI's status row drops its ctx until then ([context telemetry](internals/context-telemetry.md)).
|
|
903
|
+
- After a switch `/stats` (and the web's ctx tooltip) splits the session's tokens, cost and speed per model, with what each switch prefilled on the new model: the `models:` block ([Session commands](#session-commands)).
|
|
892
904
|
- Without `--default` the command is session-scoped and does not rewrite config files.
|
|
893
905
|
- With `--default` the new default is written to `~/.config/samagotchi/config.yml` (honoring `XDG_CONFIG_HOME`) and takes effect for all new sessions; the current session's effective model is also updated immediately. Bare aliases and `host:model` are both valid.
|
|
894
906
|
- `--default` and `--alias` change only their one key's line in `config.yml` (`default.model`, or the alias under `model_aliases:`): comments and the rest of the file stay as written, a symlinked file is written through and keeps its mode. When the edited text wouldn't read back as the expected settings (a flow-style section, anchors), chi writes the whole file out from the parsed data instead, which drops its comments.
|
data/docs/configuration.md
CHANGED
|
@@ -836,9 +836,10 @@ cache:
|
|
|
836
836
|
```
|
|
837
837
|
Each request's own counts are in the debug log's `generation_completed` line:
|
|
838
838
|
`prompt=` (its prompt tokens), `cached=` (read from the server's cache), `cache_write=` (written to it, when the
|
|
839
|
-
server reports writes) and `reprefill=` (of the previous request's prompt on the same model
|
|
840
|
-
prefilled again: an `llm_context` edit's cache break, or anything else that rewrote the prompt's
|
|
841
|
-
server reports a cached count, and not on the first request after a warm-up
|
|
839
|
+
server reports writes) and `reprefill=` (of the previous request's prompt on the same model through the same host,
|
|
840
|
+
what the server prefilled again: an `llm_context` edit's cache break, or anything else that rewrote the prompt's
|
|
841
|
+
middle; only when the server reports a cached count, and not on the first request after a warm-up; a `/model` to the
|
|
842
|
+
same model on another host is the switch's prefill, not a re-prefill); recap and side requests log theirs as `recap request_usage`.
|
|
842
843
|
|
|
843
844
|
## Llama Model Routing
|
|
844
845
|
|
data/docs/guardrails.md
CHANGED
|
@@ -568,6 +568,15 @@ next start.
|
|
|
568
568
|
The system prompt names the home directory once, with the advice to write
|
|
569
569
|
it as `~` or `$HOME`, so the model rarely has to spell it.
|
|
570
570
|
|
|
571
|
+
## An external guard: chi-nah
|
|
572
|
+
|
|
573
|
+
[chi-nah](https://github.com/dm1try/chi-nah) is a bundle kept in its own repo (`chi bundle install
|
|
574
|
+
https://github.com/dm1try/chi-nah`). Its `before_tool_call` hook asks
|
|
575
|
+
[nah](https://github.com/manuelschipper/nah)'s deterministic guards about every `execute`, `task_create`,
|
|
576
|
+
`read`, `write` and `edit`: a call nah blocks (secret exfiltration, `rm -rf ~`, `git reset --hard`,
|
|
577
|
+
`curl | sh`…) is denied with the guard's name and how the user pauses it (`nah nap <guard>`); everything
|
|
578
|
+
else goes to chi's own rules. It needs nah installed; see its README for the settings.
|
|
579
|
+
|
|
571
580
|
## Failing closed
|
|
572
581
|
|
|
573
582
|
- A config hook with `required: true`, or a bundle `before_tool_call` hook with
|
data/docs/internals/anthropic.md
CHANGED
|
@@ -38,7 +38,9 @@ Paths are under `lib/samagotchi/llm/`.
|
|
|
38
38
|
|
|
39
39
|
`AnthropicMessages.for(entry)` takes the host's API root, key variable, `auth:`, `dialect:` and remoteness;
|
|
40
40
|
`HostRegistry#adapter_for` keeps one per host for the worker's life, so what it learns from a 400 (below) lasts that
|
|
41
|
-
long. Each request posts `{base}/v1/messages` with `anthropic-version: 2023-06-01`, the key
|
|
41
|
+
long. Each request posts `{base}/v1/messages` with `anthropic-version: 2023-06-01`, the key (`LLM::ApiKey`: the
|
|
42
|
+
variable `api_key_env:` names, else, on macOS, the login Keychain's item for that name, `HostKeys`; configuration.md,
|
|
43
|
+
"API keys from the Keychain") in the header the host's
|
|
42
44
|
`auth:` names (else the dialect's: `x-api-key` on the standard one), and `Session-Id` (plus the dialect's
|
|
43
45
|
`session_header`) when the loop has a session id.
|
|
44
46
|
|
data/docs/internals/bundles.md
CHANGED
|
@@ -22,9 +22,12 @@ user-facing side (what a bundle owns, profiles, needs) is in [memory.md](../memo
|
|
|
22
22
|
|
|
23
23
|
| Piece | Role |
|
|
24
24
|
|---|---|
|
|
25
|
-
| `BundleCommand` (`lib/samagotchi/bundle_command.rb`) | `chi bundle install/upgrade/uninstall/status/diff/list/build/trash`; runs the Installer, prints its summary, runs the conflict agent step |
|
|
25
|
+
| `BundleCommand` (`lib/samagotchi/bundle_command.rb`) | `chi bundle install/upgrade/uninstall/status/diff/list/build/trash`; runs the Installer, prints its summary, runs the conflict agent step; refuses a source already installed under another name (`installed_under_another_name?`: a git URL bundle recorded under its repo's name before the manifest named it) |
|
|
26
26
|
| `SourceNormalizer` (source.rb) | source → local dir; a git/zip/tar extract is *owned* and removed after the install |
|
|
27
|
-
| `
|
|
27
|
+
| `SourceIdentity` (source.rb) | what a source is, for comparing two installs of one name: `shipped:<dir>` (any gem or checkout), `git:<url>` (no `#ref`, trailing `/` or `.git`), `path:<realpath>`; nil when it can't place one |
|
|
28
|
+
| `PreparedSource` (source.rb) | one source normalized once (dir, owned, git commit): `BundleCommand` reads the bundle's name from its `manifest.yml` (the name for every kind of source, a git URL too) and hands it to the Installer (`prepared:`), so a source is cloned or extracted once per command |
|
|
29
|
+
| `MemoryFiles` | which top-level `*.md` files of a bundle are its memories (`.select`: the `files:` list, else all but README/LICENSE and the like), `.unshipped` (listed, not shipped) and `.symlinks` (skipped: a symlink is never a memory) |
|
|
30
|
+
| `Manifest` | parses/validates `manifest.yml` (`name:` is the bundle's name and a directory under `.bundles/`, so a path such as `../x`, `a/b`, `.` or `..` is refused: `name: … is not a bundle name`; YAML that doesn't parse or isn't a mapping is a `ValidationError` too; files, hooks as `BundleHook`, plugin, scripts, needs, requires_chi, includes, context_providers) and writes it (`Manifest.write`, for Builder). It ignores `requires_chi_next`, a shipped bundle's release-time marker (`script/release/release_tools.rb`, docs/releasing.md) |
|
|
28
31
|
| `Installer` | one install/upgrade/dry run of one bundle (below) |
|
|
29
32
|
| `AssetInstaller` | the parts that live in `.bundles/<name>/`: hooks, guardrail rules, plugin, scripts; returns `AssetInstaller::Installed` (the paths the record keeps) |
|
|
30
33
|
| `Merger` | 3-way classification per file: `install`, `fast_forward`, `keep`, `noop`, `conflict` |
|
|
@@ -47,20 +50,30 @@ and checks the recorded sha256 (and `requires_chi`) before loading.
|
|
|
47
50
|
The order matters: warnings print in it, and a plugin/script the manifest names but the bundle lacks raises
|
|
48
51
|
`InstallError` after the memories are already copied.
|
|
49
52
|
|
|
50
|
-
1. `load_source`: normalize, read `manifest.yml` (strict: a missing/invalid one raises; non-strict: a warning).
|
|
51
|
-
2. `resolve_target`: scope = CLI > manifest > `system
|
|
53
|
+
1. `load_source`: normalize (or take the `PreparedSource` `BundleCommand` passed as `prepared:`), read `manifest.yml` (strict: a missing/invalid one raises; non-strict: a warning).
|
|
54
|
+
2. `resolve_target`: scope = CLI > manifest > `system`; then `check_source` against the existing record
|
|
55
|
+
(`SourceIdentity`, source.rb): a name differing from an installed one only in case raises (`force` too); a
|
|
56
|
+
shipped name (any case) from a source that isn't a dir in the Installer's `shipped_dir:` (default `SHIPPED_DIR`;
|
|
57
|
+
ShippedUpdate, Profile and SystemBundle pass theirs) raises unless `force` (a recorded source is matched by the
|
|
58
|
+
`SHIPPED_SOURCE` path pattern instead, for older gems); a plain install
|
|
59
|
+
over a record from another source raises (`force` too); an upgrade may switch sources and sets
|
|
60
|
+
`#source_change` (the summary's first line, `Source changes: old → new`). A source `SourceIdentity` can't place
|
|
61
|
+
(an older record's relative path) never refuses.
|
|
52
62
|
3. `install_memories`, per memory (`MemoryFiles.select`: a non-empty `files:` list, else every top-level `*.md` but
|
|
53
|
-
README, CHANGELOG, LICENSE and the like; a listed name not shipped warns): an upgrade 3-way merges the files the bundle owns (`merge_memory`), a dry run only
|
|
63
|
+
README, CHANGELOG, LICENSE and the like; a listed name not shipped warns; a symlink is skipped with a warning): an upgrade 3-way merges the files the bundle owns (`merge_memory`), a dry run only
|
|
54
64
|
classifies (`preview_memory`), a plain install writes new files and skips existing ones (`skip_existing`). A
|
|
55
65
|
same-name file the bundle didn't write is the user's: skipped and never recorded (`@owned`). Model overlays
|
|
56
66
|
(`<name>.<key>.md`) get no index line (`note_overlay`).
|
|
57
67
|
4. `AssetInstaller#run`: hooks (manifest-listed, else every `hooks/*.rb`), prune dropped hooks, hook checksums,
|
|
58
|
-
guardrails, plugin, scripts; each set replaces the previous version's. requires_chi warnings for hooks/rules.
|
|
68
|
+
guardrails, plugin, scripts; each set replaces the previous version's. requires_chi warnings for hooks/rules. A
|
|
69
|
+
file that is a symlink or resolves outside the bundle dir (`AssetInstaller#linked?`, a linked `hooks/` too) is
|
|
70
|
+
skipped with a warning (a hook or rule) or raises `InstallError` (the plugin, a script).
|
|
59
71
|
5. An upgrade's `prune_dropped_files`: unchanged → trash; edited → kept (`kept_pruned`); owned by another bundle → kept.
|
|
60
72
|
6. `verify_memory_checksums` (strict), `warn_missing_needs`.
|
|
61
73
|
7. `write_provenance` (manifest, not a dry run): bases come from the incoming file, except where the user's edit was
|
|
62
74
|
kept (kept/noop/conflict/kept_pruned, or a re-install's skipped edit): those keep the old base, so the next
|
|
63
|
-
upgrade still sees the edit.
|
|
75
|
+
upgrade still sees the edit. A plain install also keeps the record (the old base) of a file an earlier install
|
|
76
|
+
owned and this version doesn't ship, while it is on disk (`dropped_owned_files`), so the next upgrade prunes it.
|
|
64
77
|
8. `detect_placeholders`.
|
|
65
78
|
|
|
66
79
|
Results are `@results` (file → `{status:, reason:}`), summarised by `#summary`. An upgrade with conflicts keeps an
|
|
@@ -34,6 +34,28 @@ Emission behavior:
|
|
|
34
34
|
`context.status_thresholds` the top bucket reads critical, the one below it
|
|
35
35
|
elevated, the next moderate, and the first bucket (and the rest below) healthy.
|
|
36
36
|
|
|
37
|
+
After `/model` (`SessionCommands#switch_model`), the old model's percentage goes at once (`Engine#switch_model!`
|
|
38
|
+
clears the kept status), and `Engine#announce_switched_context!` sends a `:context_status` of its own, outside any
|
|
39
|
+
turn: the last turn's count (the old model's tokenizer's; the next generation replaces it) against the new model's
|
|
40
|
+
window (`EffectiveModel#context_window`, the lookup below: the running server first, one `/props` GET that a remote
|
|
41
|
+
chat host is spared; else the host's cached model list; else the configured window) and the session's budget,
|
|
42
|
+
with `context_window_tokens` / `context_window_source` (the web takes the window
|
|
43
|
+
from them) and `usage.source: "switch"`. It is kept for a client that joins later (`session_state_snapshot`'s
|
|
44
|
+
`context_status`). A window that doesn't resolve sends `usage: null` and `context_window_tokens: null`: the web shows
|
|
45
|
+
`ctx ?` and drops the old model's window (a chunk's usage then counts no %, until a `generation_started` brings a
|
|
46
|
+
window), and the attached TUI's status row drops its ctx until the next generation (the web's ctx tooltip then says
|
|
47
|
+
`Context window unknown until the next request` over the session's tokens, `ctxBarTip`, until that request starts;
|
|
48
|
+
plain `Context window unknown` after it, and for a session whose window lookup failed). Nothing is sent before the
|
|
49
|
+
session's first counted turn. A
|
|
50
|
+
hook's switch (`before_turn`) sends nothing: its turn's first request counts on the new model anyway.
|
|
51
|
+
|
|
52
|
+
A process that loads the session on another model than its last turn ran on (`Engine#session=`:
|
|
53
|
+
`SessionMetrics#last_model_ref` differs from `effective_model_ref`; a `--resume`'d REPL with `--model`, a worker woken
|
|
54
|
+
after a `/model` with no turn since) drops the old window (`forget_model_reports!`) and calls
|
|
55
|
+
`announce_switched_context!(configured_only: true)`: the configured window only
|
|
56
|
+
(`EffectiveModel#configured_context_window`, no `/props` GET or model list, since a worker runs it before its Bridge
|
|
57
|
+
serves); without one, no percentage until the first generation reports the window.
|
|
58
|
+
|
|
37
59
|
Under an LLM context strategy (`ContextStatus.new(llm_context:)`, the turn's `LLMContextStrategy::Resolved`, given by
|
|
38
60
|
both loops):
|
|
39
61
|
|
data/docs/internals/engine.md
CHANGED
|
@@ -131,6 +131,13 @@ The effective model doesn't resolve (a broken host config, HostRegistry#resolve
|
|
|
131
131
|
→ session_state_snapshot never raises (its model fields are nil); a turn, stats_snapshot,
|
|
132
132
|
chat_model? and system_prompt raise, as before
|
|
133
133
|
|
|
134
|
+
A session's toolkit doesn't resolve (an unknown kit name: a typo, a bundle's kit that isn't installed; a base that
|
|
135
|
+
doesn't resolve, a cycle), at a turn's start (ToolkitBinding#apply!):
|
|
136
|
+
→ that turn declares the full kit with the session's own added and removed tools on top (Toolkit#resolve's
|
|
137
|
+
fallback); the saved toolkit stays as it is, so a reinstalled bundle gets it back
|
|
138
|
+
→ each warning (that fallback, a config.yml kit with a problem: Toolkit.kits) logged once per process as
|
|
139
|
+
toolkit_unresolved (WARN), with a :guardrail_warning (label "toolkit") to the turn and the observers
|
|
140
|
+
|
|
134
141
|
Stopped before the model was asked (prepare_turn returned false):
|
|
135
142
|
→ stopped_before_model(turn) → end_turn(turn, "canceled")
|
|
136
143
|
→ nothing kept, :turn_canceled {cancellation_reason, cancelled_by, duration_ms}
|
|
@@ -283,7 +290,7 @@ One iteration with tool calls:
|
|
|
283
290
|
replace it; known-names corrects a misspelled name this way, and the model gets one line saying what ran).
|
|
284
291
|
- Emits `:tool_call_started` {iteration, call_count, call_index, tool, call, params, label (plugin tools),
|
|
285
292
|
title, called_as, via, view, ref, refs}. The row fields (title, called_as, via, view, ref) come from `ToolRowFields.for`, which the
|
|
286
|
-
reload (`
|
|
293
|
+
reload (`MessageParts#tool_part`, after `BuiltinCalls.unwrap_saved`) calls too. `ToolRowFields::ACTIVITY_KEYS` (title, called_as, via) travel
|
|
287
294
|
inside `activity` on the completed event; `EVENT_KEYS` (view, ref, refs) go top-level on both events.
|
|
288
295
|
`Bridge::TurnAccumulator` (snapshot and replay) and the web (`activity.js` `ACTIVITY_FIELDS` /
|
|
289
296
|
`EVENT_FIELDS`) copy them by these lists, so a new row field is added there. `ref` is `FileRef.for` (a
|
|
@@ -460,11 +467,11 @@ Turn events (the main ones; `Events` in `events.rb` keeps the shared sets):
|
|
|
460
467
|
|---|---|---|
|
|
461
468
|
| `:turn_started` | `prepare_turn` | `{session_id, prompt, turn_id, continue?, resumed?, images?, cut_after}` (`resumed`: a step-limit Continue) |
|
|
462
469
|
| `:reminder_injected` | due reminders went into the turn | `{reminders}` |
|
|
463
|
-
| `:context_status` | every request (unless `context.status` is off), the current estimate before it | `{iteration, status, usage, bucket, source}` (internals/context-telemetry.md) |
|
|
464
|
-
| `:generation_started` | a request begins | `{iteration, context_window_tokens, context_window_source, price}` (`price`: the turn's `ModelPrice#to_h`, nil without one; native adds `profile`, `profile_source`) |
|
|
470
|
+
| `:context_status` | every request (unless `context.status` is off), the current estimate before it; and outside any turn after `/model` (or when a session is loaded on another model than its last turn ran on), to the observers only (`Engine#announce_switched_context!`) | `{iteration, status, usage, bucket, source}`; the switch's form has no `iteration`: `{usage, bucket, context_window_tokens, context_window_source}`, with `usage.source: "switch"`, and `usage` nil when the new window doesn't resolve (internals/context-telemetry.md) |
|
|
471
|
+
| `:generation_started` | a request begins | `{iteration, context_window_tokens, context_window_source, price, model_ref}` (`price`: the turn's `ModelPrice#to_h`, nil without one; `model_ref`: the turn's model, asked `host:model`, added by the Engine's stream handler from the model `#generate` resolved after the `before_turn` hooks, so a hook's switch is the one named; native adds `profile`, `profile_source`) |
|
|
465
472
|
| `:generation_chunk` | each streamed chunk | `{iteration, content, text, thinking, payload, tool_call?}` |
|
|
466
473
|
| `:generation_retrying` | the transport retries the request, or the chat loop asks again for a step whose stream dropped mid-answer (`restarted: true`: what the step streamed is void, and the web's live step, the TUI's activity line, the Bridge's TurnAccumulator and the stream hooks' watch start it over) | `{iteration, attempt, max_retries, next_delay, error_class, error_message, status, restarted?}` |
|
|
467
|
-
| `:generation_completed` | a generation ended, or was cut | `{iteration, content_length, thinking_chars, served_model, requested_model, finish_reason, ...}`; a cut adds `stopped_by`, `stop_reason`; the Engine adds `speed`, `tokens` |
|
|
474
|
+
| `:generation_completed` | a generation ended, or was cut | `{iteration, content_length, thinking_chars, served_model, requested_model, finish_reason, ...}`; a cut adds `stopped_by`, `stop_reason`; the Engine adds `speed`, `tokens`, `model_segments` |
|
|
468
475
|
| `:generation_cancelled` | the turn was cancelled mid-loop | `{iteration, reason, stopped_by}` |
|
|
469
476
|
| `:empty_answer_retry` | an empty, cut or malformed answer is asked again | `{iteration, attempt, of, finish_reason, thinking_chars, stopped_by?, malformed?}` |
|
|
470
477
|
| `:steer_cut` | a steer cut the generation | `{iteration, source}` |
|
|
@@ -526,6 +533,10 @@ Model switching, Engine#switch_model!(model_name, persist_default:, typed:):
|
|
|
526
533
|
thread finishes for the old model is neither kept nor applied: #store_profile)
|
|
527
534
|
→ sync_model_key!, @prompt_builder.reset! (the system prompt is built again)
|
|
528
535
|
→ sync_kernel_client! (the kernel talks to the new host), the context window cache dropped
|
|
536
|
+
→ the old model's ctx dropped: @metrics.forget_model_reports! (its window and profile reports) and
|
|
537
|
+
@last_context_status = nil; /model (SessionCommands#switch_model) then calls #announce_switched_context!
|
|
538
|
+
(the ctx % on the new model's window, before its first turn; Engine#session= does the configured-window-only
|
|
539
|
+
variant for a session loaded on another model; internals/context-telemetry.md)
|
|
529
540
|
→ the kernel is never rebuilt; the next turn's backend follows the new host's api
|
|
530
541
|
|
|
531
542
|
Model probes (per turn):
|
|
@@ -149,7 +149,8 @@ is prefilled once. The native and the chat loop use the same plan.
|
|
|
149
149
|
|
|
150
150
|
- The debug log's `generation_completed` line: `prompt=` (the request's prompt tokens), `cached=` (read from the
|
|
151
151
|
server's cache), `cache_write=` (written to it, where the provider reports writes) and `reprefill=` (of the last
|
|
152
|
-
request's prompt on the same model, what was prefilled again
|
|
152
|
+
request's prompt on the same model through the same host, what was prefilled again; a `/model` to the same bare
|
|
153
|
+
model on another host is the switch's prefill, not a re-prefill). The `http` line of a
|
|
153
154
|
remote Claude or Messages request shows `cache=on` (or `cache=1h`) when it carried breakpoints.
|
|
154
155
|
- The warm-up logs `model warmup` with `cached=`, `prefilled=` and `prefill_ms=`.
|
|
155
156
|
- `/stats`: "cached N (P%)", "cache writes" and "re-prefilled" for the session.
|
data/docs/memory.md
CHANGED
|
@@ -220,6 +220,40 @@ for a file another bundle installed.
|
|
|
220
220
|
`README.markdown` (or puts docs in a subdirectory).
|
|
221
221
|
`chi bundle upgrade` moves a README an older install made a memory to the
|
|
222
222
|
trash (unless you edited it, like any file a bundle stops shipping).
|
|
223
|
+
- **A symlink is never installed.** A memory that is a symlink is skipped
|
|
224
|
+
with `notes.md is a symlink (not installed)`, and so is a hook or guardrail
|
|
225
|
+
rule that is one or lives in a linked dir (`hooks/x.rb is a symlink (not
|
|
226
|
+
installed)`); a plugin or script that is one fails the install. A tar, zip
|
|
227
|
+
or git repo keeps links, and copying through one would install whatever
|
|
228
|
+
file it points at (`~/.ssh/…`, an API key) as a memory the model reads.
|
|
229
|
+
- **A bundle is its name and its source.** A bundle's name is its
|
|
230
|
+
manifest's `name:`, for every kind of source (a git URL too), and a
|
|
231
|
+
manifest can take any name, so the source decides who installs under it:
|
|
232
|
+
- `chi bundle install` of a source whose manifest names a bundle
|
|
233
|
+
installed from another source is refused, `--force` too (`btw is
|
|
234
|
+
installed from <source>; chi bundle upgrade <new source> switches its
|
|
235
|
+
source, or chi bundle uninstall btw first`);
|
|
236
|
+
- `chi bundle upgrade` may switch the source (a git URL and a local copy
|
|
237
|
+
of it are one bundle; an archive's file name carries its version), and
|
|
238
|
+
its output starts with `Source changes: <old> → <new>` (`--dry-run`
|
|
239
|
+
too);
|
|
240
|
+
- a name chi ships (`guardrails`, `btw`, `samagotchi-system`, …, in any
|
|
241
|
+
case) installs or upgrades only from chi's own copy (a dir in the
|
|
242
|
+
running chi's shipped bundles, not a fork's `lib/samagotchi/bundles/`),
|
|
243
|
+
unless `--force` (`guardrails is a bundle shipped with chi; <source>
|
|
244
|
+
isn't chi's copy of it`);
|
|
245
|
+
- a name that differs from an installed bundle's only in case is refused,
|
|
246
|
+
`--force` too (`BTW differs only in case from the installed btw`): on a
|
|
247
|
+
case-insensitive file system (macOS) it is that bundle's dir.
|
|
248
|
+
|
|
249
|
+
Sources compare as a git URL without its `#ref` (or a trailing `/` or
|
|
250
|
+
`.git`), a shipped bundle by its dir in any chi version, and a path with
|
|
251
|
+
symlinks resolved. A git URL bundle chi 0.75.0 or earlier installed under
|
|
252
|
+
the repo's name (`chi-foo` for `name: foo`) isn't installed again beside
|
|
253
|
+
it: `chi bundle install|upgrade <url>` says `<url> is installed as
|
|
254
|
+
chi-foo: chi bundle uninstall chi-foo, then install it again`. That match
|
|
255
|
+
is by the same source, so a record of `git@github.com:x/foo` isn't found
|
|
256
|
+
from `https://github.com/x/foo` (uninstall it by name).
|
|
223
257
|
- **The index says where a memory came from.** The `index.md` line of a file
|
|
224
258
|
a bundle installed ends in `· from <bundle>`, before the description:
|
|
225
259
|
`- **memory_guide** · system · 2026-10-03 · 5120 · from samagotchi-system —
|
data/docs/plugins.md
CHANGED
|
@@ -20,7 +20,7 @@ my-bundle/
|
|
|
20
20
|
|
|
21
21
|
```yaml
|
|
22
22
|
# manifest.yml
|
|
23
|
-
name: my-bundle
|
|
23
|
+
name: my-bundle # the bundle's name for every source (a git URL too): a plain name, no / (not . or ..)
|
|
24
24
|
version: 1.0.0
|
|
25
25
|
plugin:
|
|
26
26
|
file: plugin.rb
|
|
@@ -990,6 +990,48 @@ context_providers:
|
|
|
990
990
|
the PATH may be another one, such as macOS's 2.6).
|
|
991
991
|
- The command follows [the contract](context.md#the-command-contract).
|
|
992
992
|
|
|
993
|
+
## Stable for bundles
|
|
994
|
+
|
|
995
|
+
Bundles normally use only `chi.*` and `ctx`; the constants and helpers below
|
|
996
|
+
are the rest of the surface they may use. A change to any of them gets a
|
|
997
|
+
CHANGELOG line and bumps the shipped bundles that use it. Anything else under
|
|
998
|
+
`Samagotchi::` is internal.
|
|
999
|
+
|
|
1000
|
+
**Stable**
|
|
1001
|
+
|
|
1002
|
+
1. `Samagotchi::Plugin::Api` / `chi.*` as documented above, including
|
|
1003
|
+
`Samagotchi::Plugin::ToolResult`, `Samagotchi::Plugin::ModelError` (and its
|
|
1004
|
+
subclass `Samagotchi::Plugin::ModelCancelled`),
|
|
1005
|
+
`Samagotchi::Plugin::Service::Stopped`, `Samagotchi::Plugin::Sessions::Error`
|
|
1006
|
+
and `Samagotchi::Plugin::AttachedContext::Error`.
|
|
1007
|
+
2. `Samagotchi::Plugin::Peer` as a base class. A subclass defines `post`,
|
|
1008
|
+
`check_hint!`, `paired_lines`, `transport_line`, `peer_dir` and
|
|
1009
|
+
`setup_paragraph` (optionally `skill_substitutions`), and sets `NAME`,
|
|
1010
|
+
`APP`, `COMMAND`, `USAGE`, `OUTBOX` and `SCRIPTS_DIRS`. These constants are
|
|
1011
|
+
read as `self.class::X`. See the [claude-peer](#the-claude-peer-bundle) and
|
|
1012
|
+
[codex-peer](#the-codex-peer-bundle) bundles.
|
|
1013
|
+
3. `Samagotchi::Tools::ToolIndex::Tool` and `Samagotchi::Tools::ToolIndex::Group`
|
|
1014
|
+
values, `Samagotchi::Tools::ToolIndex.schema_note` and
|
|
1015
|
+
`Samagotchi::Tools::ToolIndex.spec_tokens` (used by the mcp bundle).
|
|
1016
|
+
4. `Samagotchi::Tools::Execute::NAME`, `TIMEOUT_PREFIX`, `HANDED_OVER` and
|
|
1017
|
+
`Samagotchi::Tools::Execute.timeout_seconds`.
|
|
1018
|
+
5. `Samagotchi::Tools::TaskRuntime::TASKS_DIR` and `EXIT_CODE_FILENAME`.
|
|
1019
|
+
6. `Samagotchi::AtomicFile.write`, `Samagotchi::Log.warn` /
|
|
1020
|
+
`Samagotchi::Log.debug`, `Samagotchi::Config.get` and
|
|
1021
|
+
`Samagotchi::VERSION`.
|
|
1022
|
+
|
|
1023
|
+
**Used by shipped bundles today, but internal** (may change without notice; a
|
|
1024
|
+
bundle that needs one should ask for an API instead):
|
|
1025
|
+
|
|
1026
|
+
- `Samagotchi::Tools::MemoryRead.memories_dir`, `Samagotchi::MemoryPaths.scope_dir`,
|
|
1027
|
+
`Samagotchi::ConfigFile.global_path` and
|
|
1028
|
+
`Samagotchi::Config.validate_yaml_sections`.
|
|
1029
|
+
- `Samagotchi::Ref` / `Samagotchi::RefAction` constants and
|
|
1030
|
+
`Samagotchi::TokenUsage::CHARS_PER_TOKEN`.
|
|
1031
|
+
- `Samagotchi::ProcessGroup.stop`, `Samagotchi::Guardrails::ShellLex` and
|
|
1032
|
+
`Samagotchi::Guardrails::GitInfo`.
|
|
1033
|
+
- `Samagotchi::Tools::Execute.const_get` reflection.
|
|
1034
|
+
|
|
993
1035
|
## The btw bundle
|
|
994
1036
|
|
|
995
1037
|
`chi bundle install btw` (or the `dev` profile) installs the bundle shipped with chi. It is written
|
data/docs/releasing.md
CHANGED
|
@@ -13,7 +13,7 @@ can run the whole release; the user approves the notes before the tag and the
|
|
|
13
13
|
and `rake release:check` enforce it). The system bundle upgrades itself when
|
|
14
14
|
chi starts.
|
|
15
15
|
- **Every other shipped bundle** (btw, check-in, guardrails, known-names,
|
|
16
|
-
loop-guard, mcp, skills, source-links, github-pr, coordinator, claude-peer, codex-peer, and the profiles core and dev) has its own semver in its
|
|
16
|
+
loop-guard, mcp, skills, slow-commands, agents-md, source-links, github-pr, coordinator, claude-peer, codex-peer, and the profiles core and dev) has its own semver in its
|
|
17
17
|
`manifest.yml` and, when it needs a newer chi, a `requires_chi:` line. They never upgrade by themselves: users run
|
|
18
18
|
`chi update` (all of them, with the gem) or `chi bundle upgrade NAME`. So:
|
|
19
19
|
- a change to a bundle's files bumps that bundle's `version:`
|
|
@@ -30,6 +30,10 @@ can run the whole release; the user approves the notes before the tag and the
|
|
|
30
30
|
spec checks every bundle is in exactly one), and that profile's version
|
|
31
31
|
goes up: `chi update` then installs only the new bundle for those who
|
|
32
32
|
have the profile;
|
|
33
|
+
- a profile's `requires_chi` is its members' highest: a member's raised
|
|
34
|
+
floor (by hand, or by `release:bump` for a `requires_chi_next` marker)
|
|
35
|
+
raises its profile's too, with the profile's version (`bundles:check`
|
|
36
|
+
notes a profile below a member);
|
|
33
37
|
- the release notes say "run `chi update`" and name the bundles whose
|
|
34
38
|
version moved (what changed in each), not per-bundle upgrade steps.
|
|
35
39
|
- Pre-1.0: config and commands may change in a minor version (0.2 → 0.3); a
|
|
@@ -55,7 +59,7 @@ as the GitHub release body.
|
|
|
55
59
|
| Task | What it does |
|
|
56
60
|
| --- | --- |
|
|
57
61
|
| `rake bundles:sha` | Recomputes the sha256 lines of every shipped bundle manifest (`files:`, `hooks:`, `plugin:`). |
|
|
58
|
-
| `rake bundles:check` | Sha lines match; every top-level `*.md` of a bundle is listed in its `files:` (an install takes only the listed ones); a bundle changed since the last `v*` tag has a higher version (skipped, with a note, before the first tag). Warns (doesn't fail) when such a bundle's `requires_chi` still admits the tagged chi: mark it `requires_chi_next` if the change uses anything new. Notes a bundle marked `requires_chi_next
|
|
62
|
+
| `rake bundles:check` | Sha lines match; every top-level `*.md` of a bundle is listed in its `files:` (an install takes only the listed ones); a bundle changed since the last `v*` tag has a higher version (skipped, with a note, before the first tag). Warns (doesn't fail) when such a bundle's `requires_chi` still admits the tagged chi: mark it `requires_chi_next` if the change uses anything new. Notes a bundle marked `requires_chi_next`, and a profile (`core`, `dev`) whose `requires_chi` is below a member's. The release workflow and `release:check` run it in full. |
|
|
59
63
|
| `rake 'bundles:check[ci]'` | The CI workflow's (every push and PR): the same, except the version check leaves out the system bundle, which is changed and unbumped between releases (`release:bump` moves its version with VERSION). A task argument, not `CI=1`: GitHub sets `CI` in the release workflow too. |
|
|
60
64
|
| `rake release:draft_changelog` | A draft Unreleased section from the commit subjects since the last tag, grouped. Stdout only. |
|
|
61
65
|
| `rake release:bump[X.Y.Z]` | VERSION, the system manifest, Gemfile.lock, CHANGELOG, and `requires_chi: ">= X.Y.Z"` for each bundle marked `requires_chi_next` (the marker dropped). No commit. |
|
data/docs/sessions.md
CHANGED
|
@@ -51,6 +51,8 @@ chi sessions stats ID [--format text|json] # cost, tokens and progres
|
|
|
51
51
|
|
|
52
52
|
`chi sessions stats ID` prints what `/stats` prints in a terminal, headed by `<id8> <status> <model>`: a live worker answers its Bridge's `GET stats` (no model call, and the command never starts a worker); without one, or when the worker does not answer in time, the same snapshot is rebuilt from the session's saved `analytics.json` (and the line says so when a worker was live). `--format json` prints one object `{session_id, status, live, metrics}` with the snapshot as `metrics` (its `turn_records` / `tool_records` are the recent ones only: a live worker's newest finished turn and any not yet saved, the disk snapshot's newest saved turn; `analytics.json` holds them all); a session with no `analytics.json` yet reports `metrics: null` (`(no metrics yet)` in text) and still exits 0. An unknown or ambiguous id exits 1 with the same wording `chi sessions stop` uses.
|
|
53
53
|
|
|
54
|
+
Each turn record in `analytics.json` names the model the turn ran on as `model_ref` (the asked `host:model`, from its first generation; `model` stays the name the server says it served); a turn stopped before it asked a model has none. The first turn after a `/model` switch (or a hook's) to another model also records what its first generation prefilled on the new model: `switch_prefill_tokens` (the prompt less the cached reads; none under 128 tokens, or for a generation with estimated counts) and `switch_prefill_cost_estimate` (from the new model's configured price, no completion; none without a price). A session saved before this has no `model_ref` on its records.
|
|
55
|
+
|
|
54
56
|
A session whose worker waits for an answer (a question, a hook's question, a guardrail approval, or the step-limit question of a turn that ran out of steps, which waits between turns) reads `waiting` in the status column, and json has `waiting: "question"|"approval"|"hook"|"continue"` and `waiting_id`, the question's id for `chi answer --question` (both null otherwise). A delegate's approval its parent relayed to its own card (see *Delegating*) reads `waiting (in parent ab12)`, json `relayed_to: "ab12…"` (the parent's short id, else null). Only while a worker runs it: a question a dead worker left in the file shows the saved status. Answer it with `chi answer`, the web or `chi --attach ID`; `chi sessions stop ID` frees the worker instead.
|
|
55
57
|
|
|
56
58
|
Examples:
|
|
@@ -74,7 +76,7 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
|
|
|
74
76
|
**Ordering:**
|
|
75
77
|
|
|
76
78
|
- `Session.list` / `SessionManager.list_sessions` / `GET /api/sessions?sort=&order=&limit=&offset=` default to `updated_at desc` (newest activity first). Also supports `created_at`, `asc`. `X-Total-Count` header when paginated.
|
|
77
|
-
- Web UI (`chi web`): the page's scope is in its URL. Started in a git repo, `chi web` opens `/?dir=<that folder>`: that project's sessions, and new chats start in that folder; the header chip says `<project> · all`, and `all` opens the same place without `?dir` (every session; a new chat there starts in the server's own folder, shown on the start page, and cards name their folder; `← <project>` goes back to the project view it came from, or to the server's own project). One server serves every project: a second `chi web` (from another repo) finds it through `GET /api/info` and prints (with `--open`, opens) its page for its own folder instead of starting another. The 3 latest sessions sit above the chat (`hide ▴` puts them away behind a `▾ N sessions` pill, remembered; in a window under 900 px wide, where the strip scrolls sideways, `▴ hide` sits in the top bar where the pill comes back; a window under 700 px tall hides them by itself, so a question or approval card in the stage keeps room, and the pill shows them until the window is tall again). A parent and its delegates are one card there: its `▸ 2 delegates · 1 live · 1 waiting` chip (the waiting part in the attention colour, the card's border too) opens their rows in a popover under it (a row per delegate at any depth, indented; a click opens that one, Esc or a click outside closes it, and the strip's order holds while it is open). A fork is a card of its own, and so is a delegate whose parent isn't listed (archived, another project), with its `↳ <parent>` chip; the pill counts cards, and every live or waiting session. "All sessions" (or `/`) opens every session at `#/sessions`, with a search over preview, id and status (Esc or Back returns). There a parent's card holds its delegates the same way, the chip opening their rows inside the card (each with an archive button on hover), open by itself while one of them waits on you; the count says `14 (+7 delegates)`, a search that matches a delegate shows its parent's card with the list open and the matching rows marked, and select mode picks cards (archiving a parent takes its delegates). The open session is in the URL (`#/s/<id>`), so a reload or a copied link opens it again; the chi logo top left goes back to the empty start for a new chat. The message box grows with its text; drag its top edge to keep it taller (double-click resets). A file in a tool row (a read, edit or write's title) and its diff lines are refs: hover one and click to open the current file in your editor at that line, on this machine only (`web.editor`; the browser's "always allow" for the page's port and VS Code's "allow opening local files" are asked once; see [Opening files from chi web](configuration.md#opening-files-from-chi-web)). So are an edit or write approval card's paths (at the first line the dry run changes) and its diff lines, before you approve, and the numbered lines of a read's output: `lines 40–60` under it opens the text as read, numbered, when the output is the file as-is (not the head/tail preview of a big file, an error or an image; no numbers after a cut). A `path:line` (or `path:line-line`) in an answer or a step's text is one too once the turn has ended, and after a reload, when it names a file a row or card on the page touched: the path under the session's folder, else the one touched file it ends with (a bare `app.js:12` when only one touched file is named so; two such files make it none); a path without a line counts only when it is a touched file exactly. Code blocks, links, bare URLs and inline code with more than the ref in it are left alone, and a file the session didn't touch is never linked; a link whose text is such a ref alone (github-pr's link to the PR diff) becomes a ref too, and its plain click still goes to the PR. A plugin's refs on an answer (an `after_turn` hook's, see [Refs on the answer](hooks.md#refs-on-the-answer-display-only)) are paired with its links and text whatever the rows touched: they can link a file the session didn't touch, or an issue or a commit, and add their own actions to the ref's menu. A plugin tool's row carries the refs its `refs:` gives (see [Refs on a tool's row](plugins.md#refs-on-a-tools-row)): its title opens the first, and its `▾` menu adds the others as `<ref> · <action>` (the URL on hover); their URL actions work on a phone too. A ref with more than one target shows a small `▾` on hover: it opens a menu of them, in order **PR diff** (a link's), **Open in editor**, **Show in turn** and **Copy path**, and a plain click runs the first (a link's own, else the editor, else show in turn). **Show in turn** scrolls to the tool row that touched the file (looked for in the ref's own step, then its own turn, then the rows before it, then the nearest after it; there, the last one showing the ref's line, else the last one), opens the steps, the stage's steps and the row's diff (or the read showing the line) and flashes the row and the line; an answer's or a step's ref and an approval card's path have it, not a row's own title or lines. It needs no editor, so a viewer without one (a phone on the LAN) gets an answer's refs with **Show in turn** as their click; a row's title and diff lines stay plain text there. On a touch screen a tap opens the menu. A ref with a target is a tab stop: Enter runs its first target, ↓ opens the menu (↑/↓ move, Enter picks, Esc closes back to the ref). Diff and read lines have their click only. The info bar copies `chi --attach <id>` for a terminal. A session whose prompt carried model notes has a `notes: deepseek` chip there (the names without `model_notes_`; the tooltip lists each with its scope and size; `session.prompt_notes` in `GET /api/sessions/:id`, the live worker's, else the session file's; see [Memory](memory.md#model-notes)). Its `llm ctx` chip names the LLM context strategy the next turn runs under (`llm ctx stale,forget · session`, in the accent colour when the session set its own; the tooltip gives each value's source), and its popover sets the session's own strategy, apply rule and budget, or follows the model again, by running `/llm-context` in the worker (the reply shows as a command bubble); `GET /api/sessions/:id`'s `session.llm_context` carries the values (the live worker's, else resolved from the session file), and a `command_ran` that changed them carries the new ones. Its `think` chip does the same for the thinking level (`/thinking`), and on a chat host its `reasoning` chip for the reasoning history (`reasoning all · session`, `/reasoning-history`; `session.reasoning_history`, null on a native host). A `/thinking` or `/reasoning-history` change that restarts the prompt cache asks first: the reply bubble has an **Apply (restarts the cache)** button that sends its `--yes` line (the chips' Set sends that line itself; see [Changes that restart the prompt cache](configuration.md#changes-that-restart-the-prompt-cache)). It shows the context used (`ctx 42%`) and the last generation's decode speed (`87 tok/s`, `~64 tok/s` when estimated; see `/stats` in [cli.md](cli.md#session-commands)); hovering `ctx` there or on a session card lists the session's tokens in (summed over every request, with the cached share), out (with reasoning) and the cost when the provider reports one, for this session only (one estimated from a `hosts.<name>.models` price shows with `~`: `cost: ~$0.54 ($0.42 reported, ~$0.12 estimated)`), and the memory indexes its prompt holds (`memory index: ~3.4k tokens in this session's prompt (system 2.0k, project 1.4k)`; see [Memory](memory.md#the-indexs-size)). The start page's model picker (bottom left of the composer, the chosen name `▾`) chooses the model a new chat starts on, and a fresh chain link's (see [Session chains](#session-chains); there a pick shows as a `/model` command bubble in the link); it opens a search list over the conversation (↓ or a click; downward when the start page leaves no room above): an empty search shows the last 5 picks (Recent) and then each host's model ids A–Z under the host, the default host first (an id the host declares under `hosts.<name>.models` first, noted `config`, its tooltip `served by config (hosts.<name>.models)`; a host that failed to list its models keeps those ids, greyed and noted `host down`, still pickable; a remembered or default model stays chosen even then, greyed on the button); typed words must each match the host or the id (a substring, else for 3+ characters the letters in order, so `deepseek4.1 fla` finds `openrouter · deepseek/deepseek-v4.1-flash`), ⏎ picks, Esc/Tab close, a pick puts the focus in the message box: `GET /api/models` lists the hosts' models as chi spells them (`{default, models: [{name, host, id, configured?, unavailable?, sampling?, llm_context?}], warning?}`; `configured: true` marks an id declared under `hosts.<name>.models`, listed first under its host, `unavailable: true` one whose host failed to list its models; `sampling` is the model's configured request parameters as `/model` words them, the row's tooltip in the list; bare for the default host, `host:model` for the others, from the same cached lists as `/models`, a bounded wait, a host that is down noted in `warning`), and `POST /api/sessions` takes `model` (blank means the default). Beside it, an `llm ctx` chip shows the LLM context the picked model starts under (each `GET /api/models` row's `llm_context`, as the info bar's); its form sets one for the new chat only, sent as `POST /api/sessions`'s `llm_context` and never remembered (see [configuration.md](configuration.md#llm-context-a-sessions-own-strategy)). The start page's first message goes as later ones do: `POST /api/sessions` with `idle: true` and `preview` (the message, which names the session until its turn is saved), then `POST /api/sessions/:id/turn`, so a failed first turn puts its prompt back in the composer too; a first line that is one of the new session's commands (`/model …`) goes to `POST /api/sessions/:id/command` instead, as later ones do. The browser remembers the last model picked; a running session's model is in the info bar and changes only with `/model`. In the composer a line is a command when it is `!…` or starts with one of the session's command names (its `commands` list; `/stats`, `/exit` and the other terminal words get the page's own reply); an unknown `/word` (`/usr/bin/env is missing`) goes to the model as a prompt, as in a terminal; a one-word `/word` no command answers (`/modle`) is not sent: the page shows `Unknown command /modle. Did you mean /model? /help lists the commands.` (the "Did you mean" part only when a name is close). A `POST /turn` (or a session's first `prompt`) that is one of the worker's commands runs as that command too (`202 {status, command_id, session_id}`), so `chi send`, `chi -p` and a page whose command list lags all get the command. ↑/↓ in the composer walk the prompt history the TUI's ↑ walks ([Persistent Prompt History](cli.md#persistent-prompt-history)): ↑ with the caret on the first line, ↓ from the last line while browsing, ↓ past the newest brings the draft back; `GET /api/history` returns `{entries: [...]}`, oldest first (the page fetches it on focus and after each send). The server records a sent line in the routes once the worker took it (`POST /turn` accepted, a `!command` through `POST /command` or as a session's first line); `"history": false` in the body keeps it out (an image-only send).
|
|
79
|
+
- Web UI (`chi web`): the page's scope is in its URL. Started in a git repo, `chi web` opens `/?dir=<that folder>`: that project's sessions, and new chats start in that folder; the header chip says `<project> · all`, and `all` opens the same place without `?dir` (every session; a new chat there starts in the server's own folder, shown on the start page, and cards name their folder; `← <project>` goes back to the project view it came from, or to the server's own project). One server serves every project: a second `chi web` (from another repo) finds it through `GET /api/info` and prints (with `--open`, opens) its page for its own folder instead of starting another. The 3 latest sessions sit above the chat (`hide ▴` puts them away behind a `▾ N sessions` pill, remembered; in a window under 900 px wide, where the strip scrolls sideways, `▴ hide` sits in the top bar where the pill comes back; a window under 700 px tall hides them by itself, so a question or approval card in the stage keeps room, and the pill shows them until the window is tall again). A parent and its delegates are one card there: its `▸ 2 delegates · 1 live · 1 waiting` chip (the waiting part in the attention colour, the card's border too) opens their rows in a popover under it (a row per delegate at any depth, indented; a click opens that one, Esc or a click outside closes it, and the strip's order holds while it is open). A fork is a card of its own, and so is a delegate whose parent isn't listed (archived, another project), with its `↳ <parent>` chip; the pill counts cards, and every live or waiting session. "All sessions" (or `/`) opens every session at `#/sessions`, with a search over preview, id and status (Esc or Back returns). There a parent's card holds its delegates the same way, the chip opening their rows inside the card (each with an archive button on hover), open by itself while one of them waits on you; the count says `14 (+7 delegates)`, a search that matches a delegate shows its parent's card with the list open and the matching rows marked, and select mode picks cards (archiving a parent takes its delegates). The open session is in the URL (`#/s/<id>`), so a reload or a copied link opens it again; the chi logo top left goes back to the empty start for a new chat. The message box grows with its text; drag its top edge to keep it taller (double-click resets). A file in a tool row (a read, edit or write's title) and its diff lines are refs: hover one and click to open the current file in your editor at that line, on this machine only (`web.editor`; the browser's "always allow" for the page's port and VS Code's "allow opening local files" are asked once; see [Opening files from chi web](configuration.md#opening-files-from-chi-web)). So are an edit or write approval card's paths (at the first line the dry run changes) and its diff lines, before you approve, and the numbered lines of a read's output: `lines 40–60` under it opens the text as read, numbered, when the output is the file as-is (not the head/tail preview of a big file, an error or an image; no numbers after a cut). A `path:line` (or `path:line-line`) in an answer or a step's text is one too once the turn has ended, and after a reload, when it names a file a row or card on the page touched: the path under the session's folder, else the one touched file it ends with (a bare `app.js:12` when only one touched file is named so; two such files make it none); a path without a line counts only when it is a touched file exactly. Code blocks, links, bare URLs and inline code with more than the ref in it are left alone, and a file the session didn't touch is never linked; a link whose text is such a ref alone (github-pr's link to the PR diff) becomes a ref too, and its plain click still goes to the PR. A plugin's refs on an answer (an `after_turn` hook's, see [Refs on the answer](hooks.md#refs-on-the-answer-display-only)) are paired with its links and text whatever the rows touched: they can link a file the session didn't touch, or an issue or a commit, and add their own actions to the ref's menu. A plugin tool's row carries the refs its `refs:` gives (see [Refs on a tool's row](plugins.md#refs-on-a-tools-row)): its title opens the first, and its `▾` menu adds the others as `<ref> · <action>` (the URL on hover); their URL actions work on a phone too. A ref with more than one target shows a small `▾` on hover: it opens a menu of them, in order **PR diff** (a link's), **Open in editor**, **Show in turn** and **Copy path**, and a plain click runs the first (a link's own, else the editor, else show in turn). **Show in turn** scrolls to the tool row that touched the file (looked for in the ref's own step, then its own turn, then the rows before it, then the nearest after it; there, the last one showing the ref's line, else the last one), opens the steps, the stage's steps and the row's diff (or the read showing the line) and flashes the row and the line; an answer's or a step's ref and an approval card's path have it, not a row's own title or lines. It needs no editor, so a viewer without one (a phone on the LAN) gets an answer's refs with **Show in turn** as their click; a row's title and diff lines stay plain text there. On a touch screen a tap opens the menu. A ref with a target is a tab stop: Enter runs its first target, ↓ opens the menu (↑/↓ move, Enter picks, Esc closes back to the ref). Diff and read lines have their click only. The info bar copies `chi --attach <id>` for a terminal. A session whose prompt carried model notes has a `notes: deepseek` chip there (the names without `model_notes_`; the tooltip lists each with its scope and size; `session.prompt_notes` in `GET /api/sessions/:id`, the live worker's, else the session file's; see [Memory](memory.md#model-notes)). Its `llm ctx` chip names the LLM context strategy the next turn runs under (`llm ctx stale,forget · session`, in the accent colour when the session set its own; the tooltip gives each value's source), and its popover sets the session's own strategy, apply rule and budget, or follows the model again, by running `/llm-context` in the worker (the reply shows as a command bubble); `GET /api/sessions/:id`'s `session.llm_context` carries the values (the live worker's, else resolved from the session file), and a `command_ran` that changed them carries the new ones. Its `think` chip does the same for the thinking level (`/thinking`), and on a chat host its `reasoning` chip for the reasoning history (`reasoning all · session`, `/reasoning-history`; `session.reasoning_history`, null on a native host). A `/thinking` or `/reasoning-history` change that restarts the prompt cache asks first: the reply bubble has an **Apply (restarts the cache)** button that sends its `--yes` line (the chips' Set sends that line itself; see [Changes that restart the prompt cache](configuration.md#changes-that-restart-the-prompt-cache)). It shows the context used (`ctx 42%`) and the last generation's decode speed (`87 tok/s`, `~64 tok/s` when estimated; see `/stats` in [cli.md](cli.md#session-commands)); hovering `ctx` there or on a session card lists the session's tokens in (summed over every request, with the cached share), out (with reasoning) and the cost when the provider reports one, for this session only (one estimated from a `hosts.<name>.models` price shows with `~`: `cost: ~$0.54 ($0.42 reported, ~$0.12 estimated)`); after a `/model` switch the info bar's tooltip adds a `per model:` row per run of turns on one model above those totals (`splash:qwen3-35b · 6 turns · 96.0k in / 9.0k out · ~$0.02`, a switch's prefill under its row: `└ switch prefill 58.0k uncached prompt ≈$0.01`; the timing's `model_segments`, replaced by each `generation_completed`'s; the session cards stay as they were), and right after the switch its ctx % is the new model's window's; and the memory indexes its prompt holds (`memory index: ~3.4k tokens in this session's prompt (system 2.0k, project 1.4k)`; see [Memory](memory.md#the-indexs-size)). The start page's model picker (bottom left of the composer, the chosen name `▾`) chooses the model a new chat starts on, and a fresh chain link's (see [Session chains](#session-chains); there a pick shows as a `/model` command bubble in the link); it opens a search list over the conversation (↓ or a click; downward when the start page leaves no room above): an empty search shows the last 5 picks (Recent) and then each host's model ids A–Z under the host, the default host first (an id the host declares under `hosts.<name>.models` first, noted `config`, its tooltip `served by config (hosts.<name>.models)`; a host that failed to list its models keeps those ids, greyed and noted `host down`, still pickable; a remembered or default model stays chosen even then, greyed on the button); typed words must each match the host or the id (a substring, else for 3+ characters the letters in order, so `deepseek4.1 fla` finds `openrouter · deepseek/deepseek-v4.1-flash`), ⏎ picks, Esc/Tab close, a pick puts the focus in the message box: `GET /api/models` lists the hosts' models as chi spells them (`{default, models: [{name, host, id, configured?, unavailable?, sampling?, llm_context?}], warning?}`; `configured: true` marks an id declared under `hosts.<name>.models`, listed first under its host, `unavailable: true` one whose host failed to list its models; `sampling` is the model's configured request parameters as `/model` words them, the row's tooltip in the list; bare for the default host, `host:model` for the others, from the same cached lists as `/models`, a bounded wait, a host that is down noted in `warning`), and `POST /api/sessions` takes `model` (blank means the default). Beside it, an `llm ctx` chip shows the LLM context the picked model starts under (each `GET /api/models` row's `llm_context`, as the info bar's); its form sets one for the new chat only, sent as `POST /api/sessions`'s `llm_context` and never remembered (see [configuration.md](configuration.md#llm-context-a-sessions-own-strategy)). The start page's first message goes as later ones do: `POST /api/sessions` with `idle: true` and `preview` (the message, which names the session until its turn is saved), then `POST /api/sessions/:id/turn`, so a failed first turn puts its prompt back in the composer too; a first line that is one of the new session's commands (`/model …`) goes to `POST /api/sessions/:id/command` instead, as later ones do. The browser remembers the last model picked; a running session's model is in the info bar and changes only with `/model`. In the composer a line is a command when it is `!…` or starts with one of the session's command names (its `commands` list; `/stats`, `/exit` and the other terminal words get the page's own reply); an unknown `/word` (`/usr/bin/env is missing`) goes to the model as a prompt, as in a terminal; a one-word `/word` no command answers (`/modle`) is not sent: the page shows `Unknown command /modle. Did you mean /model? /help lists the commands.` (the "Did you mean" part only when a name is close). A `POST /turn` (or a session's first `prompt`) that is one of the worker's commands runs as that command too (`202 {status, command_id, session_id}`), so `chi send`, `chi -p` and a page whose command list lags all get the command. ↑/↓ in the composer walk the prompt history the TUI's ↑ walks ([Persistent Prompt History](cli.md#persistent-prompt-history)): ↑ with the caret on the first line, ↓ from the last line while browsing, ↓ past the newest brings the draft back; `GET /api/history` returns `{entries: [...]}`, oldest first (the page fetches it on focus and after each send). The server records a sent line in the routes once the worker took it (`POST /turn` accepted, a `!command` through `POST /command` or as a session's first line); `"history": false` in the body keeps it out (an image-only send).
|
|
78
80
|
- Web notifications: when a session needs you while the tab is hidden or behind another window (a question or a guardrail approval, a plugin card with buttons during a turn such as check-in's, a failed turn, a turn done after 10 s or more; a delegated session only for its questions, a canceled turn never), the page title counts it, `(N) Chi`, until you come back to the tab. The bell at the right of the top bar also turns on OS notifications ("needs an answer", "needs approval", "needs you", "turn failed", "done in 42 s", under the session's first message; a click opens the session): its first click asks the browser for the permission, and the choice is kept per browser. Any session of the page's scope counts, not only the open one. What was already so when the page loaded never counts, and several chi tabs show one notification per event. A chi tab in front tells the others (a `BroadcastChannel`), so while you look at one, the ones behind neither notify nor count what it shows; coming back to a tab clears those events in the other tabs' titles too. The hub's `session` frames carry what this needs: `pending_question` (`{id, kind}`, kind `question` or `approval`), `pending_card` (`{id, bundle}`: the running turn's card with actions, from `pending_card.json` that the worker keeps in the session's folder while one is open; a card without actions, such as loop-guard's, never counts) and `last_turn` (`{outcome, ended_at, seconds, origin}`, saved in the session's JSON as the engine ends each turn).
|
|
79
81
|
- The web frontend is a zero-build ES-module stack in `lib/samagotchi/web/public/`: `data.js` (retrieval, typed SSE `openStream` for a session and `openEvents` for the session list), `sessions_list.js` (the list as a pure reducer over `GET /api/events`: a snapshot replaces it, an upsert keeps a known card in place, a removal drops it; `families()` groups delegates under their parent), `families_view.js` (a parent's family chip, its rows and the strip's popover; the chip and rows as lit-html templates), `keep_focus.js` (lit reuses a list's nodes by position: a redraw keeps the focus on the same item by its `data-id` / `data-name`), `chains.js` (session chains: the links from `continues`, the cards folded into the latest link's, its `#N` chip and rows as lit-html templates, the info bar's ← → links, a refused continue, and the fresh-link rule), `notify.js` (which of those events needs the user, for the notifications), `app.js` (presentation/state; a session with no live stream gets one when its `session` event says `bridge_up`, after one re-read: `event_seq` starts over in each worker, so a dropped stream is never resumed with its old cursor), `bubbles.js` (the history's bubbles between the turns' rows: output chunks, end lines, every client's prompt bubbles by `enqueued_id` with their queued/steered badges, the idle recap, guardrail warnings, hooks' notices, plugins' steers, command and note bubbles; lit-html templates, put in the history with the sticky follow by `createBubbles`), `cards_place.js` (where plugin cards and a snapshot's notices go in the history: a card added or updated in place by its id, a running turn's into its step, a snapshot's each above the first turn after it, a finished turn's notice in its step's rows and its question resolved where it was asked; a card's action buttons), `format.js` (pure formatters such as `previewOf`), `plain_line.js` (markdown out of the plain one-line places: the stage headline, a closed step's title, an answered question card's kept-reply line), `prompt_history.js` (the composer's ↑/↓ over the shared prompt history), `commands.js` (session commands: which composer lines are commands, the page's own replies, a `command_ran`'s bubble, the page's command bubbles kept across a re-read), `composer.js` (where a composer line goes, `routeComposerLine`: /cut and /queue, the start page's own replies, a session command or `!cmd`, a typo hint, else a turn; and a session command sent from the composer or a chip), `delivery_line.js` (the composer's row above the text field in a session, always there so the composer keeps its height: the key hint while idle, and the delivery line while a turn runs: where a message typed now goes in, from the step's phase, clock and size, whether "cut in now" can land, and the queue link for a worker with the `delivery` feature, as a pure `deliveryLine`, its lit-html template and `createDeliveryControl`, which ticks it and keeps a choice armed from a link; see docs/cli.md, "Typing during a turn"), `restore.js` (a failed turn's prompt handed back into this tab's composer, before or after its `/turn` ack), `lists_view.js` (the session lists: the strip and its pill, all sessions with its search, count and select mode, their cards, the family and chain popovers; the cards, tile and pill as lit-html templates, drawn as new nodes each time, and the lists' orders as pure functions), `stream_handlers.js` (the open session's stream event handlers, by event type, over the page's views and hooks; a snapshot replays through them too), `stage_view.js` (the stage view: the running turn pinned above the composer, then handed off into the history; its pure rules and texts in `stage_model.js`, its live slots (headline, now row, trail, ticks) in `stage_slots.js`, its cap and held height in `stage_frame.js`, the in-use watch that times the hand-off in `stage_watch.js`), `info_bar.js` (the session bar's info line as a lit-html template over a plain model: id, status, model, chain links, chips, ctx, speed, clock, worker badge), `info_bar_model.js` (that model and the bar's buttons' states, built from the page's session state, its list and its in-flight continues and restarts as pure functions; `app.js`'s `updateInfoBar` applies them every second while a turn runs), `session_state.js` (what the page knows about the open session: its empty state, and `sessionStateFrom`, the fields a `GET /sessions/:id` payload sets), `refs.js` (refs: a kind and a target on the element, a plugin's ref data (label, http(s) URL actions) bound in a WeakMap by `bindRef`, the targets per kind, show in turn, one delegated set of click, key and hover listeners), `ref_menu.js` (a ref's `▾` and its menu), `ref_scan.js` (the `path:line` refs in an answer's or a step's text that name a file the session's rows touched), `answer_refs.js` (the refs a plugin attached to an answer, its `display_refs`: bound to the answer's bubble, paired with its links and text, bound with `bindRef`). Unit-tested via `npm test` (`spec/web/public/*.test.js`).
|
|
80
82
|
- Selecting a session in the web UI is read-only: `GET /api/sessions/:id` never spawns a worker (it reads a live worker's snapshot when one runs, else the session file). A prompt (`POST /turn`) or a command (`POST /command`) wakes the worker, and `/stream` briefly waits for a freshly-spawned bridge before answering. A caught-up SSE reconnect holds the stream open; `reset` markers are only sent for reconnects behind the ring window, or with a cursor from another worker (event ids are `<event_seq>-<epoch>`, one epoch per worker).
|