samagotchi 0.109.0 → 0.111.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 +57 -1
- data/bin/chi +5 -1
- data/docs/architecture.md +9 -6
- data/docs/cli.md +43 -1
- data/docs/configuration.md +140 -3
- data/docs/internals/engine.md +58 -14
- data/docs/internals/participants.md +7 -4
- data/docs/internals/prompt-caching.md +5 -0
- data/docs/internals/remote-sessions.md +3 -3
- data/docs/internals/web-push.md +200 -0
- data/docs/internals/working-set.md +7 -6
- data/docs/plugins.md +1 -1
- data/docs/sessions.md +4 -2
- data/docs/sub-agent.md +1 -1
- data/lib/samagotchi/bridge/turn_accumulator.rb +3 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/config.rb +4 -0
- data/lib/samagotchi/context_note.rb +7 -3
- data/lib/samagotchi/engine.rb +166 -499
- data/lib/samagotchi/engine_snapshots.rb +223 -0
- data/lib/samagotchi/event_fanout.rb +236 -0
- data/lib/samagotchi/idle_recap.rb +1 -1
- data/lib/samagotchi/idle_reminders.rb +1 -1
- data/lib/samagotchi/kernel_loop.rb +12 -7
- data/lib/samagotchi/llm/chat_loop.rb +9 -2
- data/lib/samagotchi/message_time.rb +104 -0
- data/lib/samagotchi/model_binding.rb +138 -0
- data/lib/samagotchi/reminder_queue.rb +20 -3
- data/lib/samagotchi/reply_wait/poll.rb +39 -1
- data/lib/samagotchi/run_options.rb +4 -1
- data/lib/samagotchi/send_command/wait_target.rb +10 -3
- data/lib/samagotchi/send_command.rb +2 -1
- data/lib/samagotchi/session.rb +12 -3
- data/lib/samagotchi/session_access/contract.rb +1 -1
- data/lib/samagotchi/session_access/local.rb +11 -5
- data/lib/samagotchi/session_access/remote.rb +2 -1
- data/lib/samagotchi/session_access.rb +7 -3
- data/lib/samagotchi/session_inbox.rb +7 -3
- data/lib/samagotchi/session_manager.rb +5 -0
- data/lib/samagotchi/steer.rb +5 -3
- data/lib/samagotchi/terminal_ui/attached_loop.rb +70 -9
- data/lib/samagotchi/terminal_ui/between_turns.rb +8 -3
- data/lib/samagotchi/terminal_ui/formatting.rb +13 -5
- data/lib/samagotchi/terminal_ui/gap_clock.rb +78 -0
- data/lib/samagotchi/terminal_ui/reline_seam.rb +20 -2
- data/lib/samagotchi/terminal_ui.rb +40 -5
- data/lib/samagotchi/tool_activity.rb +11 -0
- data/lib/samagotchi/turn_checkpoint.rb +6 -1
- data/lib/samagotchi/turn_endings.rb +137 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +28 -1369
- data/lib/samagotchi/web/grants.rb +3 -12
- data/lib/samagotchi/web/message_display.rb +274 -0
- data/lib/samagotchi/web/participant_view.rb +12 -5
- data/lib/samagotchi/web/private_files.rb +31 -0
- data/lib/samagotchi/web/public/app.js +41 -9
- data/lib/samagotchi/web/public/apple-touch-icon.png +0 -0
- data/lib/samagotchi/web/public/bubbles.js +29 -7
- data/lib/samagotchi/web/public/format.js +72 -6
- data/lib/samagotchi/web/public/icons/chi-180.png +0 -0
- data/lib/samagotchi/web/public/icons/chi-192.png +0 -0
- data/lib/samagotchi/web/public/icons/chi-512.png +0 -0
- data/lib/samagotchi/web/public/index.html +6 -0
- data/lib/samagotchi/web/public/manifest.webmanifest +13 -0
- data/lib/samagotchi/web/public/notifications.js +137 -3
- data/lib/samagotchi/web/public/notify.js +1 -1
- data/lib/samagotchi/web/public/participant.html +2 -0
- data/lib/samagotchi/web/public/participant.js +30 -7
- data/lib/samagotchi/web/public/participant_model.js +25 -2
- data/lib/samagotchi/web/public/push.js +95 -0
- data/lib/samagotchi/web/public/stage_view.js +8 -0
- data/lib/samagotchi/web/public/stream_handlers.js +32 -4
- data/lib/samagotchi/web/public/sw.js +101 -0
- data/lib/samagotchi/web/public/time_gaps.js +98 -0
- data/lib/samagotchi/web/public/tokens.css +2 -2
- data/lib/samagotchi/web/public/turn_events.js +25 -1
- data/lib/samagotchi/web/public/turn_html.js +15 -8
- data/lib/samagotchi/web/public/turn_view.js +8 -3
- data/lib/samagotchi/web/push/attention.rb +105 -0
- data/lib/samagotchi/web/push/client.rb +103 -0
- data/lib/samagotchi/web/push/crypto.rb +126 -0
- data/lib/samagotchi/web/push/notifier.rb +230 -0
- data/lib/samagotchi/web/push/store.rb +285 -0
- data/lib/samagotchi/web/push.rb +73 -0
- data/lib/samagotchi/web/push_routes.rb +99 -0
- data/lib/samagotchi/web/running.rb +39 -4
- data/lib/samagotchi/web/server.rb +112 -32
- data/lib/samagotchi/web/session_control.rb +441 -0
- data/lib/samagotchi/web/session_create.rb +384 -0
- data/lib/samagotchi/web/session_show.rb +411 -0
- data/lib/samagotchi/web/tunnel/tailscale.rb +325 -0
- data/lib/samagotchi/web/tunnel.rb +133 -0
- data/lib/samagotchi/worker.rb +3 -2
- metadata +30 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c3030ebe01072de864d730b27cfc709368d48948260f07b3e37fa83f8ba4b87a
|
|
4
|
+
data.tar.gz: 38f33d3c633d5467c8200817c2a12fc5fd0e287cf87cbd5233f0486326280eed
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: af19fcb8e29f712f43098d68d466bb8e68bdfc3b87177d7a2f99e73a2b609a535322ab181cacae03d7cf00e774a2f28933ff5b3ba30446d3e4112724f213bf6f
|
|
7
|
+
data.tar.gz: 72fb51ec5f3d216a36e27a040643517104fc8988f8010b6bdc70ac4f3b2bd2db281a4cc629a54b75e88dfbe46b78d02014bfc65142f39e40e6952ab13c919f7f
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,60 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.111.0] - 2026-10-24
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- chi web: push notifications to a phone's Home Screen app (approvals and questions) over an https public URL
|
|
16
|
+
(`chi web --tunnel tailscale`): add chi web to the Home Screen, tap the bell. An answered question's notification goes
|
|
17
|
+
when the app opens (every push shows one: iOS drops an app that pushes silently). New settings `web.push.enabled` (default on) and `web.push.contact`. See
|
|
18
|
+
docs/configuration.md (Phone notifications).
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- `chi send --new --wait` now knows the id of the turn the new session's first prompt runs, so a `--timeout` or
|
|
23
|
+
Ctrl-C there says `still running: chi send --wait --turn <T> <ID>` instead of the vaguer "waits again", and
|
|
24
|
+
`chi send --wait --turn <T> <ID>` gets that turn's reply (`--format json` carries its `turn_id`). chi web's
|
|
25
|
+
create answers the first turn's id as `first_turn_id`, so the same holds for `chi --remote … send --new --wait`.
|
|
26
|
+
- Web: a session's stream dropping mid-turn (a phone briefly offline) no longer shows "canceled (worker exited)"
|
|
27
|
+
while the worker runs on; the page re-reads first and shows it only when no worker took the turn. A re-read that
|
|
28
|
+
fails (the network still down) is retried with a backoff, and a stage that lets go of a turn clears its status row
|
|
29
|
+
and chip, so it no longer reads "thinking · step 1" for a turn it stopped following.
|
|
30
|
+
|
|
31
|
+
Update with `chi update` (the system bundle 0.111.0), then restart `chi web`.
|
|
32
|
+
|
|
33
|
+
## [0.110.0] - 2026-10-24
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- Experimental: `chi web --tunnel tailscale`: chi web on your phone anywhere on your tailnet over https (Tailscale
|
|
38
|
+
Serve on this machine's `ts.net` name, never Funnel), with your Tailscale login as the owner (or
|
|
39
|
+
`web.proxy.owner`), for this run only (nothing written to config.yml). It checks Tailscale, the certificate and
|
|
40
|
+
what Serve already has on port 443 before it changes anything, adopts a serve left by an earlier run, refuses
|
|
41
|
+
anything else on 443, prints the owner link's QR code, warns when Tailnet Lock is off, and turns the serve off on
|
|
42
|
+
exit (only while 443 still serves exactly what it set). The proxied start line (`web.public_url`) also shows its
|
|
43
|
+
link's QR code at a terminal now. See docs/configuration.md (Tailscale Serve).
|
|
44
|
+
- Sessions record when each message happened: prompts, lines merged into a turn, reminders, notes and the model's
|
|
45
|
+
replies carry `at` in the session file (never sent to the model); plugins see it in `ctx.messages`.
|
|
46
|
+
- Reminders tell the model the local time they came due (`[SYSTEM: REMINDERS DUE 2026-10-24 14:32 local]`), so it
|
|
47
|
+
can reason about how long ago something happened.
|
|
48
|
+
- Terminal (attached and the REPL): a dim `── 14:32 · 47 min later ──` divider before what comes after 10 minutes
|
|
49
|
+
or more of quiet, a `── last activity 14:32 · 47 min ago ──` line when attaching after such a gap, and the recap's
|
|
50
|
+
time in its head (`recap 14:32>`). See docs/cli.md (Time gaps).
|
|
51
|
+
- Web: a dim `── 14:32 · 47 min later ──` line in the history where 10 minutes or more of quiet passed between
|
|
52
|
+
turns, notes or reminders (live and after a reload, the participant's review page too), each prompt, answer and
|
|
53
|
+
note's full time on hover, and the recap's time in its label (`recap · 14:32`).
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- `chi send --wait` with no message on a running turn that has no turn id (a session's first prompt, a reminder
|
|
58
|
+
turn), and a `chi send --wait -m` whose message merged into the running turn, end `canceled` (`worker_stopped`)
|
|
59
|
+
when that turn's worker dies and the next worker settles it, instead of waiting until `--timeout`.
|
|
60
|
+
- Attached TUI: joining an older session (one with no saved record of the call) shows a call the guardrails denied
|
|
61
|
+
as `blocked`, not `error`.
|
|
62
|
+
|
|
63
|
+
Update with `chi update` (the system bundle 0.110.0), then restart `chi web`.
|
|
64
|
+
|
|
11
65
|
## [0.109.0] - 2026-10-24
|
|
12
66
|
|
|
13
67
|
### Added
|
|
@@ -4196,7 +4250,9 @@ and long-lived sessions.
|
|
|
4196
4250
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
4197
4251
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
4198
4252
|
|
|
4199
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
4253
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.111.0...HEAD
|
|
4254
|
+
[0.111.0]: https://github.com/dm1try/samagotchi/compare/v0.110.0...v0.111.0
|
|
4255
|
+
[0.110.0]: https://github.com/dm1try/samagotchi/compare/v0.109.0...v0.110.0
|
|
4200
4256
|
[0.109.0]: https://github.com/dm1try/samagotchi/compare/v0.108.0...v0.109.0
|
|
4201
4257
|
[0.108.0]: https://github.com/dm1try/samagotchi/compare/v0.107.0...v0.108.0
|
|
4202
4258
|
[0.107.0]: https://github.com/dm1try/samagotchi/compare/v0.106.0...v0.107.0
|
data/bin/chi
CHANGED
|
@@ -120,6 +120,10 @@ if options[:new_token] && !web
|
|
|
120
120
|
warn "Error: --new-token goes with chi web (chi web --new-token)"
|
|
121
121
|
exit 1
|
|
122
122
|
end
|
|
123
|
+
if options[:tunnel] && !web
|
|
124
|
+
warn "Error: --tunnel goes with chi web (chi web --tunnel tailscale)"
|
|
125
|
+
exit 2
|
|
126
|
+
end
|
|
123
127
|
if options[:sets] && web
|
|
124
128
|
warn "Error: --set is a new session's (chi --set, chi send --new --set), not chi web's"
|
|
125
129
|
exit 2
|
|
@@ -208,7 +212,7 @@ if options[:web]
|
|
|
208
212
|
markdown = Samagotchi::Config.get("web.markdown") if markdown.nil?
|
|
209
213
|
view = Samagotchi::Config.get("web.view")
|
|
210
214
|
exit Samagotchi::Web::Server.launch(port: port, scope: options[:scope] || "project", open_browser: options[:web_open],
|
|
211
|
-
markdown: markdown, view: view, new_token: options[:new_token],
|
|
215
|
+
markdown: markdown, view: view, new_token: options[:new_token], tunnel: options[:tunnel],
|
|
212
216
|
working_set: Samagotchi::Config.get("web.working_set"),
|
|
213
217
|
annotate_presets: Samagotchi::Config.get("web.annotate_presets"),
|
|
214
218
|
editor: Samagotchi::Config.get("web.editor"))
|
data/docs/architecture.md
CHANGED
|
@@ -153,7 +153,7 @@ agent logic and can be used without any terminal rendering; the UI is a thin lay
|
|
|
153
153
|
| UI | `Samagotchi::TerminalUI` | Interactive REPL (Reline), rendering (ANSI, spinner, status line), REPL commands. Delegates all core work to an `Engine`. |
|
|
154
154
|
| Model loops and adapters | `KernelLoop`, `LLM::ChatLoop`, `Samagotchi::Client`, `LLM::OpenAIChat`, `LLM::AnthropicMessages`, `LLM::HTTP` | The model↔tool loops and the HTTP adapters they talk through (see "Model loops and adapters"). |
|
|
155
155
|
| Bridge (SSE/HTTP) | `Samagotchi::Bridge`, `SessionManager` | The **single live client transport**: an SSE read stream + HTTP POST turn/cancel/answer surface that attaches to a worker's existing `Engine` via `Engine#subscribe`. Every session worker starts it (bound `127.0.0.1`, no auth, localhost-only). |
|
|
156
|
-
| Web (Rack) | `Samagotchi::Web::App`, `SessionManager` | Single-port `127.0.0.1:4567` control plane via `rack`+`webrick` (serve `index.html` + `/api/*`). With `web.host: lan` (or one of this machine's IPv4 addresses) the server also listens on that address, where every request but this machine's needs the access token (`App#token_gate`: the `chi_token` cookie or `Authorization: Bearer`; [CLI](cli.md#chi-web-on-your-phone)). Experimental: behind an auth proxy (`web.public_url`, `Web::ProxyGate`) every request needs the token, this machine's too, and one that came through the proxy also the configured identity header for the owner ([configuration.md](configuration.md#behind-an-auth-proxy)). A request goes through, in order: the Host check (`local_host_header?`: loopback names, the LAN address, the public host), the cross-site check (`cross_site?`: Origin this server's or the public origin, no `Sec-Fetch-Site` cross-site/same-site on a POST or `/api`), the frame check (`framed?`: a frame/embed/object load from another origin, 403 `framed`; every HTML answer carries `X-Frame-Options: DENY` and `frame-ancestors 'none'`), the token gate (`token_gate` → `principal_for`, `proxied_principal` for a proxied request; the `?token=` / `POST /api/token` trade sets the cookie, `__Host-chi_token` through the proxy; with `web.public_url` a direct request takes Bearer only, and chi's token-less `/api/info` probe gets `probe_info`), then the route's permission (`Principal#permits?`). It talks to each worker's Bridge (`App#relay`, `bridge_get`; `/stream` proxies it) and reads the session files; no `Engine` of its own. Markdown (`web.markdown`) is rendered here only (`Web::MarkdownRenderer`): the answers it serves, card bodies, and `POST /api/render` for a text the page already shows. `bin/chi web` entrypoint. |
|
|
156
|
+
| Web (Rack) | `Samagotchi::Web::App`, `SessionManager` | Single-port `127.0.0.1:4567` control plane via `rack`+`webrick` (serve `index.html` + `/api/*`). With `web.host: lan` (or one of this machine's IPv4 addresses) the server also listens on that address, where every request but this machine's needs the access token (`App#token_gate`: the `chi_token` cookie or `Authorization: Bearer`; [CLI](cli.md#chi-web-on-your-phone)). Experimental: behind an auth proxy (`web.public_url`, `Web::ProxyGate`) every request needs the token, this machine's too, and one that came through the proxy also the configured identity header for the owner ([configuration.md](configuration.md#behind-an-auth-proxy)). A request goes through, in order: the Host check (`local_host_header?`: loopback names, the LAN address, the public host), the cross-site check (`cross_site?`: Origin this server's or the public origin, no `Sec-Fetch-Site` cross-site/same-site on a POST or `/api`), the frame check (`framed?`: a frame/embed/object load from another origin, 403 `framed`; every HTML answer carries `X-Frame-Options: DENY` and `frame-ancestors 'none'`), the token gate (`token_gate` → `principal_for`, `proxied_principal` for a proxied request; the `?token=` / `POST /api/token` trade sets the cookie, `__Host-chi_token` through the proxy; with `web.public_url` a direct request takes Bearer only, and chi's token-less `/api/info` probe gets `probe_info`), then the route's permission (`Principal#permits?`). It talks to each worker's Bridge (`App#relay`, `bridge_get`; `/stream` proxies it) and reads the session files; no `Engine` of its own. Markdown (`web.markdown`) is rendered here only (`Web::MarkdownRenderer`): the answers it serves, card bodies, and `POST /api/render` for a text the page already shows. `App` keeps the gate, `ROUTES` and the helpers the handlers share; its handlers live in mixins it includes, each a `Web::` module with private methods: `SessionAccessRoutes` (`web/session_access_routes.rb`), `ParticipantAccess` (`web/participant_access.rb`), `ParticipantsAdmin` (`web/participants_admin.rb`), `ParticipantReview` (`web/participant_review.rb`), `MessageDisplay` (`web/message_display.rb`), `SessionShow` (`web/session_show.rb`), `SessionCreate` (`web/session_create.rb`), `SessionControl` (`web/session_control.rb`), `PushRoutes` (`web/push_routes.rb`) and `ProxyGate` (`web/proxy_gate.rb`). Exact paths in `GATE_FREE_PATHS` are served before the token gate (the fragment trade's script, and the Home Screen app's manifest, service worker and icons). `bin/chi web` entrypoint. |
|
|
157
157
|
| Sessions | `Samagotchi::Session`, `SessionManager` | File-based `~/.local/state/samagotchi/sessions/<uuid>.json` + sidecar `input/`/`output/`; retention (14d/500) + ordering (`updated_at desc`). |
|
|
158
158
|
|
|
159
159
|
- `bin/chi` in REPL mode (see `LaunchMode` above) builds `TerminalUI`. `TerminalUI#run` is the single
|
|
@@ -530,7 +530,7 @@ model: [internals/remote-sessions.md](internals/remote-sessions.md).
|
|
|
530
530
|
|---|---|---|---|---|
|
|
531
531
|
| `GET /api/sessions/resolve?id=` | owner | an id or a unique prefix | 200 `{id}` | 404 `not_found`; 400 `ambiguous_id` with `ids` |
|
|
532
532
|
| `GET /api/sessions/summaries` | owner | `dir` (a folder: its project; none: every project), `cwd`, `live=1`, `archived=1`, `tests=0`, `sort`, `order`, `limit` | 200: the rows `chi sessions list` prints (`SessionManager.session_summaries`: `status`, `live`, `busy`, `owner`, `recap`, `ctx_pct`, `waiting`, `waiting_id`, `title`, `preview`, …) | 400 `invalid_dir`, `invalid_cwd` |
|
|
533
|
-
| `POST /api/sessions` (more fields) | owner | `toolkit`, `tools`, `no_tools`, `parent_answers`; `client_id` (`cli:send`/`cli:answer`: the first line is checked as chi send checks it); `check_vision: true` (images follow) | 201 adds `model_warning
|
|
533
|
+
| `POST /api/sessions` (more fields) | owner | `toolkit`, `tools`, `no_tools`, `parent_answers`; `client_id` (`cli:send`/`cli:answer`: the first line is checked as chi send checks it); `check_vision: true` (images follow) | 201 adds `model_warning`, `parent_answers_warning` (a level looser than config.yml's, capped) and `first_turn_id` (the id the prompt it starts with gets for its turn; null for an `idle` create) | 400 `invalid_toolkit`, `invalid_parent_answers`, `invalid_continues` (beside `continues`); 403 `parent_answers_refused`, `parent_continue_refused`; 422 `no_vision` |
|
|
534
534
|
| `POST /api/sessions/:id/turn` (more fields) | owner | `client_id` as above; `check_vision: true` with `images` | as before | 403 `parent_answers_refused`, `parent_continue_refused`; 422 `no_vision` |
|
|
535
535
|
| `POST /api/sessions/:id/wait/prepare` | owner | `question_id` (absent: the pending one; `null`: none), `turn`, `created`, `busy` (+ `queue`) | 200 `{session_id, cursor, baseline: {messages, question_id, last_turn}, own_turn, known_turn, running, live, settled, last_turn_id}` (`WaitStart`) | 409 `busy`, `waiting` (`SendCommand::Busy`'s words, and its fields: `question`, `parent_strategy`) |
|
|
536
536
|
| `POST /api/sessions/:id/wait` | owner | `cursor`, `baseline`, `turn`, `queued`, `ended_before`, `owner_grace`, `timeout` (capped at 30 s) | 200: `ReplyWait::Result`'s fields (`status`: `done`, `waiting_for_answer`, `no_reply`, `error`, `stopped`, `worker_gone`, or `timeout`: ask again; `text`, `file`, `question` as `PendingQuestion#to_file`, `outcome`, `limit`, `error_kind`, `retryable`, `cancel_reason`, `stopped_by`, `kept_steps`, `turn_id`, `origin`; never the session) and `parent_strategy` `{name, allowed, origin}` (`ParentReport.strategy_for`, this server's config.yml) | 400 `invalid_wait`; 503 `server_stopping` |
|
|
@@ -610,9 +610,10 @@ so later requests and resumed sessions pair them, and their reasoning (`thinking
|
|
|
610
610
|
turn goes on from the run it resumes, `reasoning_since:`). It has its own system prompt
|
|
611
611
|
(no raw-prompt tool declarations; the tools go as JSON schemas).
|
|
612
612
|
|
|
613
|
-
`Engine#build_stream_event_handler`
|
|
614
|
-
|
|
615
|
-
`
|
|
613
|
+
`Engine#build_stream_event_handler` (`EventFanout`'s, which the Engine forwards
|
|
614
|
+
to) is the one seam both loops' streams pass through: with a
|
|
615
|
+
`:generation_progress` hook registered it feeds a `Hooks::StreamWatch` each
|
|
616
|
+
chunk's thinking and text after the UIs had it. A
|
|
616
617
|
hook's `stop_generation` cancels the generation's own controller (a child of
|
|
617
618
|
the turn's, `CancellationController#generation`), and each loop takes the cut
|
|
618
619
|
as an empty answer and asks again.
|
|
@@ -700,7 +701,9 @@ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/
|
|
|
700
701
|
| 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)). |
|
|
701
702
|
| 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)). |
|
|
702
703
|
| Review sessions | `review_start.rb` (`ReviewStart`), `session_review.rb` (`SessionReview`, the session's `review` field), `review_command.rb` (`ReviewCommand`, `chi review`), `review_apply.rb` (`ReviewApply`) | A session made to be shared: idle, its subjects project files checked by `Guardrails::ReviewPaths`, a fork of a working session from its recap (`--from`); Apply quotes a participant's review turn into the owner's composer ([sessions.md](sessions.md#review-sessions), [internals/participants.md](internals/participants.md)). |
|
|
703
|
-
|
|
|
704
|
+
| chi web's routes | `web/app.rb` (`Web::App`: the gate, `ROUTES`, the shared helpers), `web/message_display.rb` (`Web::MessageDisplay`: saved messages and cards as display items, `POST /api/render`), `web/session_show.rb` (`Web::SessionShow`: `GET /api/sessions/:id` with its `cards`, `tail` and `timing` re-reads, `GET …/output`, the timing payload), `web/session_create.rb` (`Web::SessionCreate`: `POST /api/sessions`, a continue, a review fork), `web/session_control.rb` (`Web::SessionControl`: answer, command, turn and images, cancel, stop, restart, archive, delete), `web/push_routes.rb` (`Web::PushRoutes`: `/api/push`, its subscriptions and test) | The handlers `App`'s `ROUTES` name, in the file that holds their kind; each mixin is included in `App` and reads its helpers and ivars, so `app.send(:handle_show, …)` and the specs that stub `App` methods work as before. |
|
|
705
|
+
| Web Push | `web/push.rb` (`Web::Push`: `Runtime`, `preflight!`, the `web.push.*` settings), `web/push/crypto.rb` (`Push::Crypto`: RFC 8291 aes128gcm, RFC 8292 VAPID), `web/push/store.rb` (`Push::Store`, `Store::Sub`: `web-push/vapid.pem`, `subscriptions.json`), `web/push/client.rb` (`Push::Client`, `Client::Result`), `web/push/attention.rb` (`Push::Attention`: notify.js's rules in Ruby), `web/push/notifier.rb` (`Push::Notifier`, a hub subscriber with a sender thread), `web/push_routes.rb` (`Web::PushRoutes`), `web/private_files.rb` (`Web::PrivateFiles`, shared with the grants), `public/sw.js`, `public/push.js`, `public/manifest.webmanifest` | chi web pushes approvals and questions to the owner's Home Screen app over an https public URL: the Notifier turns the hub's new approvals and questions into show pushes after a hold (each carries the keys still open, and the badge; no close push: every push shows a notification); the bell (`notifications.js`) subscribes the device, and the service worker and the page close the answered ones. See [internals/web-push.md](internals/web-push.md). |
|
|
706
|
+
| Behind an auth proxy | `web/public_url.rb` (`Web::PublicUrl`: `web.public_url`, `web.proxy.identity_header`, `web.proxy.owner`), `web/proxy_gate.rb` (`Web::ProxyGate`, mixed into App; `RawHeaderNames`, prepended to WEBrick's request by `Web::Server`), `web/link_codes.rb` (`Web::LinkCodes`: the links' one-time codes), `proxy_trace.rb` (`ProxyTrace`), `web/public/token_fragment.js`, `web/public/auth_lost.js`, `web/tunnel.rb` + `web/tunnel/tailscale.rb` (`Web::Tunnel`, `Web::Tunnel::Tailscale`: `chi web --tunnel tailscale`, Tailscale Serve set for one run and the PublicUrl it makes) | Experimental: chi web behind an auth proxy. Every request needs the token; a proxied one's owner also needs the identity header (one clean value under one name); the `__Host-` cookie, an absolute `Location` from the public URL, https links with a one-time code in the fragment; the page's lost-sign-in banner. Bridges refuse proxy traces ([configuration.md](configuration.md#behind-an-auth-proxy)). |
|
|
704
707
|
| Participants | `participants.rb` (`Participants`, the `participants.json` sidecar), `share_command.rb` (`ShareCommand`, `chi share`), `session_participants.rb`, `participants_watch.rb`, `web/grants.rb` (`Web::Grants`), `web/principal.rb` (`Web::Principal`), `web/participant_access.rb` (`Web::ParticipantAccess`: the participant's gate and routes), `web/participants_admin.rb` (`Web::ParticipantsAdmin`: the owner's invite, list, revoke, cap and Apply routes), `web/participant_review.rb` + `web/participant_view.rb` (`Web::ParticipantReview`, `Web::ParticipantView`), `web/participant_stream_filter.rb`, `guardrails/review_boundary.rb` + `guardrails/review_paths.rb` | The people or agents an owner invites to review one session through chi web: their grants and gate, what they read, their read-only review turns ([internals/participants.md](internals/participants.md), [guardrails.md](guardrails.md)). |
|
|
705
708
|
| 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); `session_metrics/cost.rb` (`SessionMetrics::Cost`): the spend `ctx.cost` reads; `session_metrics/usage.rb` (`SessionMetrics::Usage`): the work `ctx.usage` reads. `session_metrics/side_totals.rb` (`SessionMetrics::SideTotals`) and `side_usage.rb` (`SideUsage`): the side requests' sums, apart from the turns'. |
|
|
706
709
|
| Images / vision | `image_store.rb` (`ImageStore`), `vision_support.rb`, `vision_context.rb` | Stored prompt images and whether a model takes them. |
|
data/docs/cli.md
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
- A session id can be shortened to any unique prefix (like git): `chi --attach 2ea8`. `--resume`, `--attach`, `sessions stop`, `sessions archive` and `sessions delete` take one; an ambiguous prefix lists the sessions it matches.
|
|
14
14
|
- `chi web [--port 4567] [--open] [--scope=all]` — start the Web UI (single localhost port session control plane) on this git project's sessions (`--scope=all`, or a folder in no repo: every session); if a chi web already runs on the port, print (with `--open`, open) its page for this folder and exit. Something else on the port (an older chi web too) exits 1 with "port N is in use"
|
|
15
15
|
- `chi web --web-host lan` — the Web UI on your home network too, for your phone: a link with an access token and its QR code (see [chi web on your phone](#chi-web-on-your-phone)); `chi web --new-token` replaces the token
|
|
16
|
+
- `chi web --tunnel tailscale` — the Web UI over https on your tailnet, for your phone anywhere: Tailscale Serve (never Funnel) on this machine's `<machine>.<tailnet>.ts.net` name for this run, the owner your Tailscale login, turned off on exit (see [Tailscale Serve](configuration.md#tailscale-serve-your-tailnet)); `--tunnel` without `web` exits 2
|
|
16
17
|
- `chi web --web-markdown` — opt in to sanitized Markdown rendering for completed assistant messages, the stage's asking text and a resolved question card's reply
|
|
17
18
|
- `chi web --web-view turn` — draw turns with the turn view (each turn as one block of steps, the running one at the bottom of the history) instead of the default stage view (the running turn pinned above the composer); `?view=stage|turn` on the page URL overrides it (see [Web views](#web-views))
|
|
18
19
|
- `chi web --no-web-working-set` — turns the stage view's working set off (on by default; `--web-working-set` turns a config's `web.working_set: false` back on): what each turn brought into the model's context, in the stage and under each finished turn; `?working_set=1|0` on the page URL overrides it (see [The working set](#the-working-set))
|
|
@@ -1012,6 +1013,16 @@ steps saved before that have none, so they show no thinking. An `edit` or
|
|
|
1012
1013
|
made (up to 120 lines or 8 KB), live and after a reload (see
|
|
1013
1014
|
[Guardrails](guardrails.md#ask) for the diff an approval shows first).
|
|
1014
1015
|
|
|
1016
|
+
**When things happened**, in both views and on a participant's review page: where 10 minutes or
|
|
1017
|
+
more of quiet passed between the end of one activity and the start of the next (a turn, a context
|
|
1018
|
+
note, a reminder), the history has one dim centred line, `── 14:32 · 47 min later ──`; the date is
|
|
1019
|
+
added when the day changed or the turn wasn't today (`── Sat 24 Oct 09:10 · 13 h later ──`).
|
|
1020
|
+
Nothing marks active work: a long turn followed at once by the next has no line. Hovering a prompt,
|
|
1021
|
+
an answer, a note or a reminder shows its full local time, and the recap's label says when it was
|
|
1022
|
+
written (`recap · 14:32`). The times are the history entries' `at` (see
|
|
1023
|
+
[sessions.md](sessions.md)); a session saved before them takes its turns' times from their timing
|
|
1024
|
+
records, and shows no hover times.
|
|
1025
|
+
|
|
1015
1026
|
### The working set
|
|
1016
1027
|
|
|
1017
1028
|
The stage view shows what a turn brought into the model's context: the files
|
|
@@ -1164,6 +1175,12 @@ access token", paste the token there (the part of the link after
|
|
|
1164
1175
|
office guest network), and run `chi web --new-token` if a link leaks.
|
|
1165
1176
|
- On http the browser has no notifications: the bell is hidden on the phone,
|
|
1166
1177
|
and the tab title still counts what needs you.
|
|
1178
|
+
- Off the home Wi-Fi: `chi web --tunnel tailscale` gives chi web an https
|
|
1179
|
+
name on your tailnet (Tailscale Serve) for its run and prints its link's QR
|
|
1180
|
+
code; see [Tailscale Serve](configuration.md#tailscale-serve-your-tailnet).
|
|
1181
|
+
- Notifications on the phone, also while chi web isn't open there: on the tunnel's https link, Add to Home Screen,
|
|
1182
|
+
open the Chi app and tap its bell; approvals and questions then push to the phone (see
|
|
1183
|
+
[Phone notifications](configuration.md#phone-notifications-web-push)).
|
|
1167
1184
|
|
|
1168
1185
|
### Remote sessions
|
|
1169
1186
|
|
|
@@ -1332,7 +1349,10 @@ chi share revoke --all # every link to every session
|
|
|
1332
1349
|
- `chi --remote studio share …` invites on studio: the link names studio's LAN address. Locally, the link names
|
|
1333
1350
|
this machine's running chi web, on the port it listens on (it records it in `$XDG_STATE_HOME/samagotchi/web.json`;
|
|
1334
1351
|
`web.port` when none answers there); without one on the LAN, chi says the link opens on this machine only (as
|
|
1335
|
-
the participant there too: a participant's link never opens as the owner).
|
|
1352
|
+
the participant there too: a participant's link never opens as the owner). With `web.public_url` the link is
|
|
1353
|
+
`https://<public url>/#code=…`; with `chi web --tunnel tailscale` too (its `ts.net` name, which web.json records
|
|
1354
|
+
while that chi web answers on its port: a chi web on another port takes web.json over, and the link falls back to
|
|
1355
|
+
127.0.0.1). A tunnel link opens only on a device on your tailnet; the grant still decides.
|
|
1336
1356
|
- The owner's own token and the `chi web --new-token` rotation are separate: rotating it ends no participant's link.
|
|
1337
1357
|
- **In the attached terminal.** A participant's message reads `Alex ▸ review> is step 3 right?`, the name in that
|
|
1338
1358
|
participant's own colour (queued, running, and the last exchange when you join); yours stay as they were. A line
|
|
@@ -1433,6 +1453,28 @@ working on, what came of it and what is still open, not a turn-by-turn log.
|
|
|
1433
1453
|
- It uses the session's own model unless `recap:` names one;
|
|
1434
1454
|
`recap: false` turns it off. See configuration.md.
|
|
1435
1455
|
|
|
1456
|
+
## Time gaps (terminal)
|
|
1457
|
+
|
|
1458
|
+
Both terminals (attached and the REPL) show when things happened without a
|
|
1459
|
+
time on every line:
|
|
1460
|
+
|
|
1461
|
+
- A dim divider marks a quiet gap of 10 minutes or more, before what comes
|
|
1462
|
+
next: `── 14:32 · 47 min later ──`, the date added when the day changed
|
|
1463
|
+
(`── Sat 24 Oct 09:10 · 13 h later ──`). The gap runs from the end of the
|
|
1464
|
+
last activity (a turn's end, a reminder, a context note, a recap) to the
|
|
1465
|
+
start of the next (a prompt, yours above its line or another UI's, a
|
|
1466
|
+
reminder turn, a note, a recap), so nothing is marked during a turn or
|
|
1467
|
+
between turns run back to back. A command (`/stats`, `!cmd`) is no
|
|
1468
|
+
activity.
|
|
1469
|
+
- Attaching to a session whose last activity was 10 minutes ago or more
|
|
1470
|
+
says when, after the join block: `── last activity 14:32 · 47 min ago ──`
|
|
1471
|
+
(from the newest timed message, else the saved recap's time). With
|
|
1472
|
+
`--resume` the REPL marks the gap before the first prompt instead.
|
|
1473
|
+
- A recap names the local time it was written: `recap 14:32>`, `recap 14:32
|
|
1474
|
+
(before the last 2 turns)>`.
|
|
1475
|
+
- Only live activity and the join: neither terminal replays the history, so
|
|
1476
|
+
older gaps aren't marked. `--non-interactive` prints none.
|
|
1477
|
+
|
|
1436
1478
|
## Tool Activity Log
|
|
1437
1479
|
|
|
1438
1480
|
Samagotchi now prints a concise, human-friendly tool activity log in normal
|
data/docs/configuration.md
CHANGED
|
@@ -1591,8 +1591,9 @@ web:
|
|
|
1591
1591
|
- Only a viewer on this machine opens the editor: a loopback request, or this machine on its own LAN address
|
|
1592
1592
|
(`web.host: lan`, the LAN URL or its QR code opened here). A phone gets the targets that need no editor (an
|
|
1593
1593
|
answer's ref shows its row in the turn; see the refs in [sessions.md](sessions.md)); a row's title stays plain
|
|
1594
|
-
text there. An SSH tunnel or a local reverse proxy (`tailscale serve`) looks like loopback
|
|
1595
|
-
|
|
1594
|
+
text there. An SSH tunnel or a local reverse proxy (`tailscale serve`) looks like loopback unless `web.public_url`
|
|
1595
|
+
is set (then a request through it is never this machine's viewer), so without it a remote viewer behind one gets an
|
|
1596
|
+
editor target that can't open.
|
|
1596
1597
|
- `GET /api/sessions/:id` (and its `?tail=1`) carries `capabilities.editor` (true for such a viewer) and
|
|
1597
1598
|
`editor_url`, the template (null otherwise).
|
|
1598
1599
|
|
|
@@ -1668,12 +1669,146 @@ one.
|
|
|
1668
1669
|
| nginx `auth_request` + oauth2-proxy (`--set-xauthrequest`) | `X-Auth-Request-Email` | Only as nginx sets it from the auth reply (`auth_request_set $email $upstream_http_x_auth_request_email; proxy_set_header X-Auth-Request-Email $email;`), which replaces a client's copy; nginx drops underscore spellings by default. Checked with nginx 1.27. |
|
|
1669
1670
|
| Caddy `forward_auth` (`copy_headers X-Auth-Request-Email`) | the copied header | Replaced from the auth reply; Caddy passes an underscore spelling through, which chi refuses. Checked with Caddy 2.8. |
|
|
1670
1671
|
| Cloudflare Access | `Cf-Access-Authenticated-User-Email` | Not checked yet: verify with an echo. chi trusts the header only; it doesn't validate the `Cf-Access-Jwt-Assertion` token. |
|
|
1671
|
-
| Tailscale Serve | `Tailscale-User-Login` |
|
|
1672
|
+
| Tailscale Serve | `Tailscale-User-Login` | Replaced, not appended (`Tailscale-User-Name`, `X-Forwarded-For`, `X-Forwarded-Host` too); `X-Forwarded-For` = the requesting device's tailnet IP, Host = the public name. Tailnet only. Checked with Tailscale 1.104.2 (macOS). Not checked: Funnel and tagged-device requests; never use Funnel. See [Tailscale Serve](#tailscale-serve-your-tailnet). |
|
|
1672
1673
|
| Authelia, Authentik (behind nginx, Caddy, Traefik) | `Remote-Email` (Authelia), `X-Authentik-Email` (Authentik) | Not checked yet; only as the front proxy copies it from the auth reply (the nginx and Caddy rows). |
|
|
1673
1674
|
| A TLS proxy with no sign-in (nginx, Caddy, an SSH tunnel) | leave unset | The token alone decides. |
|
|
1674
1675
|
|
|
1675
1676
|
Keep the state dir (`web-token`, the grants) out of any folder others can read, such as a team's shared folder.
|
|
1676
1677
|
|
|
1678
|
+
### Tailscale Serve (your tailnet)
|
|
1679
|
+
|
|
1680
|
+
chi web on your phone away from the home Wi-Fi: [Tailscale](https://tailscale.com) Serve gives this Mac an https
|
|
1681
|
+
name on your tailnet, `https://<machine>.<tailnet>.ts.net`, with a real certificate, and tells chi web who is asking
|
|
1682
|
+
(your Tailscale login, next to chi's token). Only devices on your tailnet reach it: with Tailscale off on the phone the
|
|
1683
|
+
name doesn't load. By hand, on the Mac that runs chi web (or steps 1–2, then
|
|
1684
|
+
[`chi web --tunnel tailscale`](#chi-web---tunnel-tailscale) does 3–5 for one run):
|
|
1685
|
+
|
|
1686
|
+
1. Install Tailscale on the Mac and the phone and sign both in with the same login. **Rename the machines first**
|
|
1687
|
+
(admin console → Machines): HTTPS certificates go into the public Certificate Transparency log, so the
|
|
1688
|
+
`<machine>.<tailnet>.ts.net` name is public once a certificate is issued, and a later rename puts a new name there.
|
|
1689
|
+
2. Turn on HTTPS certificates (admin console → DNS; they need MagicDNS on).
|
|
1690
|
+
3. Serve chi web's port on the tailnet, never on the internet:
|
|
1691
|
+
|
|
1692
|
+
```sh
|
|
1693
|
+
tailscale serve --bg 4567
|
|
1694
|
+
```
|
|
1695
|
+
|
|
1696
|
+
On macOS the app's CLI is `/Applications/Tailscale.app/Contents/MacOS/Tailscale` unless you installed `tailscale`
|
|
1697
|
+
on your PATH. `tailscale serve status` shows what it serves; `tailscale serve --https=443 off` turns it off. Turn
|
|
1698
|
+
it off when you stop using it: while it is on, whatever listens on 127.0.0.1:4567 is reachable from your tailnet.
|
|
1699
|
+
4. Tell chi web its public name and whose login is the owner (config.yml; `chi web` reads it at start):
|
|
1700
|
+
|
|
1701
|
+
```yaml
|
|
1702
|
+
web:
|
|
1703
|
+
public_url: https://chi-host.example-tailnet.ts.net
|
|
1704
|
+
proxy:
|
|
1705
|
+
identity_header: Tailscale-User-Login
|
|
1706
|
+
owner: owner@example.com # your Tailscale login
|
|
1707
|
+
```
|
|
1708
|
+
|
|
1709
|
+
Your login: `tailscale whois <the phone's 100.x IP>` (the User block), or `tailscale status --json`
|
|
1710
|
+
(`User["<Self.UserID>"].LoginName`), or start without `owner` and read it off the 401 page's detail ("owner token
|
|
1711
|
+
with identity …"). A login made with Sign in with Apple is a private-relay address (`…@privaterelay.appleid.com`):
|
|
1712
|
+
it is stable, use it as it is. Every device signed in with that login (the phone, a laptop, an iPad) is the owner,
|
|
1713
|
+
each with the token too.
|
|
1714
|
+
5. `chi web` prints `Proxied: https://chi-host.example-tailnet.ts.net/#code=chc_…`; open that link on the phone (copy,
|
|
1715
|
+
AirDrop or type it). The code works once, within 10 minutes (`chi web` again prints a new one); the phone then
|
|
1716
|
+
keeps the `__Host-chi_token` cookie.
|
|
1717
|
+
|
|
1718
|
+
#### `chi web --tunnel tailscale`
|
|
1719
|
+
|
|
1720
|
+
Steps 3–5 in one command, after steps 1–2 (Tailscale signed in, HTTPS certificates on):
|
|
1721
|
+
|
|
1722
|
+
```sh
|
|
1723
|
+
chi web --tunnel tailscale
|
|
1724
|
+
```
|
|
1725
|
+
|
|
1726
|
+
```
|
|
1727
|
+
Chi Web on http://127.0.0.1:4567/ (public: …)
|
|
1728
|
+
Proxied: https://chi-host.example-tailnet.ts.net/#code=chc_… ← opens chi web as you once, within 10 minutes (…)
|
|
1729
|
+
(owner owner@example.com by Tailscale-User-Login, and the token; …)
|
|
1730
|
+
via Tailscale Serve (tailnet only), owner owner@example.com (this machine's Tailscale login)
|
|
1731
|
+
Tailnet Lock is off: a device Tailscale's servers add to your tailnet could reach this page; tailscale lock init turns it on (…)
|
|
1732
|
+
<the link's QR code>
|
|
1733
|
+
```
|
|
1734
|
+
|
|
1735
|
+
- It checks before it changes anything: Tailscale is connected, this machine has its `ts.net` name and a certificate
|
|
1736
|
+
for it, and what Serve already has on port 443. Nothing there: it runs
|
|
1737
|
+
`tailscale serve --bg --https=443 http://127.0.0.1:<port>`. Exactly that one handler for this port (a leftover of an
|
|
1738
|
+
earlier run killed hard, or step 3 done by hand): it adopts it. Anything else on 443 (another port, other paths, TCP
|
|
1739
|
+
forwarding, Funnel) is refused and left alone. Serve's other ports are never touched.
|
|
1740
|
+
- The owner is `web.proxy.owner` if config.yml has it, else this machine's own Tailscale login (a tagged machine has
|
|
1741
|
+
none: set `web.proxy.owner`). `public_url` and `identity_header` are the tunnel's for this run, never written to
|
|
1742
|
+
config.yml: a `web.public_url` or `web.proxy.identity_header` that says otherwise (in config.yml or the environment)
|
|
1743
|
+
is refused, so a config from step 4 with this machine's name works as it is.
|
|
1744
|
+
- On exit (Ctrl-C, a kill, closing the terminal) it runs `tailscale serve --https=443 off`, but only while 443 still
|
|
1745
|
+
serves exactly what it set; if you changed 443 meanwhile it leaves it and says so. Killed hard (`kill -9`, a power
|
|
1746
|
+
loss) the serve stays on: Serve answers 502 for the name while nothing listens on the port, and whatever later
|
|
1747
|
+
listens on 127.0.0.1:<port> is reachable from your tailnet; the next `chi web --tunnel tailscale` adopts it and
|
|
1748
|
+
turns it off on exit, a plain `chi web` doesn't look.
|
|
1749
|
+
- A chi web already on the port is refused ("stop that one first"): `--tunnel` starts its own.
|
|
1750
|
+
- The `tailscale` it runs: `SAMAGOTCHI_TAILSCALE_CLI` if set, else `tailscale` on PATH, else the macOS app's
|
|
1751
|
+
`/Applications/Tailscale.app/Contents/MacOS/Tailscale`.
|
|
1752
|
+
- Tailnet Lock off is a warning line, never a refusal.
|
|
1753
|
+
|
|
1754
|
+
- **Never Funnel** (`tailscale funnel`): it puts the name on the public internet. This recipe was checked on the
|
|
1755
|
+
tailnet only; Funnel requests weren't.
|
|
1756
|
+
- Recommended: **Tailnet Lock** (`tailscale lock init`, see Tailscale's docs): new devices then need a signature from a
|
|
1757
|
+
device you trust before they join, not only Tailscale's coordination server's say-so. chi web works either way.
|
|
1758
|
+
- What Serve sends (checked with Tailscale 1.104.2 on macOS): it **replaces** a client's `Tailscale-User-Login`,
|
|
1759
|
+
`Tailscale-User-Name`, `X-Forwarded-For` and `X-Forwarded-Host` (never appends); `X-Forwarded-For` is the
|
|
1760
|
+
requesting device's tailnet IP, Host the public name, and chi web's peer is 127.0.0.1. A process on this Mac can
|
|
1761
|
+
still reach 127.0.0.1:4567 directly with headers of its own: there the token decides, as with any proxy.
|
|
1762
|
+
- While chi web isn't running Serve answers 502 for the name (checked). Not checked yet: requests from a tagged
|
|
1763
|
+
device (expect no user login, so refused with `identity_header` set), and whether the serve stays on across a
|
|
1764
|
+
reboot.
|
|
1765
|
+
- A participant's link (`chi share`, the Invite dialog) is a `https://<machine>.<tailnet>.ts.net/…` link too: it opens
|
|
1766
|
+
only on a device on your tailnet (or one you shared this machine with in Tailscale); the grant still decides.
|
|
1767
|
+
- Without Serve, a plain tailnet IP (`web.host: 100.x.y.z`) works like the LAN (see
|
|
1768
|
+
[chi web on your phone](cli.md#chi-web-on-your-phone)): token only, plain http in the browser (WireGuard encrypts
|
|
1769
|
+
the path) so no `Secure` cookie and no notifications, and no identity.
|
|
1770
|
+
|
|
1771
|
+
### Phone notifications (Web Push)
|
|
1772
|
+
|
|
1773
|
+
chi web as an app on your iPhone's Home Screen that buzzes (the Watch too) when a session waits on an **approval** or
|
|
1774
|
+
a **question**, also while the app is closed. A tap opens that session; once it is answered anywhere (the Mac, the
|
|
1775
|
+
phone, `chi answer`), its notification goes when you open the app (or swipe it away). Every
|
|
1776
|
+
push shows a notification: iOS turns off an app's pushes after a few that show none.
|
|
1777
|
+
|
|
1778
|
+
What it needs:
|
|
1779
|
+
|
|
1780
|
+
- chi web on an **https** public URL: `chi web --tunnel tailscale` ([above](#chi-web---tunnel-tailscale)) or a
|
|
1781
|
+
`web.public_url`. On the LAN's plain http there is no push (no secure context: the bell is hidden); use the tunnel.
|
|
1782
|
+
- iOS 16.4 or later, and chi web **added to the Home Screen**: iOS gives push to a Home Screen app only.
|
|
1783
|
+
- No Apple account or registration: chi web signs its pushes with a key of its own (VAPID).
|
|
1784
|
+
|
|
1785
|
+
Turning it on, on the phone: open the `Proxied:` link in Safari → Share → **Add to Home Screen** → open the **Chi**
|
|
1786
|
+
app → tap the **bell** at the top → Allow. A test notification arrives at once. Tap the
|
|
1787
|
+
bell again to turn it off on that phone. In a browser tab the bell stays the tab's own notifications (while the tab
|
|
1788
|
+
is in the background), as before.
|
|
1789
|
+
|
|
1790
|
+
- **What is sent**: the session's name and the reason ("needs approval", "needs an answer"), never the question or
|
|
1791
|
+
the command, as the tab's notification on the Mac. The session's name shows on the lock screen and the Watch.
|
|
1792
|
+
- **What Apple sees**: the push is end-to-end encrypted to the phone; Apple's push service sees that this machine
|
|
1793
|
+
pushes to that device, and when. A subscription holds no device name and no personal data (an opaque address on
|
|
1794
|
+
`web.push.apple.com` and two keys); chi names it itself ("iPhone · 2026-10-24", from the User-Agent's device class
|
|
1795
|
+
and the date).
|
|
1796
|
+
- **When**: an approval or a question that is still open 3 s after it was asked (3.5 s for a delegated child's
|
|
1797
|
+
approval, which its parent may relay instead); one push per question; a step-limit question, a failed or a long
|
|
1798
|
+
turn don't push yet. A push arrives even while you look at the page (the phone can't tell chi web that it is in
|
|
1799
|
+
front yet).
|
|
1800
|
+
- **The files**: `$XDG_STATE_HOME/samagotchi/web-push/` (next to `web-token`): `vapid.pem` (chi web's key) and
|
|
1801
|
+
`subscriptions.json` (the devices), 0600 in a 0700 folder; chi web refuses to start when others can read them.
|
|
1802
|
+
Subscriptions belong to the public URL they were made on: after renaming the machine or the tailnet, turn the bell
|
|
1803
|
+
on again on the phone (the old ones are never sent to).
|
|
1804
|
+
- `web.push.enabled: false` turns it off; `web.push.contact` (a `mailto:` address or an `https://` URL; default:
|
|
1805
|
+
the public URL) is the contact the push services see in chi web's key. A value that is neither stops `chi web`
|
|
1806
|
+
(Apple refuses it); a `[chi web] the push service refused chi's VAPID token` line means the contact or the key was
|
|
1807
|
+
refused, and pushes stop until chi web restarts.
|
|
1808
|
+
- The app keeps its own cookie (iOS copies Safari's when it is added, 400 days): `chi web --new-token` logs it out
|
|
1809
|
+
(it shows the "needs chi web's access token" page; paste the token there). Pushes keep coming meanwhile.
|
|
1810
|
+
- chi web not running: no pushes (Serve answers 502; a tap couldn't open the page anyway).
|
|
1811
|
+
|
|
1677
1812
|
## All settings
|
|
1678
1813
|
|
|
1679
1814
|
Every setting below takes the three forms described in
|
|
@@ -1804,6 +1939,8 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
|
|
|
1804
1939
|
| `web.public_url` | | | Experimental. The https origin people open `chi web` at through an auth proxy (`https://host` or `https://host:port`, no path); set, every request needs the access token, this machine's too, and links use it. A value that isn't an https origin stops `chi web` from starting. See [Behind an auth proxy](#behind-an-auth-proxy). |
|
|
1805
1940
|
| `web.proxy.identity_header` | | | Experimental. The request header the proxy puts the signed-in user's email in (`X-Forwarded-Email` for oauth2-proxy), checked next to the token: only one the proxy sets *and* replaces. Unset or `none`: the token alone decides. See [Behind an auth proxy](#behind-an-auth-proxy). |
|
|
1806
1941
|
| `web.proxy.owner` | | | Experimental. Your email as the identity header carries it (any case): a proxied request is the owner only with this identity and the token. Unset: nobody is the owner by identity. Needs `web.proxy.identity_header`. |
|
|
1942
|
+
| `web.push.enabled` | `true` | | Push notifications to a phone's Home Screen app (approvals and questions) when the bell there is on. Does anything only with an https public URL (`web.public_url`, or `chi web --tunnel tailscale`'s) and once a device subscribed; `false` turns the pushes and the routes off. See [Phone notifications](#phone-notifications-web-push). |
|
|
1943
|
+
| `web.push.contact` | the public URL | | The contact the push services see in chi web's VAPID token: a `mailto:` address or an `https://` URL. Anything else stops `chi web` from starting (Apple refuses it). |
|
|
1807
1944
|
| `history.file` | state dir | | Prompt history path, shared by the TUI and the web composer. |
|
|
1808
1945
|
| `no_interrupt` | `false` | `--no-interrupt` | Raise the step limit of a turn to 1000 (or `turn.max_iterations`, when larger); a top-level key. |
|
|
1809
1946
|
| `no_default_input` | `false` | `--no-default-input` | Don't pre-fill `default.input`; a top-level key. |
|
data/docs/internals/engine.md
CHANGED
|
@@ -25,12 +25,14 @@ Engine#run_turn(session, prompt, on_event:, max_iterations:, cancel_controller:,
|
|
|
25
25
|
│ # TurnState#begin!(controller:, sink:) (a new CancellationController when none is given),
|
|
26
26
|
│ # a recap in flight invalidated, GuardrailWiring#begin_turn(origin)
|
|
27
27
|
│ # Returns a Turn {session, prompt (nil on a continue), continue, on_event, controller,
|
|
28
|
-
│ # origin, started_at, id (the caller's id:, else a new uuid), resumed, resume_from
|
|
28
|
+
│ # origin, started_at, id (the caller's id:, else a new uuid), resumed, resume_from,
|
|
29
|
+
│ # wall (MessageTime.now)}
|
|
29
30
|
│
|
|
30
31
|
├── Client.swap_probe_cancel(turn.controller) # a Stop cuts this turn's /props probes
|
|
31
32
|
│
|
|
32
33
|
├── prepare_turn(turn, images)
|
|
33
|
-
│ # Emits :turn_started {session_id, prompt, turn_id,
|
|
34
|
+
│ # Emits :turn_started {session_id, prompt, turn_id, at (the turn's wall time, Turn#wall: read once,
|
|
35
|
+
│ # the user message's and the reminders' stamp too), continue: true (only on a continue),
|
|
34
36
|
│ # resumed: true (only on a step-limit Continue: resumed: given),
|
|
35
37
|
│ # images (only when there are any), cut_after (the model's cut wait, s)}
|
|
36
38
|
│ # refresh_profile! (may ask the server's /props once)
|
|
@@ -91,7 +93,7 @@ StandardError in run_turn:
|
|
|
91
93
|
→ if turn.ended (it came from the after_turn/session_end hooks): logged as post_turn_error, re-raised
|
|
92
94
|
→ else: end_turn(turn, "failed", save: true) (a save that fails there is logged, turn_end_save_failed)
|
|
93
95
|
# Both loops call LLM::FailedTurn.attach(e, conversation) before re-raising, so
|
|
94
|
-
# failed_messages keeps the loop's partial conversation (tool iterations included),
|
|
96
|
+
# failed_messages (TurnEndings) keeps the loop's partial conversation (tool iterations included),
|
|
95
97
|
# else turn.messages, with TurnNote.failed(summary, continued: turn.continue) at the tail.
|
|
96
98
|
# :turn_failed {error_class, message, duration_ms}; a provider error adds
|
|
97
99
|
# error_kind, retryable, host and summary.
|
|
@@ -155,7 +157,7 @@ Stopped before the model was asked (prepare_turn returned false):
|
|
|
155
157
|
The Engine does not talk to models directly. `Engine#backend` picks the loop the effective model's host speaks:
|
|
156
158
|
|
|
157
159
|
```
|
|
158
|
-
Engine#backend → backend_for(effective_model) # EffectiveModel.of(@host_registry, @
|
|
160
|
+
Engine#backend → backend_for(effective_model) # EffectiveModel.of(@host_registry, @model_binding.choice)
|
|
159
161
|
→ host entry chat? (an api in LLM::Adapters::BY_API: openai, anthropic): LLM::ChatLoop (one per Engine, built on first use)
|
|
160
162
|
→ else: LLM::NativeBackend wrapping the Engine's KernelLoop
|
|
161
163
|
```
|
|
@@ -197,7 +199,9 @@ KernelLoop#run(messages, max_iterations:, on_stream_event:, cancel_controller:,
|
|
|
197
199
|
|
|
198
200
|
`turn.conversation` is the loop's own copy, not the Engine's `turn.messages`. A failure attaches it to the error
|
|
199
201
|
(`LLM::FailedTurn.attach`), so `Engine#failed_messages` can keep it; an Interrupt (Ctrl-C) too, for
|
|
200
|
-
`Engine#ctrl_c_messages` (see Error paths).
|
|
202
|
+
`Engine#ctrl_c_messages` (see Error paths). Both are `TurnEndings`', with the other pure builders of a turn's end
|
|
203
|
+
(`kept_messages`, `failed_event`, `turn_summary`, `stop_facts`); the Engine forwards to it and keeps `end_turn`,
|
|
204
|
+
`complete_turn`, `store_answer_display` and `record_last_turn`.
|
|
201
205
|
|
|
202
206
|
### ChatLoop (the chat APIs)
|
|
203
207
|
|
|
@@ -395,13 +399,14 @@ what a resolve gives, all without a `/model`. Frozen, so any thread may hold one
|
|
|
395
399
|
- The Engine's readers take one (`Engine#effective_model`); those whose fallback always covered the resolve go
|
|
396
400
|
through `SessionModelSettings#model_setting` (`session_model_settings.rb`: the Engine forwards its readers of the session's settings there). A turn takes one in `turn_settings` (before the hooks) and one in `generate` (after
|
|
397
401
|
them); `system_prompt`, `steer_cut_after` (at `:turn_started`) and the turn-end warm-up take their own;
|
|
398
|
-
`stats_snapshot` and `session_state_snapshot` take one each (the latter nil-tolerant
|
|
402
|
+
`stats_snapshot` and `session_state_snapshot` take one each (the latter nil-tolerant; both `EngineSnapshots`',
|
|
403
|
+
`engine_snapshots.rb`, which the Engine forwards to).
|
|
399
404
|
- Never resolved (they hold on a broken resolve): `effective_model_ref`, `bare_model_name`,
|
|
400
405
|
`guardrail_model_name`, `sync_model_key!`, `served_model(probe: false)` (`HostRegistry#model_ref`/`#bare_name`).
|
|
401
406
|
- Readers on other threads: the Bridge (`/state`, `/stats`, the SSE reset, a steer's cut wait), the worker's
|
|
402
407
|
command thread, the recap thread and plugin threads (`session_model_recap_target`), the REPL.
|
|
403
|
-
- `ModelChoice` (
|
|
404
|
-
never pairs one model's name with another's typed name.
|
|
408
|
+
- `ModelChoice` (`ModelBinding#choice`) holds the name and the typed name as one value, so a reader on another
|
|
409
|
+
thread never pairs one model's name with another's typed name.
|
|
405
410
|
- `EffectiveModel.of_session` builds a saved session's (no worker): `Web::App`, `LLMContextStrategy.for_session`.
|
|
406
411
|
|
|
407
412
|
`SessionSetting` names the four per-session settings (llm_context, thinking, reasoning_history, toolkit): the
|
|
@@ -409,6 +414,21 @@ session's own value, the commands that move each chip, its chip from an Effectiv
|
|
|
409
414
|
readers, a saved session's chip, and how the web takes a worker snapshot's. The session state, `/stats` (no
|
|
410
415
|
toolkit), a `:command_ran` and the web's session JSON list them from it.
|
|
411
416
|
|
|
417
|
+
### ModelBinding (the choice and its profile)
|
|
418
|
+
|
|
419
|
+
`ModelBinding` (`model_binding.rb`) holds the session's model choice, the profile given to `Engine.new` (until a
|
|
420
|
+
switch), the prompt profile resolved for the choice (kept as a `[choice, resolution]` pair) and the memory overlay
|
|
421
|
+
keys (`#sync_model_key!`), under one mutex; it is ToolkitBinding's counterpart, and the Engine forwards
|
|
422
|
+
`profile_resolution`, `refresh_profile!`, `store_profile`, `apply_profile`, `resolve_profile`, `sync_model_key!`
|
|
423
|
+
and `model_key` to it.
|
|
424
|
+
|
|
425
|
+
- `#switch!` (`Engine#switch_model!`'s swap) is one assignment under the mutex: a reader on another thread sees
|
|
426
|
+
the old pair or the new one. It drops the given profile and the kept resolution.
|
|
427
|
+
- `#store_profile` keeps and applies a resolution only while the choice is still the one it was resolved for,
|
|
428
|
+
by identity (`equal?`, not `==`): a `/stats` probing on a Bridge thread while `/model` runs keeps nothing.
|
|
429
|
+
- `#apply_profile` runs under the mutex: the system prompts built for another profile are dropped, then
|
|
430
|
+
`KernelLoop#use_profile!`. Nothing else takes the mutex.
|
|
431
|
+
|
|
412
432
|
### ThinkingNotices
|
|
413
433
|
|
|
414
434
|
What a turn's thinking level leads to, said once per session and host (`thinking_notices.rb`): an effort the
|
|
@@ -474,7 +494,8 @@ drain hands the message to the model at a boundary, or until the turn ends (`Ste
|
|
|
474
494
|
|
|
475
495
|
### The turn's sink (`on_event`) and the observers
|
|
476
496
|
|
|
477
|
-
`Engine#emit_event`
|
|
497
|
+
`Engine#emit_event` (`EventFanout#emit_event`, `event_fanout.rb`, which also builds the loops' stream handler and
|
|
498
|
+
emits the working set) sends each turn event to the turn's sink (`on_event`, the REPL's renderer) and then to the
|
|
478
499
|
persistent observers (`Engine#subscribe`), which get a copy with a monotonic `event_seq`. The observers are the
|
|
479
500
|
metrics, the debug log, the thinking tails, and in a worker the Bridge, which streams to the web and the attached
|
|
480
501
|
TUI. Errors in the sink or an observer are caught; they never break the turn.
|
|
@@ -491,8 +512,8 @@ Turn events (the main ones; `Events` in `events.rb` keeps the shared sets):
|
|
|
491
512
|
|
|
492
513
|
| Event | When | Payload |
|
|
493
514
|
|---|---|---|
|
|
494
|
-
| `:turn_started` | `prepare_turn` | `{session_id, prompt, turn_id, continue?, resumed?, images?, cut_after}` (`resumed`: a step-limit Continue) |
|
|
495
|
-
| `:reminder_injected` | due reminders went into the turn | `{reminders}` |
|
|
515
|
+
| `:turn_started` | `prepare_turn` | `{session_id, prompt, turn_id, at, continue?, resumed?, images?, cut_after}` (`resumed`: a step-limit Continue; `at`: the turn's wall time, its prompt's stamp) |
|
|
516
|
+
| `:reminder_injected` | due reminders went into the turn | `{reminders, at}` (`at`: the turn's wall time, the reminder message's stamp) |
|
|
496
517
|
| `: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) |
|
|
497
518
|
| `: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`) |
|
|
498
519
|
| `:generation_chunk` | each streamed chunk | `{iteration, content, text, thinking, payload, tool_call?}` (SSE clients get the Bridge's projection instead: `{iteration, text, thinking, tool_call?, counts?}`, architecture.md) |
|
|
@@ -582,8 +603,11 @@ hook registry (`@hooks.fire`), not on the sink or the observers. The other hook
|
|
|
582
603
|
```
|
|
583
604
|
Engine initialization (in this order, among other things):
|
|
584
605
|
@turn_state, @steer_cut (SteerCut: its GenerationPhase and WaitingSteer)
|
|
585
|
-
@
|
|
586
|
-
@
|
|
606
|
+
@event_fanout (EventFanout: emit_event, the stream handler, the working set; its lookups read at call time)
|
|
607
|
+
@turn_endings (TurnEndings: the turn endings' builders)
|
|
608
|
+
@host_registry, @model_binding (ModelBinding: the ModelChoice, i.e. the name and the name as typed, and the given
|
|
609
|
+
profile or nil: resolved on first need, so building an Engine makes no network call); @model_settings
|
|
610
|
+
(SessionModelSettings: the choice and the session read at use); @client = effective_model.client
|
|
587
611
|
MemoryBundle::SystemBundle.ensure! (the shipped system bundle installed; a failure is logged, not raised)
|
|
588
612
|
@event_routes (EventRoutes: where a card or a notice goes: held while the plugins load, an anytime command's, an init task's, a turn event, else announced or held)
|
|
589
613
|
@hook_routes (HookRoutes: notices (through @event_routes), hook questions, stop_turn/stop_generation, steer attribution)
|
|
@@ -607,9 +631,12 @@ Engine initialization (in this order, among other things):
|
|
|
607
631
|
@native_backend = LLM::NativeBackend.new(kernel: @kernel)
|
|
608
632
|
@prompt_builder = SystemPrompt.new(...)
|
|
609
633
|
@session_observer with the metrics, the log subscriber and the thinking tails subscribed (subscribe_session_observers)
|
|
634
|
+
@engine_snapshots (EngineSnapshots: session_state_snapshot and stats_snapshot), built last: nothing reads a
|
|
635
|
+
snapshot while the Engine is built
|
|
610
636
|
|
|
611
637
|
Model switching, Engine#switch_model!(model_name, persist_default:, typed:):
|
|
612
|
-
→ checks the host (ModelProfile.check_host!), sets
|
|
638
|
+
→ checks the host (ModelProfile.check_host!), sets the choice (ModelBinding#switch!: name and typed name in
|
|
639
|
+
one assignment)
|
|
613
640
|
→ drops the given profile and the profile resolution (resolved again on first need; one that another
|
|
614
641
|
thread finishes for the old model is neither kept nor applied: #store_profile)
|
|
615
642
|
→ sync_model_key!, @prompt_builder.reset! (the system prompt is built again)
|
|
@@ -655,6 +682,19 @@ logged). Both are display only: `AnswerDisplay::DISPLAY_ONLY` lists `display`, `
|
|
|
655
682
|
after_generation), while `KernelLoop#sanitize_history` and `ChatLoop#plain` copy them so the stored conversation keeps
|
|
656
683
|
them across turns. `Web::App#messages_for_display` sends `display_refs` re-validated (`AnswerRefs.for_display`).
|
|
657
684
|
|
|
685
|
+
History stamps: user prompts, merged input and steers, reminders, context and task notes and the model's replies carry
|
|
686
|
+
`at` (`MessageTime::KEY`, `message_time.rb`: a local-offset ISO time with milliseconds), when the entry happened. A
|
|
687
|
+
turn reads the wall clock once (`Turn#wall`, `MessageTime.now`): `:turn_started`'s `at`, its prompt's and its
|
|
688
|
+
reminders' stamps; `Steer::Merge#messages` stamps a merge at the boundary, `ContextNote.message` a note with its
|
|
689
|
+
`created_at` (else now), `KernelLoop#iterate` / `#cancelled_result` and the chat loop's `Run#with_thinking` /
|
|
690
|
+
`Run#canceled` each reply. Tool results, `!cmd` messages and TurnNote notes carry none. Like `display`, `at` must
|
|
691
|
+
survive the two rebuilds: both model branches of `KernelLoop#sanitize_history` and `ChatLoop#plain` copy it (an
|
|
692
|
+
allow-list that drops a key loses it for good on the next turn; `spec/message_time_history_spec.rb` checks both
|
|
693
|
+
loops). It is display-only and never reaches the wire (prompt-caching.md, rule 7), but it is not in
|
|
694
|
+
`AnswerDisplay::DISPLAY_ONLY`: hooks and `ctx.messages` see it. `:recap_ready` carries the saved recap's
|
|
695
|
+
`created_at` (`Engine#emit_recap(created_at:)`, from `IdleRecap#collect`). The UIs' gap dividers come from
|
|
696
|
+
`MessageTime.gap_line` (10 min, `GAP_SECONDS`).
|
|
697
|
+
|
|
658
698
|
`:question_asked` (`QuestionAsked`, `question_asked.rb`) fires from the two Engine seams where a question of the
|
|
659
699
|
model's opens, never from `QuestionDesk`: `Engine#request_question` (the kernel's `question_handler`, after
|
|
660
700
|
`AskUserQuestion.validate`; not with `interface == :non_interactive`) and the guardrail wiring's `ask:` lambda
|
|
@@ -795,6 +835,10 @@ Paths are under `lib/samagotchi/`.
|
|
|
795
835
|
| File | Role |
|
|
796
836
|
|---|---|
|
|
797
837
|
| `engine.rb` | Engine: turn lifecycle, event emission, plugins, model switching |
|
|
838
|
+
| `event_fanout.rb` | `EventFanout`: how an event reaches the turn's sink and the observers (`emit_event`), the loops' stream handler, the working set's emit and dedupe; the Engine forwards |
|
|
839
|
+
| `engine_snapshots.rb` | `EngineSnapshots`: the UIs' read model, `session_state_snapshot` (cheap, never raises) and `stats_snapshot` (may probe `/props`); the Engine forwards |
|
|
840
|
+
| `model_binding.rb` | `ModelBinding`: the model choice and the prompt profile resolved for it, under one mutex, and the overlay keys; the Engine forwards |
|
|
841
|
+
| `turn_endings.rb` | `TurnEndings`: what each turn ending keeps and announces (the kept messages and their `TurnNote`, `failed_event`, `turn_summary`, `stop_facts`); the Engine forwards |
|
|
798
842
|
| `turn_state.rb` | Cross-thread turn state (running, controller, sink, steers, activity clock) |
|
|
799
843
|
| `steer_cut.rb` | `SteerCut`: the Engine's seam for a message cutting the generation |
|
|
800
844
|
| `generation_phase.rb`, `waiting_steer.rb` | Whether a steer may cut the running generation, and the steer that waits to |
|