samagotchi 0.79.0 → 0.80.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 +30 -1
- data/README.md +7 -5
- data/docs/cli.md +24 -7
- data/docs/configuration.md +7 -4
- data/docs/internals/anthropic.md +3 -1
- data/docs/internals/bundles.md +4 -0
- data/docs/internals/engine.md +13 -3
- data/docs/plugins.md +144 -3
- data/docs/releasing.md +2 -2
- data/docs/sessions.md +1 -1
- data/docs/testing.md +32 -2
- data/lib/samagotchi/bootstrap/config_writer.rb +13 -3
- data/lib/samagotchi/bootstrap/probe.rb +153 -31
- data/lib/samagotchi/bootstrap_command.rb +92 -15
- data/lib/samagotchi/bridge.rb +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/continue_offer.rb +5 -5
- data/lib/samagotchi/engine.rb +40 -37
- data/lib/samagotchi/guardrails/approval.rb +6 -4
- data/lib/samagotchi/guardrails/gate.rb +7 -3
- data/lib/samagotchi/hooks/bundle_loader.rb +31 -16
- data/lib/samagotchi/hooks/registry.rb +44 -3
- data/lib/samagotchi/host_registry.rb +12 -4
- data/lib/samagotchi/llm/adapter_common.rb +4 -2
- data/lib/samagotchi/llm/anthropic_binding_recovery.rb +3 -0
- data/lib/samagotchi/llm/anthropic_messages.rb +2 -1
- data/lib/samagotchi/llm/http.rb +12 -7
- data/lib/samagotchi/plugin/card.rb +44 -0
- data/lib/samagotchi/plugin/context.rb +53 -8
- data/lib/samagotchi/plugin/http.rb +154 -0
- data/lib/samagotchi/plugin/http_transport.rb +77 -0
- data/lib/samagotchi/plugin/loader.rb +29 -10
- data/lib/samagotchi/question_answer.rb +29 -0
- data/lib/samagotchi/question_desk.rb +20 -19
- data/lib/samagotchi/relay_verifier.rb +1 -1
- data/lib/samagotchi/testing/boundary.rb +156 -0
- data/lib/samagotchi/testing/bundle.rb +349 -0
- data/lib/samagotchi/testing/fake_executable.rb +42 -0
- data/lib/samagotchi/testing/fake_http.rb +137 -0
- data/lib/samagotchi/testing/helpers.rb +56 -0
- data/lib/samagotchi/testing/host.rb +76 -0
- data/lib/samagotchi/testing/minitest.rb +31 -0
- data/lib/samagotchi/testing/recorder.rb +138 -0
- data/lib/samagotchi/testing/recording_api.rb +27 -0
- data/lib/samagotchi/testing/rspec.rb +31 -0
- data/lib/samagotchi/testing/sandbox.rb +163 -0
- data/lib/samagotchi/testing/sessions.rb +77 -0
- data/lib/samagotchi/testing/verdict.rb +28 -0
- data/lib/samagotchi/testing/waiting.rb +36 -0
- data/lib/samagotchi/testing.rb +26 -0
- data/lib/samagotchi/tools/delegate_relay.rb +7 -6
- data/lib/samagotchi/tools/tool_index.rb +3 -0
- data/lib/samagotchi/turn_state.rb +28 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/public/app.js +47 -23
- data/lib/samagotchi/web/public/images.js +33 -0
- data/lib/samagotchi/web/public/index.html +71 -38
- data/lib/samagotchi/web/public/level_chip.js +11 -11
- data/lib/samagotchi/web/public/lists_view.js +19 -14
- data/lib/samagotchi/web/public/llm_context_chip.js +15 -15
- data/lib/samagotchi/web/public/model_picker.js +1 -1
- data/lib/samagotchi/web/public/scope.js +3 -3
- data/lib/samagotchi/web/public/strip.js +35 -9
- data/lib/samagotchi/web/public/strip_grip.js +76 -0
- data/lib/samagotchi/web/public/thinking_chip.js +5 -5
- metadata +21 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5e8f390119025dcb6df095574fb9e59e94952c709a69ae6bc6e02ffa037e1a0d
|
|
4
|
+
data.tar.gz: c67e91b18d0ef7ac8916b03045c214c12ebcf98b469f84b0ae355bb41491388e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4c936b332ee7cafb74cb4e09b7f3b80acac7ba1247988e755078c61e72c201489b1a48f8970c6c3d70d88b7cedc31cf432d01c5b14018073a517b65f76e45de6
|
|
7
|
+
data.tar.gz: dc77457ac7bf6a0018966bef40ccaa67da732a43edbe21e0fbcef1193583a3e00934c70c4230d3fcf14f5dd23012730f9c41bb16be8d7a64754f361ff3c2ab4a
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,34 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.80.0] - 2026-10-17
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Web: the message box has an image button next to Start / Send that opens a file picker (on a phone, the camera or
|
|
16
|
+
the photo library); the picked images are added like pasted ones. A message now takes at most 20 images of up to
|
|
17
|
+
20 MB each as you add them, pasted, dropped or picked, and a toast names the files left out, instead of the send
|
|
18
|
+
failing.
|
|
19
|
+
- Bundles: `samagotchi/testing` (experimental), a test kit for bundles: load a bundle in a sandbox, fire its hooks, run its commands and tools, record steers, notices and cards (RSpec `:bundle` groups or Minitest `Testing::Case`; docs/plugins.md "Testing a bundle"). The shipped bundles' specs now live in each bundle's own `spec/` and use it; a boundary spec keeps bundles on the stable surface.
|
|
20
|
+
- Plugins: `ctx.http`, an HTTP client for calling services (`get`, `post_json`, `request`): short default timeouts, one error type (`Samagotchi::Plugin::HttpError`), a 4xx/5xx returned as a Response, one INFO `http` log line per call without query, headers or body (`purpose=plugin`, `bundle=`), and a call made for the turn stops when the turn is stopped (docs/plugins.md "Calling services"; experimental until 1.0). The test kit fakes it: `bundle.http.stub(…)`, `requests`, `unmatched`, and a request no stub answered fails the test.
|
|
21
|
+
- `chi bootstrap` sets up Anthropic Messages hosts: a server that refuses the OpenAI model list but lists Anthropic's
|
|
22
|
+
way (api.anthropic.com, a Messages-only gateway) is written as `api: anthropic` (the url without `/v1`, `auth: bearer`
|
|
23
|
+
when only Bearer worked), with `dialect: fireworks` for Fireworks' host or model ids. `--api openai|anthropic` and
|
|
24
|
+
`--dialect standard|fireworks` override the detection; direct Fireworks stays `api: openai` with a hint, and a test
|
|
25
|
+
turn's 404 on `/chat/completions` hints `--api anthropic`. A hand-written urlless `api: anthropic` host counts as
|
|
26
|
+
api.anthropic.com, so bootstrapping it again writes nothing.
|
|
27
|
+
- Plugins: `ctx.ask_model(model: "host:id")` asks a configured model (a host:id ref, an alias, or a model one of the hosts lists) instead of the session's; an unknown ref raises `ArgumentError`.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- Web: the start page says "+ new session" (and "new session in …") instead of "+ new chat", matching the rest of the UI.
|
|
32
|
+
- Web: the latest-sessions strip hides and shows with the same edge grip as the composer's (a drag up hides it, a drag
|
|
33
|
+
down or a click shows it, Enter/Space on the focused grip toggles, and the choice is remembered): the tile's `hide ▴`
|
|
34
|
+
and the top bar's `▴ hide` are gone, and while the strip is hidden its pill — now only its status — opens All
|
|
35
|
+
sessions in one click instead of showing the strip first.
|
|
36
|
+
|
|
37
|
+
Update with `chi update` (the system bundle 0.80.0), then restart `chi web`.
|
|
38
|
+
|
|
11
39
|
## [0.79.0] - 2026-10-17
|
|
12
40
|
|
|
13
41
|
### Fixed
|
|
@@ -3282,7 +3310,8 @@ and long-lived sessions.
|
|
|
3282
3310
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
3283
3311
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
3284
3312
|
|
|
3285
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
3313
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.80.0...HEAD
|
|
3314
|
+
[0.80.0]: https://github.com/dm1try/samagotchi/compare/v0.79.0...v0.80.0
|
|
3286
3315
|
[0.79.0]: https://github.com/dm1try/samagotchi/compare/v0.78.0...v0.79.0
|
|
3287
3316
|
[0.78.0]: https://github.com/dm1try/samagotchi/compare/v0.77.0...v0.78.0
|
|
3288
3317
|
[0.77.0]: https://github.com/dm1try/samagotchi/compare/v0.76.0...v0.77.0
|
data/README.md
CHANGED
|
@@ -69,10 +69,12 @@ Point chi at your model server; it works out the rest and writes
|
|
|
69
69
|
chi bootstrap 192.168.1.29:8081 # llama.cpp on another machine
|
|
70
70
|
chi bootstrap localhost:11434 # any OpenAI-compatible server
|
|
71
71
|
chi bootstrap https://openrouter.ai/api/v1 --key-env OPENROUTER_API_KEY
|
|
72
|
+
chi bootstrap https://api.anthropic.com --key-env ANTHROPIC_API_KEY
|
|
72
73
|
chi bootstrap # look on this machine's usual ports
|
|
73
74
|
```
|
|
74
75
|
|
|
75
|
-
It finds out whether the server is llama.cpp
|
|
76
|
+
It finds out whether the server is llama.cpp, OpenAI-compatible or Anthropic's
|
|
77
|
+
Messages API, picks the
|
|
76
78
|
model (or asks, when there are several), sends one test request and writes
|
|
77
79
|
the config. With a config already there, it adds a `hosts:` entry and keeps
|
|
78
80
|
the rest of the file. Then it installs the `core` bundles (loop-guard,
|
|
@@ -127,7 +129,7 @@ pbpaste | chi send -m "same bug?" <id> # a message to a session, the clipboar
|
|
|
127
129
|
chi send --new --wait -m "review feat/x" # a new session you can watch in the web; prints the answer
|
|
128
130
|
```
|
|
129
131
|
|
|
130
|
-
`@shot.png` in a prompt (or a pasted
|
|
132
|
+
`@shot.png` in a prompt (or a pasted, dropped or picked image in the web UI) shows the model an image, when it can see them; see [Images](docs/cli.md#images).
|
|
131
133
|
|
|
132
134
|
`/model` switches models, Ctrl-C cancels a turn, and Ctrl-D or `/detach` detaches (the session keeps running; `chi --attach ID` comes back). `/exit` detaches and stops the session's worker too, unless something still needs it (a running turn, another UI); `chi --resume ID` picks the conversation up again. `/exit --delete` also deletes the session once the worker has gone; `chi sessions delete ID` deletes one from the shell. `/archive` (or `chi sessions archive ID`) hides a session from every list and keeps it for good; `chi sessions list --archived` finds it again.
|
|
133
135
|
|
|
@@ -220,9 +222,9 @@ once, for the session, for the repo, or for the whole rule in the repo.
|
|
|
220
222
|
## Development
|
|
221
223
|
|
|
222
224
|
```sh
|
|
223
|
-
bundle exec rspec
|
|
224
|
-
npm test
|
|
225
|
-
npm run e2e
|
|
225
|
+
bundle exec rspec spec lib/samagotchi/bundles/*/spec # Ruby specs (chi's and the shipped bundles' own)
|
|
226
|
+
npm test # web frontend specs
|
|
227
|
+
npm run e2e # web UI happy paths in Chromium, fake model (once: npx playwright install chromium)
|
|
226
228
|
```
|
|
227
229
|
|
|
228
230
|
Integration specs against a live model server, and what the suite isolates: [docs/testing.md](docs/testing.md).
|
data/docs/cli.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Commands
|
|
4
4
|
|
|
5
|
-
- `chi bootstrap [HOST[:PORT]|URL]` — first setup: find the model server (llama.cpp
|
|
5
|
+
- `chi bootstrap [HOST[:PORT]|URL]` — first setup: find the model server (llama.cpp, OpenAI-compatible or Anthropic Messages), pick the model, send a test request and write config.yml, or add a `hosts:` entry to an existing one (see [First setup](#first-setup))
|
|
6
6
|
- `chi` — start a session in a background worker and attach the terminal to it, so the Web UI (or another terminal) can share it (see [Sharing a session](#sharing-a-session))
|
|
7
7
|
- `chi -p "your prompt"` — run a prompt, then stay attached
|
|
8
8
|
- `chi -p "your prompt" --non-interactive` — run a prompt, print the answer, exit
|
|
@@ -33,12 +33,23 @@
|
|
|
33
33
|
```sh
|
|
34
34
|
chi bootstrap 192.168.1.29:8081 # host:port (an IP or localhost: port 8080 when none; a domain: https 443, then http 80)
|
|
35
35
|
chi bootstrap https://openrouter.ai/api/v1 --key-env OPENROUTER_API_KEY
|
|
36
|
+
chi bootstrap https://api.anthropic.com --key-env ANTHROPIC_API_KEY
|
|
36
37
|
chi bootstrap # try localhost 8080, 11434, 1234, 8000
|
|
37
38
|
```
|
|
38
39
|
|
|
39
40
|
- **What it is.** llama.cpp's `/props` answering means the native API (no
|
|
40
41
|
`api:`); otherwise `GET /v1/models` answering means an OpenAI-compatible
|
|
41
|
-
server (`api: openai`).
|
|
42
|
+
server (`api: openai`). Only when that list is refused with 400, 401 or 403
|
|
43
|
+
does chi ask Anthropic's way (`anthropic-version`, the key in `x-api-key`,
|
|
44
|
+
once more as Bearer after a 401/403): a list there means Anthropic's
|
|
45
|
+
Messages API (`api: anthropic`, the url without `/v1`; `auth: bearer` when
|
|
46
|
+
only Bearer worked). A server that serves both, such as Splash, stays
|
|
47
|
+
`api: openai`. Fireworks (its host, or model ids under `accounts/fireworks/`)
|
|
48
|
+
on the Messages API gets `dialect: fireworks`; direct Fireworks is found as
|
|
49
|
+
OpenAI-compatible first and stays `api: openai`, with a line saying
|
|
50
|
+
`--api anthropic` takes its Messages API instead. `--api openai|anthropic`
|
|
51
|
+
and `--dialect standard|fireworks` (which implies `--api anthropic`) skip the
|
|
52
|
+
detection. A URL with a path is the API base as given
|
|
42
53
|
(`…/api/v1`); a domain without a scheme is tried over https, then http.
|
|
43
54
|
Each request is tried once, with 5 s timeouts, so a refused port answers
|
|
44
55
|
at once.
|
|
@@ -51,8 +62,11 @@ chi bootstrap # try localhost 8080, 11434, 1234, 8000
|
|
|
51
62
|
(a terminal gets a numbered list, a script the ids and exit 2). A llama.cpp
|
|
52
63
|
server also shows its context size and the prompt profile its chat template
|
|
53
64
|
matches.
|
|
54
|
-
- **The test.** One short chat request ("test: answered in 1.1 s"
|
|
55
|
-
skips it. A failed test still writes the config,
|
|
65
|
+
- **The test.** One short chat request ("test: answered in 1.1 s"; `POST /v1/messages`
|
|
66
|
+
on `api: anthropic`); `--no-test` skips it. A failed test still writes the config,
|
|
67
|
+
says so and exits 1. A server found as OpenAI-compatible whose `/chat/completions`
|
|
68
|
+
answers 404 gets the hint `try --api anthropic` (a gateway's Messages url that also
|
|
69
|
+
lists the OpenAI way).
|
|
56
70
|
- **The file.** With no config.yml it writes a small commented one:
|
|
57
71
|
`default.model` as `<host>:<model>` and one `hosts:` entry named `local`
|
|
58
72
|
(localhost), `lan` (an IP) or after the domain (`openrouter`); `--name`
|
|
@@ -662,9 +676,12 @@ A model that can see images gets them these ways:
|
|
|
662
676
|
- **A path in plain words**: "check /home/me/shot.png and describe it". The
|
|
663
677
|
model calls `read` on it and sees the picture; the tool line ends in
|
|
664
678
|
`→ image 1280×800`.
|
|
665
|
-
- **The Web UI**: paste or drop images into the composer
|
|
666
|
-
|
|
667
|
-
|
|
679
|
+
- **The Web UI**: paste or drop images into the composer, or pick them with
|
|
680
|
+
its image button (left of Start / Send; on a phone it offers the camera and
|
|
681
|
+
the photo library, and is hidden while a turn runs). Each shows as a chip (× removes it) and is sent with the
|
|
682
|
+
message; an image alone is sent as `[image: name]`. A message takes up to 20
|
|
683
|
+
images of up to 20 MB each; a toast names the files left out (over a cap, or
|
|
684
|
+
not an image). Messages show thumbnails; a click opens one full size.
|
|
668
685
|
- **`chi send --image PATH`** (repeatable, up to 20) with a message, from a
|
|
669
686
|
script or another terminal: `chi send --image shot.png -m "why is this red?"
|
|
670
687
|
3fa2`. The attached terminal and the web show it like an image typed there.
|
data/docs/configuration.md
CHANGED
|
@@ -10,8 +10,8 @@ Chi reads its settings from three places; the first that sets a value wins:
|
|
|
10
10
|
|
|
11
11
|
then the built-in default. [All settings](#all-settings) lists them.
|
|
12
12
|
|
|
13
|
-
`chi bootstrap HOST[:PORT]` writes a first config for a model server, or
|
|
14
|
-
it to this file as a `hosts:` entry (see [First setup](cli.md#first-setup)).
|
|
13
|
+
`chi bootstrap HOST[:PORT]` writes a first config for a model server (llama.cpp, OpenAI-compatible or Anthropic
|
|
14
|
+
Messages), or adds it to this file as a `hosts:` entry (see [First setup](cli.md#first-setup)).
|
|
15
15
|
|
|
16
16
|
Default path:
|
|
17
17
|
|
|
@@ -343,6 +343,9 @@ hosts:
|
|
|
343
343
|
|
|
344
344
|
`auth:` works on any host (`bearer` is the default everywhere but `api: anthropic`).
|
|
345
345
|
|
|
346
|
+
`chi bootstrap https://api.anthropic.com --key-env ANTHROPIC_API_KEY` writes such an entry (a gateway: its url;
|
|
347
|
+
`--api anthropic` / `--dialect fireworks` when the detection can't tell, see [First setup](cli.md#first-setup)).
|
|
348
|
+
|
|
346
349
|
**Claude 5.x preserved thinking:** Claude 5.x models (Opus, Sonnet and Haiku 5.5, Fable 5.1) bind each thinking
|
|
347
350
|
block to everything before it: the system prompt, the tools and the earlier messages. chi keeps the history
|
|
348
351
|
append-only, but the system prompt or the tools can change under a session: a resume or a worker respawn after
|
|
@@ -1467,8 +1470,8 @@ its own). The first target is a long session such as a day-long coordinator, whe
|
|
|
1467
1470
|
(the same flags on `chi send --new`), in the session with `/llm-context strategy … apply … budget …`
|
|
1468
1471
|
(docs/cli.md, "Session commands"), or from the web's `llm ctx` chip in the info bar.
|
|
1469
1472
|
- **A new web chat:** the start page's `llm ctx` chip, beside the model picker, shows the picked model's values
|
|
1470
|
-
and follows a model pick. Its form (titled "new
|
|
1471
|
-
`llm ctx stale · new
|
|
1473
|
+
and follows a model pick. Its form (titled "new session") sets a choice for that one chat: the chip reads
|
|
1474
|
+
`llm ctx stale · new session` in the accent colour, and the create sends it as `POST /api/sessions`'s
|
|
1472
1475
|
`llm_context` (`{"strategy": "stale,forget", "apply": "turn_end", "budget": "64k"}`, each a word as
|
|
1473
1476
|
`/llm-context` takes it, `default` for unset; anything else answers `400 invalid_llm_context` and starts
|
|
1474
1477
|
nothing), so the session file holds it before the first turn runs. It is not remembered: it resets after a
|
data/docs/internals/anthropic.md
CHANGED
|
@@ -180,7 +180,9 @@ Why not send drop_block on every request: it needs adaptive or enabled thinking
|
|
|
180
180
|
thinking field, `off` sends disabled), it would put a beta header on every request to every host, and it would drop
|
|
181
181
|
the same blocks again on every request until something pruned them. The reactive form does the same work in one
|
|
182
182
|
place, for one extra 400 round trip per break. The `anthropic-thinking-prefix-mismatch` diagnosis header (on the
|
|
183
|
-
400, undocumented) is
|
|
183
|
+
400, undocumented) is named by the adapter to its `LLM::HTTP` (`diagnosis_header:`, from
|
|
184
|
+
`AnthropicBindingRecovery::DIAGNOSIS_HEADER`), which keeps it on the provider error; it is logged as `diagnosis` on
|
|
185
|
+
`thinking_recovered`. The shared transport reads no provider header of its own.
|
|
184
186
|
|
|
185
187
|
## Cache breakpoints
|
|
186
188
|
|
data/docs/internals/bundles.md
CHANGED
|
@@ -44,6 +44,10 @@ user-facing side (what a bundle owns, profiles, needs) is in [memory.md](../memo
|
|
|
44
44
|
Readers of the installed state: `Hooks::BundleLoader` (Engine), `GuardrailWiring#bundle_rules`, `Plugin::Loader`,
|
|
45
45
|
`ContextProviders` (scripts), `BundleNeeds` (index markers). Each goes through `Provenance.each_installed` / `#record`
|
|
46
46
|
and checks the recorded sha256 (and `requires_chi`) before loading.
|
|
47
|
+
The checks live in `Hooks::BundleLoader.load` and `Plugin::Loader.load_bundle`; the loading itself is
|
|
48
|
+
`BundleLoader.load_hook_file` (one hook file, into a given `namespace:`) and `Plugin::Loader.register_file` (one
|
|
49
|
+
plugin file: build, `register`, `commit!`), which raise instead of logging: the bundle test kit
|
|
50
|
+
(`Samagotchi::Testing::Bundle`) loads a bundle's source through them, unchecked.
|
|
47
51
|
|
|
48
52
|
## Installer#run, in order
|
|
49
53
|
|
data/docs/internals/engine.md
CHANGED
|
@@ -350,14 +350,22 @@ read: the idle scheduler, the Bridge's request threads, plugin threads, the UIs.
|
|
|
350
350
|
TurnState
|
|
351
351
|
├── running?
|
|
352
352
|
├── controller (the running turn's CancellationController)
|
|
353
|
+
├── thread, ended (the turn's own thread; its end event is out: post-turn work)
|
|
353
354
|
├── sink (the turn's on_event)
|
|
354
355
|
├── steers (plugin steers for the running turn's next boundary)
|
|
355
356
|
├── carried_steers (for the next turn that begins)
|
|
356
357
|
└── activity clock (last_activity_at, activity_seq)
|
|
357
358
|
```
|
|
358
359
|
|
|
359
|
-
- `begin!(controller:, sink:)`: running, with controller and sink
|
|
360
|
-
|
|
360
|
+
- `begin!(controller:, sink:)`: running, with controller and sink (and the calling thread, the turn's own), in one
|
|
361
|
+
step; carried steers become steers.
|
|
362
|
+
- `ended!`: the turn's end is decided (`end_turn`, next to `turn.ended`): its after_turn / session_end hooks run
|
|
363
|
+
after it and before `finish!`.
|
|
364
|
+
- `own_controller(handler:)`: the controller for work done for the turn itself, which a plugin's `ctx.http` call
|
|
365
|
+
without `cancel:` stops with (`Plugin::Host#cancel_controller`, `Engine#plugin_cancel_controller`): the turn's, on
|
|
366
|
+
its thread or inside a plugin handler, until `ended!`; nil elsewhere (an anytime command, a service, a plugin's
|
|
367
|
+
own thread) and outside a turn. An init task's own controller wins inside one.
|
|
368
|
+
- `finish!`: not running, controller, thread and sink cleared; returns the steers never taken (logged as dropped).
|
|
361
369
|
- `steer(text, source:)`: queues a steer for the running turn (false with no turn).
|
|
362
370
|
- `steer_next_turn(text, source:)`: queues a steer for the next turn that begins.
|
|
363
371
|
|
|
@@ -515,7 +523,8 @@ Engine initialization (in this order, among other things):
|
|
|
515
523
|
@tools = Tools::Builtins.registry; @command_registry (SessionCommands' built-ins); @plugin_tasks
|
|
516
524
|
@toolkit_binding (ToolkitBinding: the kits, the plugins' among them, and the turn-start apply)
|
|
517
525
|
@thinking_notices (ThinkingNotices); @tool_index = Tools::ToolIndex.new; @peer_sources = PeerSources.new
|
|
518
|
-
the bundles' plugins load (load_plugins → ExtensionLoad#load_plugins
|
|
526
|
+
the bundles' plugins load (load_plugins → ExtensionLoad#load_plugins; each plugin's ctx is a
|
|
527
|
+
Plugin::Context over plugin_host, whose ctx.sessions it builds itself: the test kit passes its own, sessions:),
|
|
519
528
|
then (plugins or not) @tool_index.declare(@tools): find_tools + call_tool (source "index", not core),
|
|
520
529
|
declared per toolkit by Registry#declared? (a source registered at load, or a reachable built-in) and
|
|
521
530
|
sets call_tool's unwrap (Tools::ToolIndex: the chi group from Registry#reachable_names, the sources'
|
|
@@ -728,6 +737,7 @@ Paths are under `lib/samagotchi/`.
|
|
|
728
737
|
| `toolkit_binding.rb` | `ToolkitBinding`: the Engine's toolkit seam (the kits, `#explained`, `ctx.use_toolkit`, the turn-start `#apply!` behind `Engine#apply_toolkit!`) |
|
|
729
738
|
| `hooks/registry.rb` | Hook registry, fire order, `Hooks::Runtime` |
|
|
730
739
|
| `hook_routes.rb` | `HookRoutes`: the Engine's routes for a hook's or plugin's notice (`:hook_notice`, `fallback_for:`), question, stop and steer, and the guardrails' after-load line |
|
|
740
|
+
| `question_desk.rb`, `pending_question.rb`, `question_answer.rb` | `QuestionDesk`: the one open question (ask, post, answer, cancel, relay mark); `PendingQuestion` and `QuestionAnswer`, its values (Hashes only in events, Bridge JSON, the session file and the hook/plugin `ask_user` result) |
|
|
731
741
|
| `event_routes.rb` | `EventRoutes`: where a card (`Engine#show_card`), a notice (`HookRoutes#notify`) and a guardrails line (`#guardrail_notify`, turn or between turns only) go: held while the plugins load, an anytime command's, an init task's, a running turn's event, else announced or held for `holding_announcements` |
|
|
732
742
|
| `engine_peers.rb`, `peer_sources.rb` | `Engine::PeerView` (what the kernel's peer tools see of the Engine) and `Engine::RelayPeer` (a delegate's approval relay); `PeerSources`: the attached context sources a peer bundle declares as its peer's answers (`chi.peer_source`) |
|
|
733
743
|
| `extension_load.rb` | What an Engine loads at start: config.yml and bundle hooks, plugins, `bundles:` settings; events held while plugins load |
|
data/docs/plugins.md
CHANGED
|
@@ -744,6 +744,7 @@ full label, `plugin.rb (bundle my-bundle)`.
|
|
|
744
744
|
| `ctx.stop_turn(reason)` | stop the running turn after a warn notice with the reason; true when it stopped one now. In a `:before_tool_call` block it also denies that call |
|
|
745
745
|
| `ctx.stop_generation(reason)` | cut the generation that is streaming: the turn goes on and the model is asked again ([hooks.md](hooks.md#watching-the-stream)). Shows nothing: post your own notice. True when it cut one now |
|
|
746
746
|
| `ctx.ask_model(messages:, prompt:, …)` | a side answer from the session's model: see [Side answers](#side-answers-ctxask_model) |
|
|
747
|
+
| `ctx.http` | an HTTP client for calling services (`get`, `post_json`, `request`): see [Calling services](#calling-services-ctxhttp) |
|
|
747
748
|
| `ctx.sessions` | fork, send to, read, list and stop other sessions: see [Other sessions](#other-sessions-ctxsessions) |
|
|
748
749
|
| `ctx.toolkit` | the name of the session's toolkit (`full` for a session from before toolkits); nil with no session |
|
|
749
750
|
| `ctx.declared_tools` | the tool names the session's next turn declares (its toolkit resolved, with its own adds and removes): ask this, not the name, whether a tool is there |
|
|
@@ -842,6 +843,10 @@ next turn never see it, and no hook fires. It returns the answer text.
|
|
|
842
843
|
- `max_tokens:` defaults to 1024. An answer cut off by it ends with `…`.
|
|
843
844
|
- `cancel:` takes a `Samagotchi::CancellationController`; cancelling it
|
|
844
845
|
aborts the request.
|
|
846
|
+
- `model:` names a configured model instead of the session's: `"host:id"` for a host in config.yml `hosts:`, an
|
|
847
|
+
alias from `models:`, or a bare id one of the hosts lists or declares. The host's own key and settings are used;
|
|
848
|
+
a plugin never names a URL or a key here (that is [`ctx.http`](#calling-services-ctxhttp)). A ref that names
|
|
849
|
+
nothing configured raises `ArgumentError`, asking nothing: a typo never goes to another model.
|
|
845
850
|
- It blocks until the answer comes, so call it from an anytime command or a
|
|
846
851
|
thread of your own. A local server that runs one request at a time
|
|
847
852
|
(llama.cpp with one slot) answers it after a running turn's current
|
|
@@ -853,6 +858,78 @@ next turn never see it, and no hook fires. It returns the answer text.
|
|
|
853
858
|
It goes through the host's OpenAI API (`/v1/chat/completions`), which
|
|
854
859
|
llama.cpp serves on a native host too.
|
|
855
860
|
|
|
861
|
+
## Calling services: `ctx.http`
|
|
862
|
+
|
|
863
|
+
```ruby
|
|
864
|
+
# The http-echo bundle (spec fixture, docs/plugins.md "Calling services"):
|
|
865
|
+
# one tool that asks a local HTTP service and returns its answer.
|
|
866
|
+
class Plugin
|
|
867
|
+
def initialize(settings)
|
|
868
|
+
@url = settings.fetch("url", "http://127.0.0.1:9000/ask")
|
|
869
|
+
end
|
|
870
|
+
|
|
871
|
+
def register(chi)
|
|
872
|
+
chi.tool "ask_service", "Ask the local question service and return its answer",
|
|
873
|
+
params: { question: { type: "string", description: "What to ask", required: true } },
|
|
874
|
+
label: "asking" do |args, ctx|
|
|
875
|
+
res = ctx.http.post_json(@url, { question: args["question"] }, timeout: 5)
|
|
876
|
+
if res.ok?
|
|
877
|
+
res.json["answer"].to_s
|
|
878
|
+
else
|
|
879
|
+
ctx.log.warn(:ask_failed, status: res.status)
|
|
880
|
+
"the service answered #{res.status}"
|
|
881
|
+
end
|
|
882
|
+
rescue Samagotchi::Plugin::HttpError => e
|
|
883
|
+
"the service is not reachable: #{e.message}"
|
|
884
|
+
end
|
|
885
|
+
end
|
|
886
|
+
end
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
That is `spec/fixtures/http_echo_bundle` (a spec keeps the two the same; `chi bundle install` it to try it): the
|
|
890
|
+
shape to copy for a plugin that calls a service, tested on `bundle.http` as in
|
|
891
|
+
`spec/samagotchi/plugin/http_echo_bundle_spec.rb`.
|
|
892
|
+
|
|
893
|
+
One way to call an HTTP service, with timeouts, errors, logging and Stop handled the same for every bundle.
|
|
894
|
+
`Net::HTTP` stays available for what this doesn't do (streaming, multipart, redirects, connection reuse).
|
|
895
|
+
|
|
896
|
+
| | |
|
|
897
|
+
|---|---|
|
|
898
|
+
| `ctx.http.get(url, headers: {}, timeout: 10)` | GET |
|
|
899
|
+
| `ctx.http.post_json(url, body, headers: {}, timeout: 10)` | POST `body` encoded as JSON, with `Content-Type` and `Accept: application/json` (unless `headers` names them) |
|
|
900
|
+
| `ctx.http.request(method, url, body: nil, headers: {}, timeout: 10)` | GET, POST, PUT, PATCH, DELETE or HEAD; `body` is a String sent as is (set your own `Content-Type`) |
|
|
901
|
+
|
|
902
|
+
Every call also takes `cancel:`, a `Samagotchi::CancellationController`, which replaces the default (below).
|
|
903
|
+
|
|
904
|
+
- `url` is a String or URI, `http` or `https`. A bad URL, another scheme, an unknown method, a bad header, a
|
|
905
|
+
`timeout` that isn't a positive number, or a body `post_json` can't encode as JSON raises **ArgumentError**
|
|
906
|
+
before any request.
|
|
907
|
+
- `timeout` (seconds) is the connect, the write and the **per-read** timeout, not a total deadline: a server that dribbles
|
|
908
|
+
bytes can take longer.
|
|
909
|
+
- It returns a `Samagotchi::Plugin::Http::Response`: `status` (Integer), `headers` (frozen, lower-case names,
|
|
910
|
+
repeated values joined with `", "`), `body` (a String, `""` for HEAD or no body), `ok?` (2xx) and `json`. A
|
|
911
|
+
**4xx or 5xx is returned, not raised**: check `ok?`. A redirect is not followed: the 3xx comes back with its
|
|
912
|
+
`location` header.
|
|
913
|
+
- `body` is tagged UTF-8 when its bytes are valid UTF-8, else left binary: `body.scrub` it before handing unknown
|
|
914
|
+
text to the model. There is no size limit: don't read an endpoint you don't trust to be small.
|
|
915
|
+
- `json` parses the body on each call and raises `HttpError` ("not JSON: …") when it isn't JSON.
|
|
916
|
+
- **One error type.** `Samagotchi::Plugin::HttpError` means the request didn't complete: refused, timed out, DNS,
|
|
917
|
+
TLS, reset, a malformed response, a bad compressed body, or `json` on a body that isn't JSON. No raw `Errno`,
|
|
918
|
+
`Net::` or `Zlib` error escapes (the original is the error's `cause`). The message says what and where
|
|
919
|
+
(`"POST 127.0.0.1:9000: connection refused"`), never headers or a body. `Samagotchi::Plugin::HttpCancelled`
|
|
920
|
+
(a subclass) means the call was cancelled.
|
|
921
|
+
- `http_proxy`, `https_proxy` and `no_proxy` are honoured as for chi's model calls (loopback is never proxied).
|
|
922
|
+
chi's own `User-Agent` is always sent: yours is replaced.
|
|
923
|
+
- **Stop.** A call made for the turn (a tool, a hook's handler) stops when the turn is stopped, and a call inside an
|
|
924
|
+
init task when chi shuts down; a call from your own thread, a `chi.service` or an anytime command has only its
|
|
925
|
+
timeout unless you pass `cancel:`. After the turn has ended (an `:after_turn` or `:session_end` hook of a
|
|
926
|
+
stopped turn) a call is not cancelled by that stop.
|
|
927
|
+
- **The log.** Each call writes one `http` line to the debug log: INFO (ERROR when it fails) with `bundle=`,
|
|
928
|
+
`purpose=plugin`, the method, the URL **without its query**, the status and the time. Never headers or bodies.
|
|
929
|
+
A plugin that polls a dead service therefore logs an ERROR per poll.
|
|
930
|
+
|
|
931
|
+
`ctx.http` is experimental until 1.0, like the test kit.
|
|
932
|
+
|
|
856
933
|
## Other sessions: `ctx.sessions`
|
|
857
934
|
|
|
858
935
|
```ruby
|
|
@@ -1003,7 +1080,9 @@ CHANGELOG line and bumps the shipped bundles that use it. Anything else under
|
|
|
1003
1080
|
`Samagotchi::Plugin::ToolResult`, `Samagotchi::Plugin::ModelError` (and its
|
|
1004
1081
|
subclass `Samagotchi::Plugin::ModelCancelled`),
|
|
1005
1082
|
`Samagotchi::Plugin::Service::Stopped`, `Samagotchi::Plugin::Sessions::Error`
|
|
1006
|
-
and `Samagotchi::Plugin::AttachedContext::Error`.
|
|
1083
|
+
and `Samagotchi::Plugin::AttachedContext::Error`. `ctx.http` and its
|
|
1084
|
+
`Samagotchi::Plugin::HttpError` (with the subclass `HttpCancelled`) and
|
|
1085
|
+
`Samagotchi::Plugin::Http::Response` are **experimental until 1.0**, like the test kit.
|
|
1007
1086
|
2. `Samagotchi::Plugin::Peer` as a base class. A subclass defines `post`,
|
|
1008
1087
|
`check_hint!`, `paired_lines`, `transport_line`, `peer_dir` and
|
|
1009
1088
|
`setup_paragraph` (optionally `skill_substitutions`), and sets `NAME`,
|
|
@@ -1020,6 +1099,12 @@ CHANGELOG line and bumps the shipped bundles that use it. Anything else under
|
|
|
1020
1099
|
`Samagotchi::Log.debug`, `Samagotchi::Config.get` and
|
|
1021
1100
|
`Samagotchi::VERSION`.
|
|
1022
1101
|
|
|
1102
|
+
`Samagotchi::Testing::Boundary::ALLOWED` is the checked copy of this list,
|
|
1103
|
+
and `Boundary::EXCEPTIONS` the per-bundle list of what each shipped bundle
|
|
1104
|
+
(and its specs) reaches beyond it, each with why: the APIs to design before
|
|
1105
|
+
that bundle could leave chi's repo. `spec/samagotchi/testing/boundary_spec.rb`
|
|
1106
|
+
fails on a reference in neither (docs/testing.md).
|
|
1107
|
+
|
|
1023
1108
|
**Used by shipped bundles today, but internal** (may change without notice; a
|
|
1024
1109
|
bundle that needs one should ask for an API instead):
|
|
1025
1110
|
|
|
@@ -1032,6 +1117,62 @@ bundle that needs one should ask for an API instead):
|
|
|
1032
1117
|
`Samagotchi::Guardrails::GitInfo`.
|
|
1033
1118
|
- `Samagotchi::Tools::Execute.const_get` reflection.
|
|
1034
1119
|
|
|
1120
|
+
## Testing a bundle (experimental)
|
|
1121
|
+
|
|
1122
|
+
`samagotchi/testing` loads a bundle the way chi does and records what it does at chi's edges, so a test
|
|
1123
|
+
needs no fake `chi` or `ctx`. It is **experimental**: it may change in any release before 1.0.0 and is not
|
|
1124
|
+
part of "Stable for bundles".
|
|
1125
|
+
|
|
1126
|
+
```ruby
|
|
1127
|
+
require "samagotchi/testing"
|
|
1128
|
+
|
|
1129
|
+
bundle = Samagotchi::Testing::Bundle.load("path/to/my-bundle", settings: { "every" => 3 },
|
|
1130
|
+
host: { session_id: "s1", cwd: Dir.pwd })
|
|
1131
|
+
bundle.fire(:after_turn, status: "completed") # → the event, after the hooks
|
|
1132
|
+
bundle.command("/mine stop") # → what the command returned
|
|
1133
|
+
bundle.tool("my_tool", path: "a.txt") # → the text the model reads
|
|
1134
|
+
verdict = bundle.tool_call({ name: "execute", content: "rm -rf /" })
|
|
1135
|
+
verdict.deny? # a real Guardrails::Verdict (votes: verdict.votes)
|
|
1136
|
+
bundle.notices.map(&:text) # also cards, steers, stops, generation_stops, questions
|
|
1137
|
+
bundle.answer({ selected: "yes" }) # the next ask_user's answer
|
|
1138
|
+
bundle.model_answers << "a summary" # the next ask_model's answer
|
|
1139
|
+
bundle.sessions.on(:children) { |all:| [] } # ctx.sessions, scripted and recorded
|
|
1140
|
+
bundle.http.stub(:post, "http://127.0.0.1:9000/classify", json: { label: "safe" }) # ctx.http, never the network
|
|
1141
|
+
bundle.set(running: false) # the session's values: cwd:, messages:, cancelled: …
|
|
1142
|
+
bundle.shutdown
|
|
1143
|
+
```
|
|
1144
|
+
|
|
1145
|
+
- **Real seams.** The plugin loads through `Plugin::Loader.register_file` (the real `chi` API and a real `ctx`),
|
|
1146
|
+
the hook files through `Hooks::BundleLoader.load_hook_file`, into registries that already hold chi's own
|
|
1147
|
+
commands and tools: a name chi has fails the load as it would in chi. `tool_call` runs a real
|
|
1148
|
+
`Guardrails::Gate` (context and targets computed for the call; `verdict:` starts from one with a vote in it).
|
|
1149
|
+
Only the edges are recorded: the session (`Testing::Host`), the hook helpers (`Testing::Runtime`),
|
|
1150
|
+
`ctx.sessions`, `chi.init` tasks (`bundle.run_inits`) and `chi.replace_tools` sets
|
|
1151
|
+
(`bundle.apply_staged_tools!`). No sha256 or `requires_chi` check: it loads the source as it is.
|
|
1152
|
+
- **More:** `bundle.plugin` (the instance `register` ran on, for its own methods), `bundle.declarations` (the
|
|
1153
|
+
`chi.*` calls `register` made, in order, as made), `bundle.namespace`, `bundle.hook_names(event)`,
|
|
1154
|
+
`bundle.run_inits`, `sandbox.write_memory(name, text, scope:)` / `memory_dir`, `sandbox.fake_executable(name,
|
|
1155
|
+
body)` (first on `PATH`), `wait_until { }`.
|
|
1156
|
+
- **`ctx.http` is faked.** `bundle.http.stub(method, url_or_regexp, status: 200, headers: {}, body: nil, json: nil,
|
|
1157
|
+
error: nil) { |request| … }` answers it: a String URL matches exactly (query included), a Regexp the full URL, later
|
|
1158
|
+
stubs win, and a stub answers every matching call (a block that counts makes a sequence). `error: :refused` or
|
|
1159
|
+
`:timeout` raises the `HttpError` the real client would; `bundle.http.requests` lists what the plugin sent
|
|
1160
|
+
(`method`, `url`, `headers`, `body`, `json`) and `bundle.http.unmatched` what no stub answered. A request no stub
|
|
1161
|
+
answered raises `Samagotchi::Testing::UnstubbedRequest` (not an `HttpError`, but a plugin may rescue it anyway) and
|
|
1162
|
+
**fails the test at teardown** (RSpec and Minitest adapters: after the cleanup, and only a test that passed), so a
|
|
1163
|
+
spec can't reach the network. With plain `Bundle.load` there is no teardown: check `bundle.http.unmatched`.
|
|
1164
|
+
`bundle.set(cancelled: true)` makes a call without `cancel:` raise `HttpCancelled`.
|
|
1165
|
+
- **A hook that raises fails the call** (`Testing::HookFailed`, with the hook's error) where chi would log it and
|
|
1166
|
+
go on; `bundle.lenient { … }` collects them in `bundle.errors` instead.
|
|
1167
|
+
- **Sandbox.** A test runs with temp config and state homes (config.yml, memories, `ctx.data_dir`, the debug
|
|
1168
|
+
log), `HOME`, `CLAUDE_CONFIG_DIR` and `CODEX_HOME`, no git config of yours, no Keychain and none of your
|
|
1169
|
+
`SAMAGOTCHI_*` settings; `sandbox.config(hash)` writes its config.yml.
|
|
1170
|
+
- **RSpec:** `require "samagotchi/testing/rspec"`; example groups tagged `:bundle` (a spec under a bundle's own
|
|
1171
|
+
`spec/` is tagged by itself) get `load_bundle(dir)`, `shipped_bundle(name)` and `sandbox`, each example in its
|
|
1172
|
+
own sandbox, its bundles shut down after it (chi's shipped bundles keep theirs in `<bundle>/spec/`,
|
|
1173
|
+
docs/testing.md). **Minitest:** `require "samagotchi/testing/minitest"` and subclass
|
|
1174
|
+
`Samagotchi::Testing::Case` (the same helpers).
|
|
1175
|
+
|
|
1035
1176
|
## The btw bundle
|
|
1036
1177
|
|
|
1037
1178
|
`chi bundle install btw` (or the `dev` profile) installs the bundle shipped with chi. It is written
|
|
@@ -1097,7 +1238,7 @@ bundles:
|
|
|
1097
1238
|
and values: only the digest is stored) and `cwd`. A session with a saved
|
|
1098
1239
|
list can search its tools at once and **doesn't start the server**: the
|
|
1099
1240
|
first call of one of its tools does (the call's row shows the wait). So a
|
|
1100
|
-
session that never uses MCP spawns nothing, and a new
|
|
1241
|
+
session that never uses MCP spawns nothing, and a new session opens without
|
|
1101
1242
|
waiting for `npx`. If the live list differs from the saved one, the saved
|
|
1102
1243
|
one is replaced, and so is what a search finds. The model's tools never
|
|
1103
1244
|
change, so the prompt cache keeps. The saved file also keeps the first
|
|
@@ -1127,7 +1268,7 @@ bundles:
|
|
|
1127
1268
|
you want at once.
|
|
1128
1269
|
- **`npx -y …@latest` checks the npm registry on every start** (~3.4 s for
|
|
1129
1270
|
`chrome-devtools-mcp`, against ~0.9 s for the installed binary). The cache
|
|
1130
|
-
hides it from new
|
|
1271
|
+
hides it from new sessions, but not from the first call. For a faster first
|
|
1131
1272
|
call, install the server once and run it directly:
|
|
1132
1273
|
|
|
1133
1274
|
```yaml
|
data/docs/releasing.md
CHANGED
|
@@ -63,7 +63,7 @@ as the GitHub release body.
|
|
|
63
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. |
|
|
64
64
|
| `rake release:draft_changelog` | A draft Unreleased section from the commit subjects since the last tag, grouped. Stdout only. |
|
|
65
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. |
|
|
66
|
-
| `rake release:check` | Clean tracked tree, VERSION == system bundle, a CHANGELOG section, no `requires_chi_next` left, `bundles:check`, `rake lint` (RuboCop), rspec + npm test, `gem build`, then a clean install into a temp GEM_HOME that runs `chi --version`, `chi self` and `chi bundle list` with a temp HOME. Needs the network (gem dependencies). |
|
|
66
|
+
| `rake release:check` | Clean tracked tree, VERSION == system bundle, a CHANGELOG section, no `requires_chi_next` left, `bundles:check`, `rake lint` (RuboCop), rspec (both roots: `spec` and the bundles' own `spec/`) + `rake bundles:specs` + npm test, `gem build`, then a clean install into a temp GEM_HOME that runs `chi --version`, `chi self` and `chi bundle list` with a temp HOME. Needs the network (gem dependencies). |
|
|
67
67
|
| `rake release:notes[X.Y.Z]` | Prints the CHANGELOG section (default: the current VERSION). |
|
|
68
68
|
|
|
69
69
|
## Runbook
|
|
@@ -165,4 +165,4 @@ things differ there, and a new spec that trips on them fails only on CI:
|
|
|
165
165
|
To run it locally, use Docker with the Ruby build CI uses
|
|
166
166
|
(`ruby-X.Y.Z-ubuntu-24.04-x64.tar.gz` from ruby/ruby-builder's releases) on
|
|
167
167
|
`ubuntu:24.04` with `LANG=C.UTF-8`, `zip` and `git`, and run
|
|
168
|
-
`CI=1 BUNDLE_PATH=… bundle exec rspec`.
|
|
168
|
+
`CI=1 BUNDLE_PATH=… bundle exec rspec spec lib/samagotchi/bundles/*/spec`.
|
data/docs/sessions.md
CHANGED
|
@@ -76,7 +76,7 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
|
|
|
76
76
|
**Ordering:**
|
|
77
77
|
|
|
78
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.
|
|
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).
|
|
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 sessions start in that folder; the header chip says `<project> · all`, and `all` opens the same place without `?dir` (every session; a new session 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, and the grip on the chat's top edge — the same thin handle as the composer's, its twin — puts them away and brings them back: a drag up (or a click, or Enter/Space on it) hides them, a drag down (or a click) shows them again, and the choice is remembered. A hidden strip leaves its status in the top bar (`N sessions · ● 1 live · ● 1 waiting`, from the cards), and a click on that pill opens All sessions, without showing the strip first. In a window under 900 px wide, where the strip scrolls sideways, the grip stays under it, outside the scroller; a window under 700 px tall hides the strip by itself, so a question or approval card in the stage keeps room, and the grip shows it 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 session. 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 session 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 session 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).
|
|
80
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).
|
|
81
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`).
|
|
82
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).
|