samagotchi 0.131.0 → 0.133.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 +88 -1
- data/bin/chi +19 -1
- data/docs/architecture.md +59 -1
- data/docs/cli.md +29 -8
- data/docs/configuration.md +115 -16
- data/docs/desktop.md +16 -6
- data/docs/guardrails.md +10 -4
- data/docs/internals/anthropic.md +3 -2
- data/docs/internals/engine.md +4 -4
- data/docs/internals/prompt-caching.md +26 -6
- data/docs/memory.md +16 -2
- data/docs/plugins.md +68 -1
- data/docs/sessions.md +60 -6
- data/docs/testing.md +1 -1
- data/lib/samagotchi/bootstrap_command.rb +35 -6
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/plugin.rb +10 -3
- data/lib/samagotchi/bundles/model-notes/manifest.yml +4 -1
- data/lib/samagotchi/bundles/model-notes/model_notes_tone.md +3 -0
- data/lib/samagotchi/bundles/model-notes/notes/README.md +14 -0
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/cache_confirm.rb +18 -0
- data/lib/samagotchi/chi_home.rb +311 -0
- data/lib/samagotchi/chi_shim.rb +77 -0
- data/lib/samagotchi/child_env.rb +5 -3
- data/lib/samagotchi/config.rb +2 -0
- data/lib/samagotchi/context_fetch.rb +4 -2
- data/lib/samagotchi/desktop/macos.rb +100 -18
- data/lib/samagotchi/engine.rb +35 -4
- data/lib/samagotchi/engine_snapshots.rb +5 -0
- data/lib/samagotchi/gem_update.rb +19 -10
- data/lib/samagotchi/guardrail_wiring.rb +3 -1
- data/lib/samagotchi/guardrails/pair_boundary.rb +21 -2
- data/lib/samagotchi/guardrails/pair_command.rb +57 -1
- data/lib/samagotchi/guardrails/protected_paths.rb +6 -3
- data/lib/samagotchi/guardrails/review_paths.rb +7 -4
- data/lib/samagotchi/guardrails/verdict.rb +1 -1
- data/lib/samagotchi/home_command.rb +145 -0
- data/lib/samagotchi/home_doctor.rb +177 -0
- data/lib/samagotchi/home_notice.rb +80 -0
- data/lib/samagotchi/installed_versions.rb +5 -3
- data/lib/samagotchi/llm_context_command.rb +53 -7
- data/lib/samagotchi/llm_context_override.rb +3 -2
- data/lib/samagotchi/plugin/api.rb +45 -1
- data/lib/samagotchi/plugin/context.rb +34 -1
- data/lib/samagotchi/plugin/loader.rb +5 -2
- data/lib/samagotchi/plugin/prompt_sections.rb +124 -0
- data/lib/samagotchi/project_env.rb +360 -0
- data/lib/samagotchi/run_options.rb +1 -0
- data/lib/samagotchi/self_report.rb +33 -0
- data/lib/samagotchi/session_access/contract.rb +1 -1
- data/lib/samagotchi/session_commands.rb +68 -19
- data/lib/samagotchi/session_manager.rb +54 -1
- data/lib/samagotchi/session_model_settings.rb +5 -2
- data/lib/samagotchi/system_prompt.rb +34 -3
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +3 -2
- data/lib/samagotchi/terminal_ui/formatting.rb +3 -0
- data/lib/samagotchi/testing/boundary.rb +3 -1
- data/lib/samagotchi/testing/bundle.rb +22 -3
- data/lib/samagotchi/testing/host.rb +22 -1
- data/lib/samagotchi/testing/recorder.rb +6 -1
- data/lib/samagotchi/testing/recording_api.rb +1 -1
- data/lib/samagotchi/tools/builtins.rb +13 -2
- data/lib/samagotchi/update_command.rb +65 -7
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +61 -1
- data/lib/samagotchi/web/public/app.js +4 -1
- data/lib/samagotchi/web/public/chip_popover.js +5 -2
- data/lib/samagotchi/web/public/composer.css +11 -1
- data/lib/samagotchi/web/public/data.js +8 -4
- data/lib/samagotchi/web/public/index.html +52 -10
- data/lib/samagotchi/web/public/level_chip.js +22 -38
- data/lib/samagotchi/web/public/llm_context_chip.js +30 -73
- data/lib/samagotchi/web/public/model_pick.js +26 -0
- data/lib/samagotchi/web/public/model_picker.js +3 -1
- data/lib/samagotchi/web/public/review_start.js +13 -57
- data/lib/samagotchi/web/public/send_turn.js +3 -2
- data/lib/samagotchi/web/public/start_page.js +131 -99
- data/lib/samagotchi/web/public/start_settings.js +298 -0
- data/lib/samagotchi/web/public/start_toolkit.js +157 -0
- data/lib/samagotchi/web/public/thinking_chip.js +9 -22
- data/lib/samagotchi/web/server.rb +27 -8
- data/lib/samagotchi/web/update_check.rb +3 -2
- metadata +11 -1
data/docs/guardrails.md
CHANGED
|
@@ -4,7 +4,7 @@ Every tool call the model makes passes one check before it runs, in both
|
|
|
4
4
|
loops (native and chat hosts). The verdict is **allow**, **ask** or
|
|
5
5
|
**deny**; the strictest vote wins, and a deny can't be undone by a later
|
|
6
6
|
voter. Of two asks the first one names the rule, except a rule only the
|
|
7
|
-
user may allow (`chi-config`, `chi-hooks`, `shell-touches-chi`,
|
|
7
|
+
user may allow (`chi-config`, `chi-home`, `chi-hooks`, `shell-touches-chi`,
|
|
8
8
|
`chi-context-cmd`, `chi-broadcast`): it takes over, and only the scopes both asks offer
|
|
9
9
|
are offered.
|
|
10
10
|
|
|
@@ -215,7 +215,7 @@ rules off: `guardrails: {disable: [write-outside-repo, git-outside-repo]}`.
|
|
|
215
215
|
|
|
216
216
|
Whatever the setting, a parent can't allow a call on chi's own files: a
|
|
217
217
|
`write`, `edit` or `execute` on the config dir (config.yml, installed bundles'
|
|
218
|
-
rules), the hooks dir or the approval store, by its rule (`chi-config`,
|
|
218
|
+
rules), the hooks dir or the approval store, by its rule (`chi-config`, `chi-home`,
|
|
219
219
|
`chi-hooks`, `shell-touches-chi`), its paths or its command. "Allow once" on a
|
|
220
220
|
config.yml rewrite would allow everything from then on, so those questions are
|
|
221
221
|
the user's alone (`user_only`): `chi answer` and the worker refuse the allow
|
|
@@ -280,7 +280,7 @@ Built in, for `write`, `edit` and `memory_write` (symlinks resolved; see
|
|
|
280
280
|
- deny: the approval store's dir, installed bundles (`memories/.bundles/`),
|
|
281
281
|
and [attached context](context.md) sources (`<state dir>/context/`: a
|
|
282
282
|
source's command runs later, outside the gate; `chi context` changes them);
|
|
283
|
-
- ask (once or for the session): `config.yml` and the plain hooks dir.
|
|
283
|
+
- ask (once or for the session): `config.yml`, `home.json` (the [chi home](configuration.md#the-chi-home): the Ruby every chi launch boots) and the plain hooks dir.
|
|
284
284
|
|
|
285
285
|
`execute` can still reach them; the guardrails bundle asks about shell
|
|
286
286
|
commands that name them.
|
|
@@ -429,7 +429,13 @@ message:
|
|
|
429
429
|
get-access-token`, `heroku auth:token`, `kubectl config view --raw`;
|
|
430
430
|
`PairCommand::CREDENTIAL_PRINTERS`). On top of it, a pair turn's `execute` runs
|
|
431
431
|
without every variable whose name matches `KEY|TOKEN|SECRET|PASS|CREDENTIAL|AUTH`
|
|
432
|
-
(`PairBoundary::SECRET_ENV_NAME`, case-insensitive)
|
|
432
|
+
(`PairBoundary::SECRET_ENV_NAME`, case-insensitive) and without the session's
|
|
433
|
+
project env ([Project env](sessions.md#project-env)); a command that runs
|
|
434
|
+
`mise` or `direnv` asks the owner (`ENV_LOADER`: they load the project's env
|
|
435
|
+
files, and `.envrc`, `mise.local.toml` and its kin are secrets like `.env`),
|
|
436
|
+
and so does a write (by a file tool or a command) to mise's config
|
|
437
|
+
(`MISE_CONFIG`: the owner's next worker start runs its `[env]`);
|
|
438
|
+
and a command still
|
|
433
439
|
running at the limit is killed, never handed over as a background task;
|
|
434
440
|
- git settings that make git run a command (`core.hooksPath`,
|
|
435
441
|
`core.fsmonitor`, `core.sshCommand`, `core.pager`, `core.editor`, a `!`
|
data/docs/internals/anthropic.md
CHANGED
|
@@ -133,8 +133,9 @@ append-only within a process, so what changes the prefix is:
|
|
|
133
133
|
1. **A system-prompt rebuild** (`SystemPrompt#build` is cached per process and key; `SystemPrompt#reset!` drops it):
|
|
134
134
|
a new process (`--resume`, a respawn after `session.idle_exit_minutes`, a crash) after a memory index line, an
|
|
135
135
|
AGENTS.md, an appended file, a model note or a bundle memory changed; `/model` (the tail's `Model:` line, its
|
|
136
|
-
overlays and notes); `/thinking` (another key); a toolkit or profile change; a plugin's `tools_changed
|
|
137
|
-
session's first participant joining or last
|
|
136
|
+
overlays and notes); `/thinking` (another key); a toolkit or profile change; a plugin's `tools_changed!` or
|
|
137
|
+
`prompt_changed!` (its prompt section's text, plugins.md); the session's first participant joining or last
|
|
138
|
+
leaving (the tail's participants line, participants.md).
|
|
138
139
|
2. **A tool set change**: the same triggers, plus MCP lines that differ on a respawn.
|
|
139
140
|
3. **History edits by design**: the `llm_context` `stale`/`forget` stubs, and the image cut moving a batch.
|
|
140
141
|
|
data/docs/internals/engine.md
CHANGED
|
@@ -832,10 +832,10 @@ answer is stripped from prefill + response, so the reasoning never lands in it).
|
|
|
832
832
|
persistent.
|
|
833
833
|
5. **A stable system prompt for the cache.** `SystemPrompt#build` caches the prompt per Engine, keyed by
|
|
834
834
|
`[chat, thinking, layers]` (chat/native, the thinking level, the turn's LLM context layers), and is reset
|
|
835
|
-
only on a model switch, a tools change, a
|
|
836
|
-
stable part (base prompt, the toolkit's not-loaded line,
|
|
837
|
-
AGENTS.md, appended files, the toolkit's kit prompt,
|
|
838
|
-
working directory, session).
|
|
835
|
+
only on a model switch, a tools change, a plugin's `prompt_changed!` (`Engine#prompt_changed!`), a profile
|
|
836
|
+
change or a toolkit change (`Engine#apply_toolkit!`). The stable part (base prompt, the toolkit's not-loaded line,
|
|
837
|
+
rg guidance, identity, model notes, preloaded memories, AGENTS.md, appended files, the toolkit's kit prompt, the
|
|
838
|
+
plugins' prompt sections, memory indexes) comes before the volatile tail (model, working directory, session).
|
|
839
839
|
prompt-caching.md covers this in detail.
|
|
840
840
|
|
|
841
841
|
## Key files
|
|
@@ -40,6 +40,7 @@ right-hand column happens.
|
|
|
40
40
|
│ AGENTS.md (root → cwd) │ (picked up by a new
|
|
41
41
|
│ appended files (append_system_prompt:, --append-system-prompt) │ process or a rebuild)
|
|
42
42
|
│ kit prompt (per toolkit: its kit's prompt: text, only when it has one) │
|
|
43
|
+
│ plugin prompt sections (chi.prompt_section, by (bundle, name)) │ chi.prompt_changed!
|
|
43
44
|
│ project + system memory indexes │
|
|
44
45
|
│ ◄── remote Claude / Messages: cache_control breakpoint #1 │
|
|
45
46
|
├──────────────────────────────────────────────────────────────────────────┤
|
|
@@ -65,8 +66,8 @@ right-hand column happens.
|
|
|
65
66
|
- The system prompt is built **once per key** and kept (`SystemPrompt#build`, keyed by `[chat, thinking, layers]`:
|
|
66
67
|
the loop, the thinking level, the turn's LLM context layers; a `/thinking` change builds under another key). All
|
|
67
68
|
of them are dropped only on `@prompt_builder.reset!`: a model switch (`Engine#switch_model!`), a plugin's
|
|
68
|
-
`tools_changed
|
|
69
|
-
everything below it.
|
|
69
|
+
`tools_changed!` or `prompt_changed!` (`Engine#prompt_changed!`), a profile change, or a toolkit change
|
|
70
|
+
(`Engine#apply_toolkit!`). A per-turn rebuild would break everything below it.
|
|
70
71
|
- The tools layer is shared **per toolkit** (configuration.md, "Toolkits"): sessions on the same toolkit share it,
|
|
71
72
|
and a chain's links keep theirs. A session's toolkit is saved when it is created and never resolved again from
|
|
72
73
|
config. A change (`/toolkit`, `ctx.use_toolkit`) is applied only at a turn's start (`Engine#apply_toolkit!`,
|
|
@@ -88,6 +89,15 @@ right-hand column happens.
|
|
|
88
89
|
sessions on the kit share it; a memory write (the indexes after it) doesn't move it. It is part of the
|
|
89
90
|
`Toolkit::Resolved` the Registry holds, so a switch that changes only the text is a toolkit change
|
|
90
91
|
(`apply_toolkit!` → `reset!`). `full` and kits without one send the bytes they sent before.
|
|
92
|
+
- A plugin's prompt sections (`chi.prompt_section`, `Plugin::PromptSections`) sit after the kit prompt and before
|
|
93
|
+
the memory indexes (`SystemPrompt#plugin_prompt_section`), sorted by (bundle, name), each
|
|
94
|
+
`From <bundle> (<name>):\n<text>`; with none (every block nil) the bytes are what they were before. A block runs
|
|
95
|
+
only inside a build, never per request, so its text changes the prompt only at a rebuild: `/model` (a block
|
|
96
|
+
reading `ctx.model` re-resolves for free) or the plugin's `chi.prompt_changed!`, which its command calls only
|
|
97
|
+
after asking (`ctx.cache_confirm`, rule 6); a running turn keeps the prompt it started with (`turn_messages` puts
|
|
98
|
+
it in the turn's messages once). A block that raises, returns a non-String or text over 2000 characters keeps
|
|
99
|
+
the section's last good text for the session: a skip on a transient error would break the cache twice (and on
|
|
100
|
+
Claude 5.x cost the preserved thinking each time).
|
|
91
101
|
- find_tools' description is a function of the toolkit and the source groups **at load**: `ToolIndex#declare`
|
|
92
102
|
snapshots source group names and description lines (name, summary, callable count). `ToolIndex#refresh!` (from
|
|
93
103
|
`ToolkitBinding#apply!`) filters that snapshot by the new toolkit and rebuilds the `chi` line from the built-ins
|
|
@@ -221,7 +231,9 @@ is prefilled once. The native and the chat loop use the same plan.
|
|
|
221
231
|
3. **Deterministic order.** Tools, memory lists and anything else that's rendered from a hash or a set needs a
|
|
222
232
|
fixed sort. Plugin tools are sorted by (bundle, name) whatever order their plugins finished starting in
|
|
223
233
|
(R5a).
|
|
224
|
-
4. **Don't rebuild the system prompt per turn.** Rebuild only when its content really changed (`reset!`).
|
|
234
|
+
4. **Don't rebuild the system prompt per turn.** Rebuild only when its content really changed (`reset!`). A
|
|
235
|
+
plugin's prompt section is the same text for the same inputs, and its `chi.prompt_changed!` is called only when
|
|
236
|
+
its text changed.
|
|
225
237
|
5. **Measure.** Every generation logs `prompt=`, `cached=` and, where the provider reports it, `cache_write=`
|
|
226
238
|
on its `generation_completed` log line. `/stats` shows "cached N (P%)" and "cache writes". A change that
|
|
227
239
|
drops `cached=` on the second fresh session is a regression. Update the prompt snapshot fixtures with
|
|
@@ -230,13 +242,21 @@ is prefilled once. The native and the chat loop use the same plan.
|
|
|
230
242
|
the conversation again asks a person at a web tab or a TUI first (`CacheConfirm`: the reply carries the
|
|
231
243
|
`--yes` line, `SessionCommands::Result#confirm` and the `command_ran` event's `confirm`). Each setting has a
|
|
232
244
|
classifier: `Thinking.cache_cost` (`:full` for Gemma, `:provider` for a remote chat host, `:tail` otherwise) and
|
|
233
|
-
`ReasoningHistory.cache_cost` (`:history` for a change to or from `all` once an earlier reply has thinking)
|
|
245
|
+
`ReasoningHistory.cache_cost` (`:history` for a change to or from `all` once an earlier reply has thinking),
|
|
246
|
+
`/model` (`SessionCommands#model_ask`: `:full` for any other resolved ref, `Engine#model_switch_ref` checking
|
|
247
|
+
the name first, so a typo errors instead of asking; every switching branch, `--default`, `--alias`, `clear`), and `LLMContextCommand#cache_cost` (`:full` for forget
|
|
248
|
+
on or off, `:history` for a layer with applied stubs turned off; stale on, the apply rule and the budget only
|
|
249
|
+
decide when a batch's break comes, so they don't ask).
|
|
234
250
|
`CacheConfirm.needed?` asks only for `:full`, `:provider` and `:history`, when the effective level changes and
|
|
235
251
|
the session has a model reply. The confirmation estimates the reread size from the latest known context count:
|
|
236
252
|
the whole conversation for `:full` / `:provider`, or up to the first changed reply for `:history`; it omits the
|
|
237
253
|
estimate when no count is known. On `api: anthropic`, a `:full` / `:provider` change also warns that preserved
|
|
238
|
-
thinking in replies is dropped from then on. A new setting with a cost
|
|
239
|
-
|
|
254
|
+
thinking in replies is dropped from then on. A new setting with a cost adds its classifier and goes through
|
|
255
|
+
the same step. A launch flag (`chi --model`, `--llm-context`, `--thinking` on an attached or resumed session,
|
|
256
|
+
`LLMContextOverride.command_line`) and a chip whose form states the cost (`level_chip.js`, `llm_context_chip.js`)
|
|
257
|
+
send their line with `--yes`: typing the flag, or the Set, is the choice. A plugin command asks
|
|
258
|
+
through `ctx.cache_confirm(line, changed:)` (cost `:full`: its prompt section is in the stable part), with the
|
|
259
|
+
sender's flag `SessionCommands#run` gives it (`CacheConfirm.asking`), and returns `[text, confirm]`.
|
|
240
260
|
7. **No wall-clock time rendered at wire time.** A history entry's `at` (`MessageTime::KEY`, when it happened) is
|
|
241
261
|
display-only: no wire format reads it, and a guard spec checks that `ChatLoop#wire_messages` and
|
|
242
262
|
`Prompt.format_with_images` give the same bytes with and without it (`spec/message_time_history_spec.rb`). A
|
data/docs/memory.md
CHANGED
|
@@ -164,9 +164,23 @@ Working habits for this model:
|
|
|
164
164
|
longest run without an edit and the Continues and steers each needed; run a
|
|
165
165
|
note on and muted for the same kind of task. See
|
|
166
166
|
[testing.md](testing.md) ("The model notes report").
|
|
167
|
+
- **A note for every model**: `models: *` matches any id, so one note sets a
|
|
168
|
+
house style for all of them. The model-notes bundle ships one,
|
|
169
|
+
`model_notes_tone`:
|
|
170
|
+
|
|
171
|
+
```markdown
|
|
172
|
+
models: *
|
|
173
|
+
How to write answers:
|
|
174
|
+
- Answer first, in plain words. Skip preambles, restating the question and closing offers. Use a list only for steps or several parallel items.
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
It stacks with a family's note. Off for one session: `chi --mute
|
|
178
|
+
model_notes_tone` (at launch); everywhere: `chi memory rm model_notes_tone
|
|
179
|
+
--scope system`, or keep your own text under that name (yours wins).
|
|
167
180
|
- **Shipped notes: the model-notes bundle.** `chi bundle install model-notes`
|
|
168
|
-
(or the `dev` profile) installs reviewed notes into the system scope
|
|
169
|
-
|
|
181
|
+
(or the `dev` profile) installs reviewed notes into the system scope:
|
|
182
|
+
`model_notes_deepseek` (`models: *deepseek-v4*`) and `model_notes_tone`
|
|
183
|
+
(`models: *`, concise answers for every model). Each note's provenance
|
|
170
184
|
and evidence is in the bundle's `notes/README.md`, and its spec pins which
|
|
171
185
|
model ids load which note ([Plugins](plugins.md#the-model-notes-bundle)).
|
|
172
186
|
- **Your own note of the same name wins.** The install skips a
|
data/docs/plugins.md
CHANGED
|
@@ -150,6 +150,34 @@ chi.command "/hello-slow", "greet after 2 s, even mid-turn", anytime: true do |a
|
|
|
150
150
|
end
|
|
151
151
|
```
|
|
152
152
|
|
|
153
|
+
#### Commands that ask: `ctx.cache_confirm`
|
|
154
|
+
|
|
155
|
+
A command whose change restarts the model server's prompt cache (one that changes the text of a
|
|
156
|
+
[prompt section](#chiprompt_sectionname--ctx---and-chiprompt_changed)) asks first, as `/thinking` and `/toolkit` do:
|
|
157
|
+
`ctx.cache_confirm(line, changed:)` returns nil when it may apply now, or a confirm to return with the command's
|
|
158
|
+
text, `[text, confirm]`, changing nothing. `line` is the command line that applies it (without `--yes`), `changed:`
|
|
159
|
+
whether your text would change. chi decides the rest: it asks only a person at a web tab or a TUI (never `chi send`,
|
|
160
|
+
a delegate or `chi -p`), only once the session has a model reply, never for a line with `--yes` (or `-y`), and
|
|
161
|
+
writes the confirm's text (the conversation's size to read again; on an `api: anthropic` host, that preserved
|
|
162
|
+
thinking is dropped). The TUI prefills `line --yes`, the web shows an Apply button that sends it, and the command
|
|
163
|
+
then runs again with `--yes` in `args` (drop it from your words). Outside a command (a hook, an init task, your
|
|
164
|
+
own thread) it is always nil. A plain String or nil return is unchanged.
|
|
165
|
+
|
|
166
|
+
A command that asks must not be `anytime: true`: the REPL shows only an anytime command's text, so its Apply would
|
|
167
|
+
be lost. Needs chi 0.132.0.
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
chi.command "/tone", "show or set this session's tone" do |args, ctx|
|
|
171
|
+
word = (args.split - %w[--yes -y]).join(" ")
|
|
172
|
+
ask = ctx.cache_confirm("/tone #{word}", changed: word != current_tone)
|
|
173
|
+
next ["Switching to #{word} restarts the prompt cache.", ask] if ask
|
|
174
|
+
|
|
175
|
+
save_tone(word)
|
|
176
|
+
@chi.prompt_changed!
|
|
177
|
+
"tone: #{word} from the next turn"
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
153
181
|
#### `/help`
|
|
154
182
|
|
|
155
183
|
`/help` (itself an anytime command) lists every command the session knows:
|
|
@@ -607,6 +635,38 @@ chi.toolkit "coding", base: "child", prompt: "- Be concise in your responses"
|
|
|
607
635
|
chi.toolkit "web", base: "lean", groups: %w[chrome db*]
|
|
608
636
|
```
|
|
609
637
|
|
|
638
|
+
### `chi.prompt_section(name) { |ctx| … }` and `chi.prompt_changed!`
|
|
639
|
+
|
|
640
|
+
Text the session's system prompt carries, from a plugin: the block gets the Context and returns a String (at most
|
|
641
|
+
2000 characters, stripped) or nil for no section. Each goes into the prompt's stable part right after the
|
|
642
|
+
toolkit's prompt text and before the memory indexes, as `From <bundle> (<name>):` and the text, sorted by
|
|
643
|
+
(bundle, name). With no section (every block nil, or no plugin uses it) the prompt is exactly what it was without.
|
|
644
|
+
`name` is `a-z`, `0-9`, `_` and `-` (at most 32), once per bundle.
|
|
645
|
+
|
|
646
|
+
The block runs when the prompt is built: a session's first turn, after `/model` (so `ctx.model` and
|
|
647
|
+
`ctx.model_key` are the new model's), after `chi.prompt_changed!` or a tools change. Never per request, and on
|
|
648
|
+
whatever thread builds, so keep it quick and thread-safe (no I/O beyond reading a small file). **Same inputs, same
|
|
649
|
+
text**: anything that differs (a time, a random pick, a count) breaks the model server's prompt cache at the next
|
|
650
|
+
build. A block that raises, returns something other than a String or nil, or text over the limit keeps that
|
|
651
|
+
section's last good text for the session (one warn in the debug log, `prompt_section_failed`); with none yet the
|
|
652
|
+
section is left out. A deliberate nil removes it.
|
|
653
|
+
|
|
654
|
+
`chi.prompt_changed!` says your section would give other text now (the user picked another setting): the prompt is
|
|
655
|
+
built again at its next build, the next turn's start (a running turn keeps the prompt it started with). That
|
|
656
|
+
restarts the prompt cache for the whole conversation, so a command that changes it asks first
|
|
657
|
+
([Commands that ask](#commands-that-ask-ctxcache_confirm)) and calls `chi.prompt_changed!` only once it applies.
|
|
658
|
+
Keep `chi` from `register` for that (`@chi = chi`). `/stats` names the sections the prompt holds
|
|
659
|
+
(`prompt sections: tone (tone)`). The test kit runs them as a build does (`bundle.prompt_text`,
|
|
660
|
+
`bundle.prompt_sections`) and counts `chi.prompt_changed!` (`bundle.prompt_changes`). Needs chi
|
|
661
|
+
0.132.0: a bundle using it sets `requires_chi`.
|
|
662
|
+
|
|
663
|
+
```ruby
|
|
664
|
+
def register(chi)
|
|
665
|
+
@chi = chi
|
|
666
|
+
chi.prompt_section("tone") { |ctx| preset_text(ctx.model_key) } # nil: no section
|
|
667
|
+
end
|
|
668
|
+
```
|
|
669
|
+
|
|
610
670
|
### `chi.tool_source(call:, acts_as:, label:, wait:, prefix:)`
|
|
611
671
|
|
|
612
672
|
Tools the model reaches through a search instead of declaring them one by
|
|
@@ -764,6 +824,7 @@ full label, `plugin.rb (bundle my-bundle)`.
|
|
|
764
824
|
| `ctx.fork?` | whether the session is a fork (`ctx.sessions.fork`): it has a parent but isn't a delegate child, and started from a conversation, so its first prompt isn't its first user message |
|
|
765
825
|
| `ctx.model` | the model the session runs on now: its resolved ref (`host:id`), right after `/model` too |
|
|
766
826
|
| `ctx.model_key` | that model's memory overlay key (`<name>.<key>.md`, what `memory_write current_model_only` writes) |
|
|
827
|
+
| `ctx.model_names` | the names chi's own per-model config looks that model up under (a `models:` map in config.yml): as typed (an alias), the alias's target, the resolved ref, without the host, the bare id; deduped, most specific first, frozen (`[]` when it doesn't resolve). Match a map of yours over them downcased, first hit, as chi does. Needs chi 0.132.0 |
|
|
767
828
|
| `ctx.cost` | the session's spend so far, read now, as a frozen value: `total` (USD, reported + estimated, side requests included; 0.0 before any), `reported` (by providers, `usage.cost`), `estimated` (from `hosts.<name>.models` prices; a generation is one or the other, never both), `side` (recap, `/btw`, `ask_model`: their share of `total`) and `known?` (false while generations or side requests ran but none was reported or priced, so a limit on it can't be enforced; a session with some priced generations reads known, the unpriced ones counting 0). It includes the running turn's finished generations and what earlier workers of the session spent. Use its methods, not its class. Needs chi 0.83.0 |
|
|
768
829
|
| `ctx.usage` | the session's work so far, read now, as a frozen value: `turns` (finished ones and the running one), `tool_calls` and `tool_calls_by_tool` (every call, running ones included; canonical names: an alias counts under its tool, a `call_tool` call under the inner tool), `steps` (the loop rounds that dispatched tools, `/stats`' iterations) and `active_ms` (`active_minutes` too): work time, the generations' time plus the finished tool calls' time, never the waits between them (a question, an approval, the user's answer to `ask_user_question`), with `tool_ms_by_tool` its tool part by tool, so a waiting tool's time (`task_wait`) can be left out. Its counts are the ones `/stats` and `chi sessions stats` show, earlier workers' included. Use its methods, not its class. Needs chi 0.100.0 (guard it with `ctx.respond_to?(:usage)`) |
|
|
769
830
|
| `ctx.turn_author` | who wrote the running turn when it isn't the session's owner: a participant's `"p:<id>"` (`chi share`); nil for the owner's turns and between turns. Every hook event of such a turn carries the same as `author:`. The handler runs with the owner's power whatever the author ([hooks](hooks.md#participants-turns)). Needs chi 0.105.0 (guard it with `ctx.respond_to?(:turn_author)`) |
|
|
@@ -788,6 +849,7 @@ full label, `plugin.rb (bundle my-bundle)`.
|
|
|
788
849
|
| `ctx.use_toolkit(name)` | put the session on toolkit `name` (yours from `chi.toolkit`, or chi's): saved, applied at the next turn's start and announced (`toolkit> toolkit: NAME from the next turn's start`, the web's tools chip follows). Only before the session's first model reply and outside a turn (the declared tools start every request, and a plugin doesn't ask first); otherwise it returns false and changes nothing: tell the user to run `/toolkit NAME`. An unknown name raises ArgumentError. Needs chi 0.66.0 |
|
|
789
850
|
| `ctx.load_tools(names)` | load tools the session reaches but doesn't declare, so the model calls them by name: built-ins (`"delegate"`, `"chi/delegate"`), MCP tools (`"mcp_fs_read"`, `"fs/read"`). Returns the names it staged (the wire names); a declared tool, one loaded before, a removed or unknown one is skipped (an unknown one logged), so calling it every turn is fine. From a `before_turn` hook it loads for that turn; anywhere else (mid-turn too) from the next turn's start. Call it from a hook: before the session's first turn its toolkit isn't applied yet and nothing resolves. **A hint, not access**: the tools keep their guardrails and approvals, and only a host that takes tool changes (`api: anthropic`, not Fireworks) gets one, as a system message after the user's (`[tools loaded: delegate (your-bundle)]` in the session, a notice line live); elsewhere nothing is sent or saved, and the tools stay reachable with `call_tool`. A hook may switch the model after you call it, so the return value can't promise delivery. Raises ArgumentError for anything but an Array. Needs chi 0.99.0 |
|
|
790
851
|
| `ctx.context` | attach outside text to this session and list it: see [Attached context](#attached-context-ctxcontext) |
|
|
852
|
+
| `ctx.cache_confirm(line, changed:)` | inside a command: nil to apply a prompt-cache-restarting change now, or a confirm to return as `[text, confirm]`: see [Commands that ask](#commands-that-ask-ctxcache_confirm). Needs chi 0.132.0 |
|
|
791
853
|
|
|
792
854
|
The Engine itself is never handed to a plugin.
|
|
793
855
|
|
|
@@ -1211,6 +1273,10 @@ bundle.fire(:after_turn, status: "completed") # → the event, after
|
|
|
1211
1273
|
bundle.question_asked("Fix JIRA-1?", question: { question: "Go?" }) # → the refs, as chi's seams fire it
|
|
1212
1274
|
event = bundle.before_tool_call_event({ name: "read", path: "a.txt" }, params: "read a.txt")
|
|
1213
1275
|
bundle.command("/mine stop") # → what the command returned
|
|
1276
|
+
bundle.command("/tone x", confirm: true) # sent from a web tab or a TUI: ctx.cache_confirm may ask
|
|
1277
|
+
bundle.set(history: true, context_tokens: 900) # a model reply to restart (ctx.cache_confirm), its size
|
|
1278
|
+
bundle.set(model_names: %w[tiny box:gemma gemma]) # ctx.model_names (default: model_name and its id)
|
|
1279
|
+
bundle.prompt_text # the prompt sections' text, as a build runs them
|
|
1214
1280
|
bundle.tool("my_tool", path: "a.txt") # → the text the model reads
|
|
1215
1281
|
verdict = bundle.tool_call({ name: "execute", content: "rm -rf /" })
|
|
1216
1282
|
verdict.deny? # a real Guardrails::Verdict (votes: verdict.votes)
|
|
@@ -2510,11 +2576,12 @@ gem.
|
|
|
2510
2576
|
`chi bundle install model-notes` (or the `dev` profile) installs reviewed
|
|
2511
2577
|
[model notes](memory.md#model-notes) into the system scope: working habits
|
|
2512
2578
|
for one model family, loaded into the prompt only for the models a note's
|
|
2513
|
-
`models:` line names. It has no plugin or hook. It ships
|
|
2579
|
+
`models:` line names. It has no plugin or hook. It ships two notes:
|
|
2514
2580
|
|
|
2515
2581
|
| note | models | for |
|
|
2516
2582
|
|---|---|---|
|
|
2517
2583
|
| `model_notes_deepseek` | `*deepseek-v4*` | DeepSeek V4 / V4.1: explore briefly then act, commit one item at a time, stop digging, `memory_read` for memories, file tools rather than heredocs that guardrail rules ask about |
|
|
2584
|
+
| `model_notes_tone` | `*` | every model: answer first, in plain words; no preambles, restated questions or closing offers; lists only for steps or parallel items |
|
|
2518
2585
|
|
|
2519
2586
|
- **Reviewed before it ships.** A note is generic (no repo or machine facts),
|
|
2520
2587
|
says nothing the harness already does (a tool's description, an alias),
|
data/docs/sessions.md
CHANGED
|
@@ -96,9 +96,9 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
|
|
|
96
96
|
- The web header's project chip opens a picker. The start project is the default row (including its start folder); other rows show their root, session and waiting counts. **All projects** opens every session, and choosing a project drops the open-session hash while keeping `?view=`.
|
|
97
97
|
|
|
98
98
|
- `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.
|
|
99
|
-
- 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 as `?scope=all` (every session; a new session there starts in chi web's start folder, shown on the start page, and cards name their folder; `← <project>` goes back to the project view it came from, or to the start folder's project). A bare `/` (the LAN link, the Home Screen app) is the start folder's project too: the folder chi web runs in, or `web.start_dir` / `chi web --start-dir DIR`; outside a repo it shows every session. A pushed notification opens its session in that session's own project (`/?dir=<its project>#/s/<id>`; `?scope=all` outside a repo). 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).
|
|
99
|
+
- 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 as `?scope=all` (every session; a new session there starts in chi web's start folder, shown on the start page, and cards name their folder; `← <project>` goes back to the project view it came from, or to the start folder's project). A bare `/` (the LAN link, the Home Screen app) is the start folder's project too: the folder chi web runs in, or `web.start_dir` / `chi web --start-dir DIR`; outside a repo it shows every session. A pushed notification opens its session in that session's own project (`/?dir=<its project>#/s/<id>`; `?scope=all` outside a repo). 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`; `toolkits`: `{default, suggest, kits: [{name, description?}]}`, the start page's 🧰 list: the configured `toolkit.default`, `suggest` `on` only when a new session's first message may get a suggested kit (`toolkit.suggest.mode: on`, a System 1 host, two described kits; else `audit` or `off`), and the built-in and config kits a new session can start on, without `review`, `child`, `undecided`, a kit `groups:` refuses or one that doesn't resolve (a base that is a bundle kit), the default always listed; left out when it can't be read), and `POST /api/sessions` takes `model` (blank means the default). Beside it, ⚙ opens the new session's rarely used settings (thinking and LLM context, following each `GET /api/models` row's `thinking` and `llm_context`, and the review files; see [configuration.md](configuration.md#llm-context-a-sessions-own-strategy)), and 🧰 its toolkit: **Auto** (with `toolkit.suggest.mode: on`: the first message may get a suggested kit, else the default) or a kit from the payload's `toolkits`, sent as `POST /api/sessions`'s `toolkit` (see [configuration.md](configuration.md#toolkits)); a refused kit (`400 toolkit_refused`) is said in the list, opened again. What is set shows on the row as an accent chip (`think high`, `tools: lean`; a dot on ⚙ or 🧰 on a phone); each is for the next new session only, never remembered. 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).
|
|
100
100
|
- 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).
|
|
101
|
-
- 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`), `markdown_place.js` (where rendered markdown and its refs go: a text as markdown HTML, its `path:line` refs, a final answer's rendering), `images.js` (image chips in the composer: paste, drop and the image button, the upload before a turn, the thumbnails and the lightbox), `composer_grip.js` (the composer's grip and fold: the drag, tap and keys, the fold per visit), `composer_mount.js` (the composer mounted on a page's markup: the message box and its height, the image chips from a paste, a drop or the image button, the grip and fold, ↑/↓ over the prompt history and the keys; the page's own parts come in as hooks: what Enter and Send do, keys taken first, Esc, where the history comes from, the info bar's Stop turn and drop while folded, typing anywhere to open it; its styles, the card it sits in (`.composer-card`, its footer and its line above the text field) included, are the shared `composer.css`, linked after `tokens.css` by `index.html` and `participant.html`), `strip_grip.js` (the latest-sessions strip's grip, the composer grip's twin: hides and shows the strip), `review_history.js` (a participant's ↑/↓ list on the participant page: their own comments, in localStorage per grant, never the owner's `/api/history`; `participant.js` mounts `composer_mount.js` with it, no `/` list, no delivery choices, images off), `storage.js` (the page's per-browser choices in localStorage, guarded: a choice that can't be read or written is not remembered), `unqueue.js` (the × on a message queued for after the turn: takes it back, its text into an empty composer or a toast), `toasts.js` (the page's toast stack: a timer each, sticky ones until acted on, the same text reusing its item), `toast_place.js` (where the toast sits: above the composer card, the stage and an open model panel), `dock_reserve.js` (what floats over the history: the history's bottom reserve (`--dock-h`) at the composer card's and the showing stage's height, a reader at the end kept there, and the toast's place measured for `toast_place.js`), `notice_links.js` (a hook notice line's http(s) URLs as links), `start_page.js` (the start page's model picker
|
|
101
|
+
- 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`), `markdown_place.js` (where rendered markdown and its refs go: a text as markdown HTML, its `path:line` refs, a final answer's rendering), `images.js` (image chips in the composer: paste, drop and the image button, the upload before a turn, the thumbnails and the lightbox), `composer_grip.js` (the composer's grip and fold: the drag, tap and keys, the fold per visit), `composer_mount.js` (the composer mounted on a page's markup: the message box and its height, the image chips from a paste, a drop or the image button, the grip and fold, ↑/↓ over the prompt history and the keys; the page's own parts come in as hooks: what Enter and Send do, keys taken first, Esc, where the history comes from, the info bar's Stop turn and drop while folded, typing anywhere to open it; its styles, the card it sits in (`.composer-card`, its footer and its line above the text field) included, are the shared `composer.css`, linked after `tokens.css` by `index.html` and `participant.html`), `strip_grip.js` (the latest-sessions strip's grip, the composer grip's twin: hides and shows the strip), `review_history.js` (a participant's ↑/↓ list on the participant page: their own comments, in localStorage per grant, never the owner's `/api/history`; `participant.js` mounts `composer_mount.js` with it, no `/` list, no delivery choices, images off), `storage.js` (the page's per-browser choices in localStorage, guarded: a choice that can't be read or written is not remembered), `unqueue.js` (the × on a message queued for after the turn: takes it back, its text into an empty composer or a toast), `toasts.js` (the page's toast stack: a timer each, sticky ones until acted on, the same text reusing its item), `toast_place.js` (where the toast sits: above the composer card, the stage and an open model panel), `dock_reserve.js` (what floats over the history: the history's bottom reserve (`--dock-h`) at the composer card's and the showing stage's height, a reader at the end kept there, and the toast's place measured for `toast_place.js`), `notice_links.js` (a hook notice line's http(s) URLs as links), `start_page.js` (the start page's model picker, its ⚙ panel's and 🧰 list's choices for the next new session only and their override chips; a fresh chain link's picker and its `/model`), `start_settings.js` (the start page's ⚙ panel: thinking and LLM context as rows of segments, applied at once, the Apply and budget disclosure, the review files' view; the override chips' words and ⚙'s dot), `start_toolkit.js` (the start page's 🧰 list: Auto with suggest on, the kits, the default marked; a pick closes it; its override chip and dot), `session_actions.js` (Continue, Archive and its Undo, Delete and Stop, and the start page when the open session is gone), `send_turn.js` (the composer's send: a new session first on the start page, the route, the echo, the image upload, the `/turn` ack, a fast failure's restore and each error's place). Unit-tested via `npm test` (`spec/web/public/*.test.js`).
|
|
102
102
|
- 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).
|
|
103
103
|
- The web session page's message box folds away for room to read. Drag the grip on its top edge (the pill that shows on hover; always on a phone) down, past the box by 32 px: the pill turns accent while releasing would hide it, and on release the box folds and the info bar stays as the page's bottom edge. The grip stays as a handle on the info bar's top edge, drawn always: pull it up (8 px opens it, and the box follows the pointer in the same gesture), click or tap it (opens it and focuses the box), or just type on the page (the key lands in the box) to bring it back; Enter or Space on the focused grip folds or opens too. A draft (text or images, which folding never clears) colours the handle with the accent, a session waiting on you with the attention colour; its tooltip says so. Stop turn moves into the info bar while it is folded. The fold is remembered per browser (`chi_composer_collapsed`), an archived session opens folded for the visit, and opening it there, like the page opening it itself (a restored line, a quote, continue →'s opener), is for that visit only. The fold keeps the height the grip set (`chi_composer_floor`): the box opens again at the height it had, and only a double-click on the grip forgets it. That height never takes more than half of the window the box is drawn in: a floor saved in a bigger window is held to that half in a smaller one (a rotated phone, a resized window, another device on the same origin), and the saved floor itself stays, so the height comes back when the room does. A click on the expanded grip hides the box as well: it folds 250 ms after the press, so a second click inside that window is read as a double-click and resets the height instead (a drag, Enter or Space act at once, and a drag after a click takes the wait back). The start page's grip only resizes.
|
|
104
104
|
|
|
@@ -296,10 +296,10 @@ sessions](cli.md#review-sessions)); both start idle and print the `chi share` li
|
|
|
296
296
|
folder and setup are taken (`dir` and the setup fields beside it are 400 `invalid_review`) and `model` is the
|
|
297
297
|
only choice. A subject refused, or the source's folder gone, is 400 `invalid_review` with the one message; an
|
|
298
298
|
unknown source 404 `not_found`.
|
|
299
|
-
- **The page:** the start page's
|
|
300
|
-
space-separated, relative to the page's folder; a path with a space in double quotes, `"docs/my plan.md"`):
|
|
301
|
-
|
|
302
|
-
turn; with none it starts idle). A refused file shows the server's one message in the
|
|
299
|
+
- **The page:** the start page's ⚙ panel, **Review files…**, takes the files (comma- or
|
|
300
|
+
space-separated, relative to the page's folder; a path with a space in double quotes, `"docs/my plan.md"`): the
|
|
301
|
+
row then shows a `review: plan.md +1` chip, and Start makes a review session (the message typed, if any, is its first
|
|
302
|
+
turn; with none it starts idle). A refused file shows the server's one message in the panel's review view, opened again. On a session that isn't a review session the participants panel (the session bar's
|
|
303
303
|
participants chip) says so before its Invite, with the share warning (that a participant's turns act there, writes
|
|
304
304
|
included and asks to the owner, or only read while an older worker holds it; how many tool outputs its history holds)
|
|
305
305
|
and **Start a review of it**: a review fork of it, opened, with a toast (`Review 4b7e9a01 started from 3f9a1c2e's
|
|
@@ -364,6 +364,60 @@ otherwise; `settings` beside `continues` is refused). A respawned or woken worke
|
|
|
364
364
|
delegate child starts without (a bundle has its own setting for children, such as cost-cap's `child:`), so its recap
|
|
365
365
|
follows config.yml. `/stats` names them on a `settings:` line, and `chi sessions stats --format json` as `settings`.
|
|
366
366
|
|
|
367
|
+
## Project env
|
|
368
|
+
|
|
369
|
+
The commands a session's worker starts for the session (`execute`, tasks, `!cmd`, MCP servers, context sources)
|
|
370
|
+
follow the session's project, not whoever started the worker (a terminal on another Ruby, chi web, the macOS helper
|
|
371
|
+
under launchd). At start a worker runs `mise env -C <session folder> --json` once (`ProjectEnv`,
|
|
372
|
+
`session.project_env: mise`, the default) and merges the answer into the env of those commands: a project whose
|
|
373
|
+
mise.toml pins Ruby 3.4 gets that `ruby`, `gem` and `bundle` in its tools while chi itself runs on the
|
|
374
|
+
[chi home](configuration.md#the-chi-home).
|
|
375
|
+
|
|
376
|
+
- **Tools only.** The worker's own process keeps the env it was started with: chi's model calls and API keys, its
|
|
377
|
+
proxies, config, dirs, hooks and plugins never see the project's `[env]` (a mise.toml that sets
|
|
378
|
+
`ANTHROPIC_API_KEY` or `https_proxy` changes only what its tools see). Hooks run with the worker's env, not the
|
|
379
|
+
project's. A `chi` a tool runs (`chi -p`, `chi models`, `chi send`) takes the worker's values back first thing, so
|
|
380
|
+
it too runs on your keys and proxies, not the project's.
|
|
381
|
+
- **Once per worker.** A mise.toml edit (or a `mise use` in a tool) reaches the session at its next worker:
|
|
382
|
+
`chi sessions restart ID` (or the web's restart) picks it up.
|
|
383
|
+
- **GEM_HOME/GEM_PATH.** The starter's are dropped from the tools' env when the project's `ruby` is another one (they
|
|
384
|
+
belong to the starter's Ruby), unless mise sets its own; kept when the Ruby stays the same.
|
|
385
|
+
- **Never what a `chi` in a tool reads.** The tools' env never takes `HOME`, any `XDG_*`, any `SAMAGOTCHI_*`,
|
|
386
|
+
`CHI_REMOTE`, `RACK_ENV` or `CI` from the project (a `chi` run there would read another config or state dir, or
|
|
387
|
+
send its session commands to another machine): they are dropped
|
|
388
|
+
and the debug log names them once (`project_env_reserved`). `mise env` runs without `MISE_YES` (it would trust an
|
|
389
|
+
untrusted mise.toml for you) and `CI`; your `MISE_TRUSTED_CONFIG_PATHS` stays.
|
|
390
|
+
- **Only this session.** A worker started from this one (a restart, a delegate child) starts on the worker's own
|
|
391
|
+
env. One started from a tool (`chi send --new --dir other/project`) too: the tools' env carries the worker's values
|
|
392
|
+
(`SAMAGOTCHI_PROJECT_ENV_ORIG`) and that `chi` takes them back, so project A's PATH, `[env]` values, GEM_* and
|
|
393
|
+
library paths never reach a session in B.
|
|
394
|
+
- **Context sources** run in the project env of the worker that fetches them: the same source can read differently
|
|
395
|
+
from another session's worker (or a REPL, which has none).
|
|
396
|
+
- **Pair turns** (a participant's, see [Participants](internals/participants.md)) run their `execute` without the
|
|
397
|
+
project env. MCP servers are the session's, started once in the project env, so a pair turn's MCP calls do use
|
|
398
|
+
it. A participant's command that runs `mise` or `direnv`, and any write to mise's config (`mise.toml`,
|
|
399
|
+
`.mise.toml`, `mise.<env>.toml`, `.config/mise.toml`, `.config/mise/…`, `mise/` or `.mise/config*.toml`: your
|
|
400
|
+
next worker start runs it, and its `[env]` can run commands), asks you first. `.envrc` and mise's local files
|
|
401
|
+
(`mise.local.toml`, `mise.<env>.local.toml`, dotted or not, and `config.local.toml` / `config.<env>.local.toml` in
|
|
402
|
+
`.config/mise/`, `mise/`, `.mise/`) are secrets to them, as `.env` is.
|
|
403
|
+
- **What tools still drop**: every `execute`, task and `!cmd` takes no `RUBYOPT`, `RUBYLIB`, `BUNDLE_GEMFILE`,
|
|
404
|
+
`BUNDLE_BIN_PATH` or `BUNDLER_VERSION` from the project's mise env: it runs without `RUBYLIB` and Bundler's
|
|
405
|
+
variables, and with the worker's own `RUBYOPT`, Bundler's part removed (`TaskRuntime`).
|
|
406
|
+
- **`chi` in a tool** stays the home's: with a chi home the tools' PATH starts with `$XDG_STATE_HOME/samagotchi/bin`,
|
|
407
|
+
a dir holding only a `chi` script that boots the home (rewritten at each worker start when the home changed; it
|
|
408
|
+
unsets `RUBYOPT`, `RUBYLIB`, `RUBYGEMS_GEMDEPS`, `GEMRC` and `BUNDLE*` before it runs, so a `bundle exec` around it
|
|
409
|
+
doesn't load the project's gems into chi; and `LD_PRELOAD`, `LD_LIBRARY_PATH` and their `DYLD_*` kin). The project's `ruby`, `gem` and `bundle` stay the project's; `bin/chi`, a
|
|
410
|
+
checkout path or `bundle exec chi` stay themselves. Without a home there is no script and no PATH change.
|
|
411
|
+
- **When it does nothing**: `session.project_env: off` (mise isn't asked; with a chi home the `chi` script still
|
|
412
|
+
goes first on the tools' PATH); a session without a folder (`none (no_dir)`); no `mise` (looked up as home.json's `mise`, then PATH, then
|
|
413
|
+
`/opt/homebrew/bin`, `/usr/local/bin`, `~/.local/bin`); the session's folder is gone (the worker runs elsewhere).
|
|
414
|
+
A `mise env` that fails (an untrusted mise.toml: run `mise trust` there; a pinned Ruby not installed), takes over
|
|
415
|
+
5 s or doesn't answer JSON changes nothing and leaves one `project_env` warning in the debug log; `chi home doctor`
|
|
416
|
+
run in that folder shows it, with the fix, until a later worker there applies the env.
|
|
417
|
+
- `chi self` run in a tool shows what the worker computed (`project env`: `mise (ruby PATH)` or `none (why)`) and the
|
|
418
|
+
chi home (`home`).
|
|
419
|
+
- The REPL modes (`--no-shared`, `--non-interactive`, `--verbose`) run no worker: their tools keep the shell's env.
|
|
420
|
+
|
|
367
421
|
## Delegating
|
|
368
422
|
|
|
369
423
|
The `delegate` tool hands a task to a **child session**: an ordinary chi session in a worker of its own, started in the parent's `working_directory` with the task, verbatim, as its first user message, on the parent's model unless the call names one (`model:` takes a name or an alias, resolved as `--model` is; an unknown host comes back as the tool's `Error:` line; an id the host's saved model list doesn't have starts the child with a `Warning: host 'h' doesn't list model 'x' (did you mean: …?)` line in the result). Children run in parallel and only their **final reply** comes back to the parent: the child's newest `output/<timestamp>.txt` file, which the worker writes at the end of each turn that produced visible text, cut head-and-tail above 64 KB (`DelegateWait.cut`); a reply longer than `max_tool_output_chars` keeps its start and its end (the conclusion), with a gap line naming that `output/` file, where the whole reply is (`ToolOutputCut`'s `:head_tail_ref`; see [Tool output guardrails](internals/tool-guardrails.md#the-runners-cut-max_tool_output_chars)). Nothing of the child's trace enters the parent's context. Nothing is hidden: a child shows in `chi sessions list` (`↳ <parent>`), the web (folded into its parent's card on the strip and in all sessions, whose `▸ N delegates` chip lists the delegates at any depth, see Web UI above; a card of its own with a `↳` chip when its parent isn't listed; `delegated by` in its info bar; the parent's info bar carries a `⑂ N` chip counting its delegates, with how many run, wait, are done, failed, stopped or idle in the tooltip and the waiting ones named in the attention colour; a click opens all sessions), the terminal's status row (a parent's `⑂ 2 running · 1 waiting`, see [Status Line](cli.md#status-line)) and the `list_sessions` tool (`child`; the parent shows as `parent` in the child's own list); the user can `chi --attach <child id>` and steer it mid-run, and `chi send` and `send_note` reach it like any session.
|
data/docs/testing.md
CHANGED
|
@@ -44,7 +44,7 @@ exception's text changes between Ruby versions: write the string yourself).
|
|
|
44
44
|
The web specs run in plain node. One that loads lit-html, itself or through a module it imports (`turn_view.js`,
|
|
45
45
|
`turn_html.js`, `card.js`, `copy.js`, `images.js`, `scope.js`, `command_view.js`, `diff_view.js`, `read_view.js`,
|
|
46
46
|
`question_cards.js`, `stream_handlers.js`, `stage_view.js`, `bubbles.js`, `cards_place.js`, the chips, `info_bar.js`, `info_bar_model.js`, `lists_view.js`, `context_chips.js`, `families_view.js`,
|
|
47
|
-
`chains.js`, `working_set_view.js`, `working_set_rail.js`, `unqueue.js`, `toasts.js`, `notice_links.js`, `start_page.js`, `session_actions.js`, `send_turn.js`, `composer_mount.js`, `dock_reserve.js`,
|
|
47
|
+
`chains.js`, `working_set_view.js`, `working_set_rail.js`, `unqueue.js`, `toasts.js`, `notice_links.js`, `start_page.js`, `start_settings.js`, `start_toolkit.js`, `session_actions.js`, `send_turn.js`, `composer_mount.js`, `dock_reserve.js`,
|
|
48
48
|
`vendor/lit-html.js`) needs a DOM: `import "./dom_shim.js";` as its first import (happy-dom, a devDependency only,
|
|
49
49
|
never shipped; lit reads `document` as it loads). The shim's `markupOf(template)`,
|
|
50
50
|
`htmlOf(nodes)` and `canon(html)` compare markup without lit's marker comments; the DOM writes text with only `&`, `<`, `>`
|
|
@@ -5,7 +5,9 @@ require_relative "config"
|
|
|
5
5
|
require_relative "model_profile"
|
|
6
6
|
require_relative "host_registry"
|
|
7
7
|
require_relative "host_keys"
|
|
8
|
+
require_relative "chi_home"
|
|
8
9
|
require_relative "gem_update"
|
|
10
|
+
require_relative "installed_gem"
|
|
9
11
|
require_relative "bootstrap/probe"
|
|
10
12
|
require_relative "bootstrap/config_writer"
|
|
11
13
|
require_relative "bootstrap/bundles"
|
|
@@ -87,8 +89,10 @@ module Samagotchi
|
|
|
87
89
|
# the process ENV points, not +env+)
|
|
88
90
|
# @param gem_runner [#run] runs `gem install commonmarker` (GemUpdate::Runner)
|
|
89
91
|
# @param gem_present [#call, nil] → whether commonmarker is installed
|
|
92
|
+
# @param home [#current, #record_first!] the chi home (ChiHome)
|
|
90
93
|
def initialize(argv, stdin: $stdin, stdout: $stdout, stderr: $stderr, env: ENV, probe: nil, config_path: nil,
|
|
91
|
-
platform: RbConfig::CONFIG["host_os"], bundles: nil, gem_runner: GemUpdate::Runner.new, gem_present: nil
|
|
94
|
+
platform: RbConfig::CONFIG["host_os"], bundles: nil, gem_runner: GemUpdate::Runner.new, gem_present: nil,
|
|
95
|
+
gem_spec: InstalledGem.spec, home: ChiHome)
|
|
92
96
|
@argv = argv.dup
|
|
93
97
|
@stdin = stdin
|
|
94
98
|
@stdout = stdout
|
|
@@ -99,8 +103,14 @@ module Samagotchi
|
|
|
99
103
|
@platform = platform
|
|
100
104
|
@bundles = bundles || Bootstrap::Bundles.new
|
|
101
105
|
@gem_runner = gem_runner
|
|
102
|
-
|
|
103
|
-
@
|
|
106
|
+
@gem_spec = gem_spec
|
|
107
|
+
@home = home
|
|
108
|
+
# Installed (in the chi home's gems when there is one: chi web runs
|
|
109
|
+
# there), without loading its native extension into this process.
|
|
110
|
+
@gem_present = gem_present || lambda {
|
|
111
|
+
home = @home.current(gem_spec: @gem_spec)
|
|
112
|
+
home ? home.gem?(MARKDOWN_GEM) : Gem::Specification.find_all_by_name(MARKDOWN_GEM).any?
|
|
113
|
+
}
|
|
104
114
|
end
|
|
105
115
|
|
|
106
116
|
# @return [Integer] 0 written (or already there), 1 failed (a failed
|
|
@@ -371,6 +381,7 @@ module Samagotchi
|
|
|
371
381
|
# each. Then Markdown in chi web (#offer_markdown). 1 when a bundle
|
|
372
382
|
# failed; the Markdown step never changes it.
|
|
373
383
|
def install_bundles(dry_run: false)
|
|
384
|
+
record_home unless dry_run
|
|
374
385
|
ok = system_bundle_line(@bundles.sync_system(dry_run: dry_run), dry_run)
|
|
375
386
|
ok = install_profile(CORE, dry_run) && ok
|
|
376
387
|
unless dry_run || !tty? || (members = dev_to_install).empty?
|
|
@@ -409,13 +420,31 @@ module Samagotchi
|
|
|
409
420
|
false
|
|
410
421
|
end
|
|
411
422
|
|
|
412
|
-
#
|
|
413
|
-
#
|
|
423
|
+
# The first chi home (ChiHome.record_first!: the helper's Ruby, else
|
|
424
|
+
# this one), one line; nothing when one is recorded already.
|
|
425
|
+
def record_home
|
|
426
|
+
recorded = @home.record_first!(gem_spec: @gem_spec, launch_path: helper_launch_path)
|
|
427
|
+
@stdout.puts("chi home: #{recorded.ruby} (chi home set to change)") if recorded
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def helper_launch_path
|
|
431
|
+
return nil unless @platform.to_s.include?("darwin")
|
|
432
|
+
|
|
433
|
+
require_relative "desktop/macos"
|
|
434
|
+
Desktop::MacOS.new(env: @env, register: false).launch_path
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
# true when commonmarker is there or `gem install` (the chi home's gem
|
|
438
|
+
# command, else this Ruby's own) put it there; else one line with gem's
|
|
439
|
+
# last word on it.
|
|
414
440
|
def install_markdown_gem
|
|
415
441
|
return true if @gem_present.call
|
|
416
442
|
|
|
417
443
|
@stdout.puts("#{MARKDOWN_GEM}: installing…")
|
|
418
|
-
|
|
444
|
+
home = @home.current(gem_spec: @gem_spec)
|
|
445
|
+
argv = [ChiHome.gem_bin(home), "install", MARKDOWN_GEM, "--no-document"]
|
|
446
|
+
env = ChiHome.install_env(home, env: @env)
|
|
447
|
+
output, ok = env.empty? ? @gem_runner.run(argv) : @gem_runner.run(argv, env: env)
|
|
419
448
|
return true if ok
|
|
420
449
|
|
|
421
450
|
last = output.to_s.lines.map(&:strip).reject(&:empty?).last || "no output"
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mcp
|
|
3
|
-
version: 0.7.
|
|
3
|
+
version: 0.7.3
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Tools from MCP servers (stdio): each server in config.yml bundles: mcp: servers: runs for the session; their tools go into chi's tool index: the model searches them with find_tools and calls one with call_tool (<server>/<tool>); /mcp lists them"
|
|
6
6
|
trust_level: reviewed
|
|
7
7
|
files: {}
|
|
8
8
|
plugin:
|
|
9
9
|
file: plugin.rb
|
|
10
|
-
sha256: sha256:
|
|
10
|
+
sha256: sha256:65532b75b33fa7a58b4a35f9ed32e2705f4e3d323c6f770e71448da5cc9314d0
|
|
11
11
|
# 0.67.0 is the release with chi.tool_source (the core tool index the bundle publishes into).
|
|
12
12
|
requires_chi: ">= 0.121.0"
|