samagotchi 0.82.0 → 0.84.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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -1
  3. data/README.md +2 -2
  4. data/bin/chi +27 -2
  5. data/docs/architecture.md +3 -0
  6. data/docs/cli.md +1 -0
  7. data/docs/configuration.md +52 -9
  8. data/docs/hooks.md +1 -1
  9. data/docs/internals/bundles.md +1 -1
  10. data/docs/memory.md +1 -1
  11. data/docs/plugins.md +85 -3
  12. data/docs/releasing.md +1 -1
  13. data/docs/sessions.md +13 -1
  14. data/docs/sub-agent.md +8 -0
  15. data/lib/samagotchi/bundle_session_settings.rb +212 -0
  16. data/lib/samagotchi/bundles/core/manifest.yml +4 -2
  17. data/lib/samagotchi/bundles/cost-cap/manifest.yml +13 -0
  18. data/lib/samagotchi/bundles/cost-cap/plugin.rb +235 -0
  19. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  20. data/lib/samagotchi/child_reports.rb +25 -8
  21. data/lib/samagotchi/engine.rb +2 -1
  22. data/lib/samagotchi/memory_bundle/builder.rb +3 -2
  23. data/lib/samagotchi/memory_bundle/installed_bundle.rb +6 -3
  24. data/lib/samagotchi/memory_bundle/installer.rb +2 -1
  25. data/lib/samagotchi/memory_bundle/manifest.rb +24 -2
  26. data/lib/samagotchi/memory_bundle/provenance.rb +2 -1
  27. data/lib/samagotchi/plugin/context.rb +40 -3
  28. data/lib/samagotchi/reply_wait.rb +13 -15
  29. data/lib/samagotchi/send_command.rb +24 -7
  30. data/lib/samagotchi/session.rb +16 -1
  31. data/lib/samagotchi/session_metrics/cost.rb +37 -0
  32. data/lib/samagotchi/session_metrics.rb +9 -0
  33. data/lib/samagotchi/session_setup.rb +14 -6
  34. data/lib/samagotchi/terminal_ui/attach_launcher.rb +8 -5
  35. data/lib/samagotchi/terminal_ui.rb +7 -2
  36. data/lib/samagotchi/testing/bundle.rb +31 -5
  37. data/lib/samagotchi/testing/host.rb +26 -1
  38. data/lib/samagotchi/testing/recorder.rb +4 -1
  39. data/lib/samagotchi/tools/delegate_cursor.rb +45 -7
  40. data/lib/samagotchi/tools/delegate_wait.rb +40 -6
  41. data/lib/samagotchi/version.rb +1 -1
  42. data/lib/samagotchi/web/app.rb +13 -4
  43. metadata +5 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 02be0edba759be799428a67ac174beba8a77c2869f3de8f677dec9a14977ae60
4
- data.tar.gz: d28e0688aec5428264fc7a48162a7f4e03ea461371cc48aba4da3c64ecf2e9d8
3
+ metadata.gz: e5e180d3ef3bc5dd32bd646d471b1ef88e11083aba16f6908a8c4588b0173b4c
4
+ data.tar.gz: b337e5690f3094d5f3b0e8528aacbedab734211c9be60ed5ff314f6eb4299ea7
5
5
  SHA512:
6
- metadata.gz: 1df47ccd973923c62848fef02807f3982ede1b86ebf032425c1ecd311014103286803822a290b24f117d705e3e8f845b22cb4804e90cc731a8dd48a3946a3b6f
7
- data.tar.gz: 3c901380929266d60c820ebba9ad3581bdb4392f498fbb43dcef6ba157e675c100a8dba752ee27a79d464692ffb0a407b75ead89961f38c89e6551bf8a9a6ada
6
+ metadata.gz: 4ce619eccbfc69729b160a2b19c65d3b8e91d0f2b641fb6d4eb288d8fd12e9a147529745a8a3b5272270a2fabec6e597f423086c6574cb06f689816b32550568
7
+ data.tar.gz: 4a9922290d92cabab3aac30f17292119cc6e778243a7e1b57bd8260423a27446b34b978346056566619f8e358da8dabbd9e80f8bc3aa9fc198dab99e9b27b38c
data/CHANGELOG.md CHANGED
@@ -8,6 +8,31 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.84.0] - 2026-10-18
12
+
13
+ ### Added
14
+
15
+ - `--set BUNDLE.KEY=VALUE` on `chi` and `chi send --new`: a bundle setting for this session, saved with it and kept
16
+ by forks and continued links. A bundle lists the keys a session may set in its manifest's `session_settings:`;
17
+ its plugin reads them in `ctx.settings`, over config.yml's. `POST /api/sessions` takes `bundle_settings`.
18
+ - The cost-cap bundle takes a session's own cap: `chi send --new --set cost-cap.cap=3 -m "…"` (cost-cap 0.1.1).
19
+
20
+ ## [0.83.0] - 2026-10-18
21
+
22
+ ### Added
23
+
24
+ - Bundles: `ctx.cost` reads the session's spend so far (reported + estimated, side requests included).
25
+ - The cost-cap bundle (in the core profile): a per-session $ cap that pauses the turn with Continue / Stop, `/cap`
26
+ to see or set it; config `bundles: cost-cap: default/child/warn_at`. Existing installs get it with `chi update`
27
+ (core 0.3.2, + cost-cap).
28
+ - Docs: docs/configuration.md "When a change applies" says, per config.yml key, whether an edit applies on the
29
+ next turn, at worker start, at session creation or at `chi web` start.
30
+
31
+ ### Fixed
32
+
33
+ - A delegate reply already taken as a report isn't returned again by `delegate_result` in the same turn.
34
+ - `chi send --wait ID` with no message recognizes a finished turn's reply across time zones.
35
+
11
36
  ## [0.82.0] - 2026-10-18
12
37
 
13
38
  ### Added
@@ -3364,7 +3389,9 @@ and long-lived sessions.
3364
3389
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
3365
3390
  hotkey panel that send selected text or the clipboard to your sessions.
3366
3391
 
3367
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.82.0...HEAD
3392
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.84.0...HEAD
3393
+ [0.84.0]: https://github.com/dm1try/samagotchi/compare/v0.83.0...v0.84.0
3394
+ [0.83.0]: https://github.com/dm1try/samagotchi/compare/v0.82.0...v0.83.0
3368
3395
  [0.82.0]: https://github.com/dm1try/samagotchi/compare/v0.81.0...v0.82.0
3369
3396
  [0.81.0]: https://github.com/dm1try/samagotchi/compare/v0.80.0...v0.81.0
3370
3397
  [0.80.0]: https://github.com/dm1try/samagotchi/compare/v0.79.0...v0.80.0
data/README.md CHANGED
@@ -78,8 +78,8 @@ Messages API, picks the
78
78
  model (or asks, when there are several), sends one test request and writes
79
79
  the config. With a config already there, it adds a `hosts:` entry and keeps
80
80
  the rest of the file. Then it installs the `core` bundles (loop-guard,
81
- check-in, guardrails, slow-commands, agents-md: the safety set for a small
82
- local model) and, on a terminal, offers `dev` (known-names, mcp, btw, skills,
81
+ check-in, guardrails, slow-commands, agents-md, cost-cap: the safety set for a
82
+ small local model, the cost cap inert until you set one) and, on a terminal, offers `dev` (known-names, mcp, btw, skills,
83
83
  source-links). Edit the
84
84
  file later as in [Configure](#configure); `chi bootstrap --help` has the
85
85
  options.
data/bin/chi CHANGED
@@ -82,7 +82,7 @@ ARGV.each do |arg|
82
82
  base = arg.split("=", 2).first
83
83
  # If dashed version is a known flag (registry or hardcoded), reject underscore variant
84
84
  dashed = base.tr("_", "-")
85
- known = %w[--prompt --resume --attach --shared --no-shared --model --thinking --toolkit --tool --no-tool --memory --mute --append-system-prompt --llm-context --llm-context-apply --llm-context-budget --no-interrupt --non-interactive --verbose --no-default-input --port --open --new-token] + Samagotchi::Config.cli_entries.map(&:cli_flag)
85
+ known = %w[--prompt --resume --attach --shared --no-shared --model --thinking --toolkit --tool --no-tool --set --memory --mute --append-system-prompt --llm-context --llm-context-apply --llm-context-budget --no-interrupt --non-interactive --verbose --no-default-input --port --open --new-token] + Samagotchi::Config.cli_entries.map(&:cli_flag)
86
86
  if known.include?(dashed) || known.include?(base)
87
87
  warn "Unknown option: #{arg} (did you mean #{dashed}?)"
88
88
  exit 1
@@ -125,6 +125,11 @@ parser = OptionParser.new do |opts|
125
125
  # --no-tool is taken out of ARGV before parse! (OptionParser would read it
126
126
  # as --tool's negation); this is its help line.
127
127
  opts.separator " --no-tool NAME Leave this tool out of the toolkit (repeatable; a comma list too)"
128
+ opts.on("--set BUNDLE.KEY=VALUE", "A bundle setting for this session, over config.yml's bundles: (repeatable; a key the " \
129
+ "bundle's manifest lists in session_settings:; numbers, true, false and off are typed, the rest is text; " \
130
+ "saved in the session: --set cost-cap.cap=3)") do |v|
131
+ (options[:sets] ||= []) << v
132
+ end
128
133
  opts.on("--memory NAME", "Preload a memory entry into the system prompt (repeatable; a comma list too)") { |m| (options[:memories] ||= []) << m }
129
134
  opts.on("--append-system-prompt FILE", "Put FILE's text into the system prompt, after AGENTS.md (repeatable; saved in the session; " \
130
135
  "config append_system_prompt: is the baseline)") { |f| (options[:append_system_prompt] ||= []) << f }
@@ -225,6 +230,10 @@ if options[:new_token] && !web
225
230
  warn "Error: --new-token goes with chi web (chi web --new-token)"
226
231
  exit 1
227
232
  end
233
+ if options[:sets] && web
234
+ warn "Error: --set is a new session's (chi --set, chi send --new --set), not chi web's"
235
+ exit 2
236
+ end
228
237
 
229
238
  # A scratch session is new, runs here and goes when it ends.
230
239
  if scratch
@@ -372,6 +381,20 @@ if options[:toolkit] || options[:tools] || options[:no_tools]
372
381
  remove: options[:no_tools])
373
382
  end
374
383
 
384
+ # --set BUNDLE.KEY=VALUE: a new session's bundle settings, checked here
385
+ # against the installed bundles (a typo isn't saved).
386
+ if options[:sets]
387
+ require "samagotchi/bundle_session_settings"
388
+ begin
389
+ options[:bundle_settings] = Samagotchi::BundleSessionSettings.from_flags(options[:sets])
390
+ rescue Samagotchi::BundleSessionSettings::Error => e
391
+ warn "Error: #{e.message}"
392
+ exit 2
393
+ end
394
+ # An existing session's run names it among the flags it ignores.
395
+ options[:toolkit_flags] = (options[:toolkit_flags] || []) + ["--set"]
396
+ end
397
+
375
398
  chi_update_hint.call
376
399
  require "samagotchi/launch_mode"
377
400
  launch, launch_note = if scratch
@@ -408,7 +431,8 @@ if launch == :attached
408
431
  thinking: options[:thinking],
409
432
  appended_prompt_files: options[:appended_prompt_files] || [],
410
433
  toolkit: options[:toolkit_value],
411
- toolkit_flags: options[:toolkit_flags] || [])
434
+ toolkit_flags: options[:toolkit_flags] || [],
435
+ bundle_settings: options[:bundle_settings] || {})
412
436
  rescue Samagotchi::TerminalUI::AttachLauncher::Error => e
413
437
  warn "Error: #{e.message}"
414
438
  exit 1
@@ -430,6 +454,7 @@ begin
430
454
  appended_prompt_files: options[:appended_prompt_files] || [],
431
455
  toolkit: options[:toolkit_value],
432
456
  toolkit_flags: options[:toolkit_flags] || [],
457
+ bundle_settings: options[:bundle_settings] || {},
433
458
  non_interactive: options[:non_interactive],
434
459
  model_name: options[:model],
435
460
  scratch: scratch
data/docs/architecture.md CHANGED
@@ -346,6 +346,9 @@ Side requests (the idle recap, a plugin's `ctx.ask_model` such as `/btw`) are co
346
346
  `analytics.json` between turns (a running turn's end saves it). `tokens`, `model_segments` and the turn records
347
347
  stay the turns' only. `SessionMetrics#persist` is serialized (its own mutex, taken before the metrics one), so two
348
348
  threads' saves land in snapshot order.
349
+ A bundle's `ctx.cost` reads the same sums (`SessionMetrics#cost`, a frozen `SessionMetrics::Cost`): the `tokens`
350
+ block's `cost_sum` and `cost_estimate_sum` with the running turn's finished generations folded in, plus the `side`
351
+ block's, so it counts what earlier workers spent too ([plugins.md](plugins.md)).
349
352
 
350
353
  The page's frequent reads of `GET /api/sessions/:id` stay light too:
351
354
  - `?tail=1&recent=1` (each turn's end, a cancel, an answer display): `{tail, session: {id, status,
data/docs/cli.md CHANGED
@@ -204,6 +204,7 @@ controls exit behavior (`--non-interactive`); `--resume` composes with both.
204
204
  | `--model NAME` | Use this model for the run (overrides the configured default and a resumed session's model). |
205
205
  | `--thinking LEVEL` | This session's own thinking level: `off`, `low`, `medium` or `high`, saved in the session and first in the order (`default` unsets it). On `--resume` or `--attach` it is set at once, through a `/thinking` on an attached worker. With `chi web` it is instead the default for the sessions its workers run, below a session's own (as env `SAMAGOTCHI_THINKING_LEVEL` is). See "Thinking" in configuration.md. |
206
206
  | `--toolkit NAME`, `--tool NAME`, `--no-tool NAME` | A new session's toolkit, the tools its prompts declare: `full` (every built-in), `lean`, `child`, one of config's `toolkit.kits` or a bundle's (without `--toolkit`: config's `toolkit.default`), with tools declared on top of it (`--tool`) or left out (`--no-tool`); both repeatable, a comma list too. Saved in the session as given; a respawned worker, a fork and a continue keep it. Ignored with `--attach`/`--resume` (one line, as `--memory`); `/toolkit` changes it. Not on `chi web` (config's `toolkit.default` covers its sessions). See [Toolkits](configuration.md#toolkits). |
207
+ | `--set BUNDLE.KEY=VALUE` | A bundle setting for a new session (repeatable; a later one of the same key wins), laid over config.yml's `bundles: <bundle>:` in what the bundle's plugin reads (`ctx.settings`): `--set cost-cap.cap=3`. The value is typed simply: an integer (`3`, `-1`) or a decimal (`2.5`) is a number, `true` is true, `false` and `off` are false, anything else stays the text as given (`010`, `0x10`, `3:30`, `No`, `null`). For cost-cap, `0`, a negative number or `off` is no cap. Only a key the installed bundle's manifest lists in `session_settings:` is taken: a bundle that isn't installed (or whose plugin doesn't load), a key it doesn't list (a record installed before the bundle listed it says to run `chi update` or `chi bundle upgrade NAME`), or another shape (`--set foo`, `--set a.b.c=1`) is an error naming what it takes, exit 2, nothing started. Saved in the session; a respawned worker, a fork and a continue keep it, a delegate child doesn't. Ignored with `--attach`/`--resume` (one line, as `--toolkit`); refused with `chi web`. See [Per-session settings](plugins.md#per-session-settings). |
207
208
  | `--profile NAME` | Prompt profile (`qwen36` or `gemma4`) for every model in this run, over config and the server's template (same as `--model-profile`, env `SAMAGOTCHI_MODEL_PROFILE`). See "Prompt profile" in configuration.md. |
208
209
  | `--memory NAME` | Preload a memory entry into the system prompt (repeatable; a comma list too). Merged under the config.yml `memories:` baseline. Works attached: the list is stored on the session, so its worker builds the same prompt on every respawn. |
209
210
  | `--append-system-prompt FILE` | Put `FILE`'s text into the system prompt, after AGENTS.md and before the memory indexes (repeatable; relative to the shell's directory). Added after the config.yml `append_system_prompt:` list. A missing or unreadable file is an error, exit 1. Saved in the session, so a respawned worker, a fork and a continue keep it; ignored with `--attach`/`--resume` (one line, as `--memory`). See [Appended prompt files](configuration.md#appended-prompt-files). |
@@ -100,15 +100,15 @@ Behavior:
100
100
  `config: unknown key 'default.modle' (did you mean 'default.model'?)`.
101
101
  Names you choose under the maps below (host names, model ids) don't warn.
102
102
  - Environment variables and CLI flags win over config-file values.
103
- - An edit to the file needs no restart of `chi web`: a new session's worker
104
- reads the file when it starts, and a running chi takes a changed value the
105
- next time it reads that setting (a worker keeps `hosts:` and what it set up
106
- at its start until it is stopped; the web's model picker lists a `hosts:`
107
- edit at once). A worker gets the CLI flags of the chi that started it
108
- through its environment.
109
- - A worker reads `hosts:` from the file as it starts, as every other setting, so a delegate (or any session
110
- started later) sees a host added after its parent started. Hosts come only from config.yml: no environment
111
- variable sets them.
103
+ - An edit to the file needs no restart of `chi web`. A new worker reads the
104
+ file when it starts; a running worker sees only settings whose readers check
105
+ the file again (see [When a change applies](#when-a-change-applies)). The web
106
+ model picker refreshes `hosts:` when the file changes, but a running worker
107
+ keeps its host registry until it is restarted. A worker gets the CLI flags of
108
+ the chi that started it through its environment.
109
+ - A newly started worker reads `hosts:` from the file, so a delegate (or any
110
+ session started later) sees a host added after its parent started. Hosts
111
+ come only from config.yml: no environment variable sets them.
112
112
 
113
113
  This lets you run `chi` without repeating common defaults such as model
114
114
  and llama host/port on every invocation.
@@ -125,6 +125,49 @@ any per-run `--memory` values (config baseline first, deduped). A per-run
125
125
  `--mute NAME` removes an entry from the merged list for that session (see
126
126
  "Muting a memory" in cli.md).
127
127
 
128
+ ## When a change applies
129
+
130
+ The config reader checks the file's modification time and size whenever a
131
+ setting is read. That makes a changed value available to a live reader; it
132
+ does not reload every object that already holds a snapshot. "Worker start"
133
+ includes a worker respawn. A running turn keeps the settings it already
134
+ resolved.
135
+
136
+ | Key | Applies | Note |
137
+ |---|---|---|
138
+ | `default.model` | New session | Existing sessions keep their saved model; change one with `/model`. |
139
+ | `default.input`, `no_default_input`, `no_interrupt` | Next CLI run | These set up that run's first prompt or turn limit. |
140
+ | `default.max_tokens`, `max_tool_output_chars`, `read.*`, `execute.*` except `execute.description`, `image.*` | Next matching request or tool use | An operation already in progress keeps its resolved limits. |
141
+ | `server.host`, `server.port`, `server.transport`, `hosts.*` | Worker start | Includes host addresses, APIs, declared models and host-level settings. The web model picker refreshes `hosts:` on file change; a running worker does not. |
142
+ | `server.open_timeout`, `server.read_timeout`, `server.first_token_timeout` | Worker start | The worker's clients retain the timeouts set when they were built. |
143
+ | `model_aliases` | Next model lookup | A session keeps its resolved model unless it is explicitly switched. |
144
+ | `models.<key>.profile` | Session start or `/model` | It is resolved when that model is selected; editing it does not switch the current model's profile by itself. |
145
+ | Other `models.<key>.*` settings | Next turn or model lookup | Sampling, thinking, vision, context, window, price and steer settings are resolved from the file for their next use. |
146
+ | `thinking.level` | Next turn start | A session's own `/thinking` level is saved in that session and takes precedence; a running turn keeps its level. |
147
+ | `cache.*` | Next request or turn-end warm-up | Cache key and TTL are read for a request; warm-up is checked after a turn. |
148
+ | `retry.max`, `retry.base_delay`, `retry.max_delay` | Worker start | The client's network retry policy is built with its HTTP client. |
149
+ | `retry.empty_answer` | Next empty-answer retry | It is checked when a turn needs to retry an empty answer. |
150
+ | `memories` | Worker start | The running engine holds its preload list. The config list is not saved in the session file, so a respawn reads the current list. |
151
+ | `append_system_prompt` | Worker start for the file list | The config list is not saved in the session file. File text is read when the prompt is built; `/model` rebuilds it, while an already-built prompt is cached. CLI `--append-system-prompt` files are saved in the session. |
152
+ | `toolkit.default`, `toolkit.child` | Session creation | The chosen toolkit is saved; changing the defaults does not move existing sessions. |
153
+ | `toolkit.kits`, `execute.description`, `llm_context.policy` | Worker start | These are held by the engine's toolkit or tool registry. |
154
+ | `thinking.turn_preamble`, `skip_agents_md` | Prompt rebuild | A running engine caches its built prompt; a new worker or `/model` rebuild uses the changed value. |
155
+ | `context.*` except `context.every_seconds`, `llm_context.*` except `llm_context.policy`, `steer.cut_after`, `memory.index_warn_tokens`, `status.line` | Next matching turn, display, poll or write | Readers resolve these settings when they perform that operation. Session-specific LLM context and thinking values remain saved in the session. |
156
+ | `context.every_seconds` | Next context poll | The poller checks it for each source that has no explicit interval. |
157
+ | `guardrails.*` | Next tool call or approval | Guardrail rules and disable lists are reloaded when the config file changes; rule settings take effect on that evaluation. |
158
+ | `hooks`, `bundles` | Worker start | Hooks, plugins and their bundle settings load with the engine. |
159
+ | `recap.*` | Worker start | The engine builds its recap job once. |
160
+ | `session.shared` | Next `chi` invocation | It selects attached-worker or in-process mode for that invocation. |
161
+ | `session.idle_exit_minutes`, `session.keep_empty`, `session.max_children`, `session.delegate_reports`, `session.max_wakes` | Worker start or next matching lifecycle decision | Existing workers keep their idle-exit timer; the other values are read for the next corresponding decision. |
162
+ | `session.retention_*` | Next retention sweep | `chi web` sweeps on its configured schedule and on `GET /api/sessions`; `chi sessions prune` uses the values for that invocation. |
163
+ | `web.*` | `chi web` start | Restart `chi web` to change its bind address, port or page settings. |
164
+ | `log.*` | Process start | Each process holds its configured log sink and level after first use; restart that process. |
165
+ | `history.file` | Next history access | The TUI and web composer resolve its path when they read or append history. |
166
+ | `broadcast.*` | Next `chi broadcast` | Each broadcast invocation reads its targeting and triage settings. |
167
+ | `update.*` | Next `chi update` | The command reads which components to update when it runs. |
168
+ | `kitty.*`, `tmux.*` | `chi desktop upgrade` or desktop update | These settings are copied into the desktop helper's launch file. |
169
+ | `model.profile`, `reasoning_history` | Not from config.yml | These are CLI/environment settings; `models:` and `hosts:` provide their model- and host-specific forms. |
170
+
128
171
  ### Environment variables
129
172
 
130
173
  Every setting has an environment variable (except the ones the table marks
data/docs/hooks.md CHANGED
@@ -592,7 +592,7 @@ Notes:
592
592
  - Ordering: bundle hooks fire by `(priority, bundle_name, hook_name)` (lower priority first), then plain `config.yml` hooks in registration order.
593
593
  - Settings: a hook class with `initialize(settings = {})` gets the bundle's section of `config.yml` `bundles:` (see [Settings](#settings)).
594
594
  - A bundle can also ship a `plugin.rb` whose `chi.on(event)` blocks are bundle hooks too, next to commands and tools; see [Plugins](plugins.md). There `ctx.notify`, `ctx.steer` and the other `ctx` helpers act as this page's `event[:notify]`… do for the event the block runs for.
595
- - Shipped bundles (each in a profile: `core` or `dev`, see [Bundle profiles](memory.md#bundle-profiles-core-and-dev)): `chi bundle install guardrails` (rules, see [Guardrails](guardrails.md#the-guardrails-bundle)) and `chi bundle install known-names` (a hook, see [Guardrails](guardrails.md#the-known-names-bundle)), `chi bundle install source-links` (a hook: announces source refs, see [The source-links bundle](#the-source-links-bundle)), `chi bundle install btw` (a plugin: `/btw`, see [Plugins](plugins.md#the-btw-bundle)), `chi bundle install mcp` (a plugin: tools from MCP servers, see [Plugins](plugins.md#the-mcp-bundle)) `chi bundle install loop-guard` (a plugin: breaks tool-call loops, see [Plugins](plugins.md#the-loop-guard-bundle)), `chi bundle install check-in` (a plugin: checks on a long turn, see [Plugins](plugins.md#the-check-in-bundle)), `chi bundle install slow-commands` (a plugin: commands that ran past the execute limit are run as tasks next time, see [Plugins](plugins.md#the-slow-commands-bundle)), `chi bundle install agents-md` (a plugin: a nested AGENTS.md is steered into the conversation on the first touch under its directory, see [Plugins](plugins.md#the-agents-md-bundle)) and `chi bundle install skills` (a plugin: `/skill`, versions of skills, see [Plugins](plugins.md#the-skills-bundle)).
595
+ - Shipped bundles (each in a profile: `core` or `dev`, see [Bundle profiles](memory.md#bundle-profiles-core-and-dev)): `chi bundle install guardrails` (rules, see [Guardrails](guardrails.md#the-guardrails-bundle)) and `chi bundle install known-names` (a hook, see [Guardrails](guardrails.md#the-known-names-bundle)), `chi bundle install source-links` (a hook: announces source refs, see [The source-links bundle](#the-source-links-bundle)), `chi bundle install btw` (a plugin: `/btw`, see [Plugins](plugins.md#the-btw-bundle)), `chi bundle install mcp` (a plugin: tools from MCP servers, see [Plugins](plugins.md#the-mcp-bundle)) `chi bundle install loop-guard` (a plugin: breaks tool-call loops, see [Plugins](plugins.md#the-loop-guard-bundle)), `chi bundle install check-in` (a plugin: checks on a long turn, see [Plugins](plugins.md#the-check-in-bundle)), `chi bundle install slow-commands` (a plugin: commands that ran past the execute limit are run as tasks next time, see [Plugins](plugins.md#the-slow-commands-bundle)), `chi bundle install agents-md` (a plugin: a nested AGENTS.md is steered into the conversation on the first touch under its directory, see [Plugins](plugins.md#the-agents-md-bundle)), `chi bundle install cost-cap` (a plugin: a per-session $ cap, `/cap`, see [Plugins](plugins.md#the-cost-cap-bundle)) and `chi bundle install skills` (a plugin: `/skill`, versions of skills, see [Plugins](plugins.md#the-skills-bundle)).
596
596
  - Installing a bundle executes its hook code at `Engine` startup. Only install bundles you trust, as you would a gem. Hooks are **not** executed at install time (copy-only); they are `module_eval`'d at `Engine.new` inside per-bundle `Samagotchi::Bundles::<name>` namespaces (no top-level `require` collisions). Keep hook files side-effect-free at load time; do work in `#call` — top-level side effects (require, IO, `at_exit`, global assignment) run once per `Engine.new` (class redefinition is idempotent).
597
597
 
598
598
  Lifecycle:
@@ -27,7 +27,7 @@ user-facing side (what a bundle owns, profiles, needs) is in [memory.md](../memo
27
27
  | `SourceIdentity` (source.rb) | what a source is, for comparing two installs of one name: `shipped:<dir>` (any gem or checkout), `git:<url>` (no `#ref`, trailing `/` or `.git`), `path:<realpath>`; nil when it can't place one |
28
28
  | `PreparedSource` (source.rb) | one source normalized once (dir, owned, git commit): `BundleCommand` reads the bundle's name from its `manifest.yml` (the name for every kind of source, a git URL too) and hands it to the Installer (`prepared:`), so a source is cloned or extracted once per command |
29
29
  | `MemoryFiles` | which top-level `*.md` files of a bundle are its memories (`.select`: the `files:` list, else all but README/LICENSE and the like), `.unshipped` (listed, not shipped) and `.symlinks` (skipped: a symlink is never a memory) |
30
- | `Manifest` | parses/validates `manifest.yml` (`name:` is the bundle's name and a directory under `.bundles/`, so a path such as `../x`, `a/b`, `.` or `..` is refused: `name: … is not a bundle name`; YAML that doesn't parse or isn't a mapping is a `ValidationError` too; files, hooks as `BundleHook`, plugin, scripts, needs, requires_chi, includes, context_providers) and writes it (`Manifest.write`, for Builder). It ignores `requires_chi_next`, a shipped bundle's release-time marker (`script/release/release_tools.rb`, docs/releasing.md) |
30
+ | `Manifest` | parses/validates `manifest.yml` (`name:` is the bundle's name and a directory under `.bundles/`, so a path such as `../x`, `a/b`, `.` or `..` is refused: `name: … is not a bundle name`; YAML that doesn't parse or isn't a mapping is a `ValidationError` too; files, hooks as `BundleHook`, plugin, scripts, needs, requires_chi, includes, context_providers, session_settings: the keys a session's `chi --set` may give, recorded at install in `manifest.json` for `BundleSessionSettings` to check) and writes it (`Manifest.write`, for Builder). It ignores `requires_chi_next`, a shipped bundle's release-time marker (`script/release/release_tools.rb`, docs/releasing.md) |
31
31
  | `Installer` | one install/upgrade/dry run of one bundle (below) |
32
32
  | `AssetInstaller` | the parts that live in `.bundles/<name>/`: hooks, guardrail rules, plugin, scripts; returns `AssetInstaller::Installed` (the paths the record keeps) |
33
33
  | `Merger` | 3-way classification per file: `install`, `fast_forward`, `keep`, `noop`, `conflict` |
data/docs/memory.md CHANGED
@@ -316,7 +316,7 @@ profiles, meta bundles that install a set of them:
316
316
 
317
317
  | profile | bundles |
318
318
  |---|---|
319
- | `core` | loop-guard, check-in, guardrails, slow-commands, agents-md: the recommended safety set |
319
+ | `core` | loop-guard, check-in, guardrails, slow-commands, agents-md, cost-cap: the recommended safety set |
320
320
  | `dev` | known-names, mcp, btw, skills, source-links, github-pr, coordinator, claude-peer, codex-peer |
321
321
 
322
322
  `chi bootstrap` installs `core` and, on a terminal, offers `dev`. An existing
data/docs/plugins.md CHANGED
@@ -28,6 +28,7 @@ plugin:
28
28
  requires_chi: ">= 0.1.28" # optional: a gem-style requirement (">= 0.1.28, < 0.2"); the bundle's hooks/ check it too
29
29
  # requires_chi_next: true # shipped bundles only, ignored by chi: rake release:bump raises requires_chi
30
30
  # to the chi being released (docs/releasing.md)
31
+ session_settings: [cap] # optional: the settings a session may set (chi --set my-bundle.cap=3)
31
32
  needs: [gh] # optional: outside commands it relies on (see memory.md#bundles-that-need-outside-commands)
32
33
  ```
33
34
 
@@ -70,7 +71,9 @@ of [anytime](#anytime-true) and the `save_note` tool (see `chi.tool` below). Ins
70
71
 
71
72
  Settings work as they do for hooks ([hooks.md](hooks.md#settings)). An
72
73
  `initialize` that takes an argument gets the bundle's section of config.yml
73
- `bundles:`, as one Hash with string keys.
74
+ `bundles:`, as one Hash with string keys. It runs once, when the plugin loads, before any session: it never
75
+ sees a session's own `--set` values. A value a session may set is read from `ctx.settings` in the handler or
76
+ command that uses it (see [Per-session settings](#per-session-settings)).
74
77
 
75
78
  ## The API: `register(chi)`
76
79
 
@@ -730,7 +733,8 @@ full label, `plugin.rb (bundle my-bundle)`.
730
733
  | `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 |
731
734
  | `ctx.model` | the model the session runs on now: its resolved ref (`host:id`), right after `/model` too |
732
735
  | `ctx.model_key` | that model's memory overlay key (`<name>.<key>.md`, what `memory_write current_model_only` writes) |
733
- | `ctx.settings` | the bundle's settings, frozen |
736
+ | `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 |
737
+ | `ctx.settings` | the bundle's settings, frozen: config.yml's `bundles: <bundle>:` with the session's own `--set` values laid over it ([Per-session settings](#per-session-settings)); the config alone until the session is bound (in `register` and `chi.ctx`) |
734
738
  | `ctx.data_dir` | `$XDG_STATE_HOME/samagotchi/plugins/<bundle>/`, created on first use. A session's own state goes in `sessions/<id>.json` or `sessions/<id>/` there: chi removes it when the session is deleted, discarded or pruned |
735
739
  | `ctx.log` | `ctx.log.info(:event, key: value)`: debug-log records tagged `plugins`, with `bundle=<bundle>` |
736
740
  | `ctx.messages` | the conversation, as a frozen copy, without the system prompt. While a turn runs, a session worker's (attached, web) adds that turn so far: its prompt, the model's text and the lines merged into it (no tool calls or thinking); the REPL's is the conversation before that turn |
@@ -753,6 +757,19 @@ full label, `plugin.rb (bundle my-bundle)`.
753
757
 
754
758
  The Engine itself is never handed to a plugin.
755
759
 
760
+ ### Per-session settings
761
+
762
+ A bundle's settings are config.yml's, for every session. A setting a session may have of its own (a child's cap,
763
+ say) is one the manifest lists in `session_settings:`; `chi --set BUNDLE.KEY=VALUE` and `chi send --new --set …`
764
+ then give it to a new session, saved with it (a respawn, a fork and a continued link keep it; a delegate child
765
+ doesn't). chi refuses a bundle that isn't installed (or whose plugin doesn't load) and a key the manifest doesn't
766
+ list, so a typo isn't saved. Values are typed simply: an integer (`3`, `-1`) or a decimal (`2.5`) is a number,
767
+ `true` is true, `false` and `off` are false, and anything else stays the text as given (`010`, `0x10`, `3:30`,
768
+ `No`, `null`); `POST /api/sessions` `bundle_settings` types its strings the same way and keeps JSON numbers and
769
+ booleans. `ctx.settings` is the config's section with the session's values laid over it (one level: a session's
770
+ `cap` replaces the config's `cap`). Read such a key from `ctx.settings` where it is used, not in
771
+ `initialize(settings)`: that runs once at load, before any session, and gets the config only.
772
+
756
773
  ## Cards
757
774
 
758
775
  A card is a small framed message with buttons: a title, a body and
@@ -1081,7 +1098,7 @@ CHANGELOG line and bumps the shipped bundles that use it. Anything else under
1081
1098
 
1082
1099
  **Stable**
1083
1100
 
1084
- 1. `Samagotchi::Plugin::Api` / `chi.*` as documented above, including
1101
+ 1. `Samagotchi::Plugin::Api` / `chi.*` and `ctx` as documented above (`ctx.cost`'s value by its methods), including
1085
1102
  `Samagotchi::Plugin::ToolResult`, `Samagotchi::Plugin::ModelError` (and its
1086
1103
  subclass `Samagotchi::Plugin::ModelCancelled`),
1087
1104
  `Samagotchi::Plugin::Service::Stopped`, `Samagotchi::Plugin::Sessions::Error`
@@ -1145,6 +1162,8 @@ bundle.model_answers << "a summary" # the next ask_model's
1145
1162
  bundle.sessions.on(:children) { |all:| [] } # ctx.sessions, scripted and recorded
1146
1163
  bundle.http.stub(:post, "http://127.0.0.1:9000/classify", json: { label: "safe" }) # ctx.http, never the network
1147
1164
  bundle.set(running: false) # the session's values: cwd:, messages:, cancelled: …
1165
+ bundle.set(cost: 2.5) # ctx.cost: reported USD, or { reported:, estimated:, side:, known: }
1166
+ bundle.set(session_settings: { "cap" => 1 }) # the session's --set values (Bundle.load takes session_settings: too)
1148
1167
  bundle.shutdown
1149
1168
  ```
1150
1169
 
@@ -1168,6 +1187,14 @@ bundle.shutdown
1168
1187
  **fails the test at teardown** (RSpec and Minitest adapters: after the cleanup, and only a test that passed), so a
1169
1188
  spec can't reach the network. With plain `Bundle.load` there is no teardown: check `bundle.http.unmatched`.
1170
1189
  `bundle.set(cancelled: true)` makes a call without `cancel:` raise `HttpCancelled`.
1190
+ - **`ctx.cost`** is zero and known unless the test sets it: `bundle.set(cost: 2.5)` (reported USD), a Hash of the
1191
+ parts (`reported:`, `estimated:`, `side:`, `known:`; `total` is reported + estimated) or a callable giving either,
1192
+ called at each read (a counting lambda shows whether the bundle read it).
1193
+ - **`session_settings:`** (on `Bundle.load` and `bundle.set`) are the session's own `--set` values for the bundle,
1194
+ keys its manifest lists in `session_settings:` (another raises `ArgumentError`, as chi refuses it). `ctx.settings`
1195
+ lays them over `settings:` through chi's own `Context#settings`; the plugin's `initialize(settings)` gets
1196
+ `settings:` only. `bundle.set(session_settings:)` moves the session to a new id (`test-session-2`, or the
1197
+ `session_id:` given beside it): chi never changes them within a session, and builds `ctx.settings` once per id.
1171
1198
  - **A hook that raises fails the call** (`Testing::HookFailed`, with the hook's error) where chi would log it and
1172
1199
  go on; `bundle.lenient { … }` collects them in `bundle.errors` instead.
1173
1200
  - **Sandbox.** A test runs with temp config, state, data and cache homes (config.yml, memories, `ctx.data_dir`, the
@@ -1542,6 +1569,61 @@ Not caught (yet):
1542
1569
  - loops across turns;
1543
1570
  - alternating calls (A, B, A, B) that each return something new.
1544
1571
 
1572
+ ## The cost-cap bundle
1573
+
1574
+ `chi bundle install cost-cap` installs the bundle shipped with chi (`chi bootstrap` installs it with the `core`
1575
+ profile; an existing install gets it with `chi update`). It caps what one session spends, so neither you nor the
1576
+ model has to watch `chi sessions stats`: the harness watches the money and the model never sees any of it (no tool,
1577
+ no steer, no prompt text). It is inert without a cap, and there is none by default.
1578
+
1579
+ ```yaml
1580
+ # config.yml
1581
+ bundles:
1582
+ cost-cap:
1583
+ default: 5.00 # USD cap for a session with none set; absent/0/off: no cap
1584
+ child: 2.00 # for delegate children; absent: default
1585
+ warn_at: 0.8 # tell the user (only) once at this share of the cap; 0/off: no warning
1586
+ ```
1587
+
1588
+ - **What counts:** `ctx.cost`'s total: what the provider reported (`usage.cost`), else the estimate from the
1589
+ model's configured price (`hosts.<name>.models.<id>.price`), plus side requests (the idle recap, `/btw`,
1590
+ `ask_model`), including what earlier workers of the session spent.
1591
+ - **When it checks:** before each generation, so before the money is spent. A generation that crosses the cap is
1592
+ caught at the start of the next one: the overshoot is at most one generation (plus any side requests between).
1593
+ A turn whose last generation crossed it ends normally, and the next turn's first generation asks.
1594
+ - **At `warn_at` of the cap** you get one notice, `cost-cap> $2.41 of $3.00 spent (80%)`; a raised cap warns again.
1595
+ - **At the cap** the turn pauses on a question (header "Cost cap"): `Cost cap reached: $3.04 spent of $3.00.
1596
+ Continue raises it to $6.00.` with **Continue** (another cap's worth: $3 → $6 → $9, as many steps as it takes to
1597
+ pass the spend, saved for the session) and **Stop** (the turn stops, `canceled` with `stopped_by: cost-cap`, and a
1598
+ card shows the spend, reported, estimated and side). With no one to ask (`--non-interactive`) it stops.
1599
+ - **The cap of a session:** the one `/cap` or a Continue set for it, else its own `cap:` (`chi --set
1600
+ cost-cap.cap=3`, `chi send --new --set cost-cap.cap=3`; `--set cost-cap.cap=off`, `0` or a negative number for none), else `child:` for a
1601
+ delegate child, else `default:`. Delegates and continued links each have their own cap (a continued link keeps
1602
+ the `--set` cap, not a `/cap` or Continue); there is no family cap.
1603
+ - **A host it can't watch:** a model that reports no cost and has no configured price (a local server, a free
1604
+ model that reports 0) reads unknown: one notice, `the $3.00 cap can't be enforced: <model> reports no cost and has
1605
+ no price`, and it never stops for an unknown cost. A session with some priced generations counts the unpriced
1606
+ ones as $0.
1607
+
1608
+ **`/cap`** (works while a turn runs; the next generation reads it):
1609
+
1610
+ - `/cap`: `cost-cap: $1.20 spent of $3.00 (reported $1.10, estimated $0.10, side $0.02)`, or `cost-cap: no cap
1611
+ ($1.20 spent)`, with `(can't be enforced: no cost reported or priced)` when it can't be.
1612
+ - `/cap 5` or `/cap $5`: the cap (and what Continue adds) becomes $5.00 for this session. A cap at or under the
1613
+ spend is allowed: the next generation asks.
1614
+ - `/cap off` (or `/cap 0`): no cap for this session, over the config's `default:`.
1615
+
1616
+ A parent agent starts a session with a cap of its own with `--set` (`chi send --new --set cost-cap.cap=3 -m "task"`;
1617
+ `cost-cap.cap` is the bundle's one session setting), or by sending `/cap N` as a first message
1618
+ (`chi send --new -m '/cap 3'`).
1619
+ At the cap, `chi send --wait` exits 3 with a question of kind `hook` (header "Cost cap"); `chi answer ID --question
1620
+ Q --option Continue` (or `--option 1`) goes on, `--option Stop` stops the turn ([sub-agent.md](sub-agent.md)). A
1621
+ parent may Continue: nothing restricts it, as with the step limit. A background delegate child at its cap waits on
1622
+ its own card and doesn't wake its parent ([sessions.md](sessions.md#delegating)).
1623
+
1624
+ The state is one file per session, `<data_dir>/sessions/<session id>.json` (`{"cap": 3.0, "step": 3.0}`), removed
1625
+ with the session.
1626
+
1545
1627
  ## The check-in bundle
1546
1628
 
1547
1629
  `chi bundle install check-in` installs the bundle shipped with chi (`chi
data/docs/releasing.md CHANGED
@@ -13,7 +13,7 @@ can run the whole release; the user approves the notes before the tag and the
13
13
  and `rake release:check` enforce it). The system bundle upgrades itself when
14
14
  chi starts.
15
15
  - **Every other shipped bundle** (btw, check-in, guardrails, known-names,
16
- loop-guard, mcp, skills, slow-commands, agents-md, source-links, github-pr, coordinator, claude-peer, codex-peer, and the profiles core and dev) has its own semver in its
16
+ loop-guard, mcp, skills, slow-commands, agents-md, cost-cap, source-links, github-pr, coordinator, claude-peer, codex-peer, and the profiles core and dev) has its own semver in its
17
17
  `manifest.yml` and, when it needs a newer chi, a `requires_chi:` line. They never upgrade by themselves: users run
18
18
  `chi update` (all of them, with the gem) or `chi bundle upgrade NAME`. So:
19
19
  - a change to a bundle's files bumps that bundle's `version:`
data/docs/sessions.md CHANGED
@@ -209,7 +209,8 @@ chi send --new --continues last:3fa2 -m "new day" # the next link of 3fa2's
209
209
  - Exit codes with `--wait`: 0 answered; 3 the turn waits for an answer from you (a question, a hook's question, a guardrail approval, or the step-limit question: see [chi as a sub-agent](sub-agent.md#the-step-limit)), and the session keeps waiting: stderr gets the whole question (`waiting for an answer (approval): …`, its text, the numbered options, whether free text is allowed) and how to answer it (`chi answer ID --question QID --option N`, or `chi --attach ID`, the web); 4 `--timeout S` passed with the turn still running (it goes on; wait again); 1 the turn ended without a reply (canceled, failed or empty), the worker failed or vanished, or the session was stopped; 130 on Ctrl-C, which leaves the turn running (`still running: chi --attach ID`). There is no default timeout.
210
210
  - `--format json` (with `--wait`) prints one JSON object on stdout however the wait ends, for a script or a parent agent (see [chi as a sub-agent](sub-agent.md)): `{"status":"answered","session_id":…,"text":…}`; `{"status":"question",…,"question":{"id","kind","header","text","options","multi_select","allow_freeform","approval"},"answer_with":"chi answer ID --question QID --option N"}`, where `kind` is `question` (the model's), `hook` or `approval` (with the tool, command, cwd, rule, reason and offered scopes); `{"status":"running",…,"detail":…}` (exit 4, and on Ctrl-C); and `failed`, `canceled`, `no_answer`, `error`, `worker_gone` or `stopped` with a `detail` line (exit 1); `failed` adds `error_kind` and `retryable`, `canceled` adds `cancel_reason` and `stopped_by`, when known. A failure before the wait (no such session, a busy one) is `error` too. A usage error (exit 2) prints no JSON.
211
211
  - `--wait ID` with no message (no `-m`, nothing piped) sends nothing: it waits for the session's next reply and prints and exits as above. With a turn running, that turn's reply (its `interrupted_turn` mark names it), not a turn's queued behind it; with the step-limit question standing, the reply of the turn its Continue runs; with an idle session whose last turn already ended, that ended turn is the answer: its reply, or its end when it left no reply — a turn that finished in the gap between waits is reported, not waited for. With none of those (or a worker whose `bridge.json` lacks `turn_ids`), the next reply of any turn. A running session is fine (that is the point), so after exit 3, 4 or 130 an agent waits again this way while the user answers in the web or `chi --attach`; the question pending when it starts doesn't count again. An idle session with no worker waits until something wakes one. With `--new`, or two ids, it is a usage error (exit 2).
212
- - `--continues ID` (with `--new`) starts the next link of that session's chain instead (see *Session chains*): in its folder, on its model (or the one `--model` names), LLM context, thinking level and toolkit, so `--dir`, `--llm-context*`, `--thinking`, `--toolkit`, `--tool` and `--no-tool` are refused beside it; `ID` may be a prefix or `last:ID`, the chain's latest link. Without a message the new link starts idle; `--wait` then has nothing to wait for (a usage error). It prints `<full id> started (continues 3fa2c1d0)`, with `; 2 delegates moved` inside the parentheses when open delegates moved to it; a refusal (continued already, naming the next link; an open delegate that can't move; a turn running) is `chi send: refused: …`, exit 1, and a `--model` it can't start on is `chi send: could not start a session: …`, exit 1, nothing archived. A fallback to the default model (below) prints `chi send: warning: …`.
212
+ - `--set BUNDLE.KEY=VALUE` (with `--new`, repeatable) gives the new session a bundle setting of its own, as `chi --set` does (see *Bundle settings* below): `chi send --new --set cost-cap.cap=3 -m "…"` starts it with a $3 cap. A bundle or key the installed bundles don't offer is a usage error (exit 2), as is `--set` without `--new`.
213
+ - `--continues ID` (with `--new`) starts the next link of that session's chain instead (see *Session chains*): in its folder, on its model (or the one `--model` names), LLM context, thinking level, toolkit and bundle settings, so `--dir`, `--llm-context*`, `--thinking`, `--toolkit`, `--tool`, `--no-tool` and `--set` are refused beside it; `ID` may be a prefix or `last:ID`, the chain's latest link. Without a message the new link starts idle; `--wait` then has nothing to wait for (a usage error). It prints `<full id> started (continues 3fa2c1d0)`, with `; 2 delegates moved` inside the parentheses when open delegates moved to it; a refusal (continued already, naming the next link; an open delegate that can't move; a turn running) is `chi send: refused: …`, exit 1, and a `--model` it can't start on is `chi send: could not start a session: …`, exit 1, nothing archived. A fallback to the default model (below) prints `chi send: warning: …`.
213
214
  - The 16 KiB cap applies: `git diff | chi send --new …` on a big diff is refused; name the branch in the message instead and let the session read it.
214
215
  - A session nobody attaches to stalls at its first question or guardrail ask until someone answers it (`chi answer`, the web, `chi --attach`), and its worker stays up meanwhile: it doesn't idle-exit while a question waits (`chi sessions list` shows it as `waiting`; `chi sessions stop ID` frees it). Otherwise it idle-exits after `session.idle_exit_minutes` like any worker. These are ordinary sessions: delete them like any other.
215
216
 
@@ -243,6 +244,16 @@ declares every tool (full). The file stores only the name: a kit's tools and pro
243
244
  `/toolkit` changes it, saved at once, from the next turn's start. See
244
245
  [Toolkits](configuration.md#toolkits).
245
246
 
247
+ ## Bundle settings
248
+
249
+ A session file's `bundle_settings` (`{"cost-cap": {"cap": 3}}`, `{}` for none and for a session from before them)
250
+ holds the bundle settings it was created with: `chi --set BUNDLE.KEY=VALUE`, `chi send --new --set …`, or `POST
251
+ /api/sessions` `bundle_settings` (an object of the same shape, checked the same way; `400 invalid_bundle_settings`
252
+ otherwise). They never change after: a respawned or woken worker reads the same, and a continue (`continue →`,
253
+ `chi send --new --continues`) and a fork (`ctx.sessions.fork`) copy them, as they copy the toolkit. A delegate
254
+ child starts without (a bundle has its own setting for children, such as cost-cap's `child:`). A bundle's plugin
255
+ reads them in `ctx.settings`, over config.yml's `bundles:` ([Per-session settings](plugins.md#per-session-settings)).
256
+
246
257
  ## Delegating
247
258
 
248
259
  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.
@@ -253,6 +264,7 @@ The `delegate` tool hands a task to a **child session**: an ordinary chi session
253
264
  - A wait that timed out or was canceled still gets its delegate report: neither moves the parent's cursor, so the child's reply comes as a report when its turn ends. The result says so (`chi brings its reply here by itself as a delegate report …`; with `queue`, that it joins the next step or turn) and that `delegate_result` waits again only if the turn needs the reply now. A parent that gets no reports (`session.delegate_reports: off`, a `--no-shared` REPL, `--non-interactive`) is told `delegate_result <id> waits again` instead, and `wait: false`'s result says `delegate_result waits for its reply`.
254
265
  - What a wait returns, `session: <id>` then `status:` and the text, with the status words `chi send --wait --format json` uses: `answered` with the reply after a `---` line; `question` as soon as the child opens a question (the model's own, or a hook's), with the question's text, options and the `chi answer` command for it (nothing is answered for it: `chi answer`, attach or use the web, then wait again); `failed`, `canceled` or `no_answer` when the child's turn ended with nothing to say (its session shows what happened); `error` (its worker crashed) or `stopped` (`chi sessions stop`) at once; `worker_gone` when the child has had no worker for 15 seconds (it crashed without saying so, or idle-exited): `delegate` with `session:` starts it again; `running` on timeout, or when the parent's own turn is canceled (the child keeps running; the next turn can wait again). A reply saved before the child's worker idle-exited is returned at once.
255
266
  - **A child's step-limit question goes back to the parent model**, not to its user: `delegate_result` returns `status: question` with the `chi answer` commands for Continue and Stop, and says the model may instead send a narrower follow-up (`delegate session:`, which drops the question) or report back. A parent's `chi answer` follows `turn.parent_continue` (see [chi as a sub-agent](sub-agent.md#the-step-limit)).
267
+ - **A child at its cost cap** (the [cost-cap bundle](plugins.md#the-cost-cap-bundle), the child's own cap: `child:` in its settings, or `/cap`) asks a `hook` question, "Cost cap": a parent waiting on it (`delegate`, `delegate_result`) gets it back as `status: question` with the `chi answer` commands, `--option Continue` or `--option Stop`. A **background** child (`wait: false`) paused at its cap does not wake its parent: delegate reports skip hook questions (they are the user's, on the child's card) and the approval relay carries approvals only, so the child waits on its card, with the web's bell. The parent sees it at its next `delegate_result`.
256
268
  - **A child's approvals go to the parent's user** (the approval relay). While the parent waits (`delegate`, `delegate_result`), a guardrail approval the child opens reopens as the parent session's own approval: the same card in the parent's web page and attached terminal, with the same facts and diff, a line naming the delegate and its task (`delegate ab12 · fix the flaky spec…`, linked to its session) and the header `Approve delegate ab12's tool call?`. The options are the child's, by index; "for the session" says it is the delegate's session (the session scope keys on the child; repo and rule approvals are one shared store). The user answers once there; the parent's model never sees the approval, only one line per relayed approval in the tool's result, before the reply: `approval relayed to your user: execute: git push origin main → allowed once` (or `denied ("why")`, `dismissed (denied)`, `answered on the child`, `not delivered (the child's worker is gone)`, `refused by the child …`, `still open (your turn was stopped)`). The time the card is open doesn't count against `timeout`. While the parent waits on one child, its other running children's approvals are relayed too, oldest first, one card at a time (`+1 more delegate waiting`); each outcome comes in that child's next result.
257
269
  - The child's own card stays answerable (the first answer wins: answered there, the parent's card closes as "answered in ab12"). While relayed it says `Waiting for approval in parent ab12: answering here works too`; `chi sessions list` shows the child as `waiting (in parent ab12)` (`--format json`: `relayed_to`), and the web's bell rings for the parent's card, not the child's.
258
270
  - How the child trusts it: the parent's POST that says "answered" (Bridge `POST /session/<child>/relay`) carries no answer. The child asks its parent session's live Bridge (`POST /session/<parent>/relay/status`), which reports only what the parent's question flow took from a UI (`RelayDesk`, in memory only), and takes it only for this child, the question pending now, answered. A parent agent's answer on the relay card (a piped or marked `chi --attach PARENT`, `chi answer`) is held to `guardrails.parent_approvals` by the parent and again by the child.
data/docs/sub-agent.md CHANGED
@@ -36,6 +36,10 @@ chi answer ID --question QID (--option N|LABEL)... [--text T] [--timeout S] [--f
36
36
  chi answer ID --question QID --dismiss [--timeout S] [--format json]
37
37
  ```
38
38
 
39
+ A child with a bundle setting of its own, such as a $ cap (the [cost-cap bundle](plugins.md#the-cost-cap-bundle)):
40
+ `chi send --new --wait --format json --set cost-cap.cap=3 -m "task"`. At the cap the wait exits 3 with the cost-cap
41
+ question; `chi answer ID --question QID --option Continue` (or `Stop`) answers it.
42
+
39
43
  `chi answer` waits after answering, the same way `chi send --wait` does, so the
40
44
  next question comes back as exit 3 again. What it waits for is the answered
41
45
  turn: the turn that asked (or, for the step-limit question, the turn it
@@ -105,6 +109,10 @@ A chi parent's `delegate_result` gets a child's step-limit question back as a
105
109
  `delegate session:` (which drops the question), or report back. It isn't
106
110
  relayed to the parent's user the way approvals are.
107
111
 
112
+ A session at its cost cap (the [cost-cap bundle](plugins.md#the-cost-cap-bundle)) waits the same way, as a
113
+ question of kind `hook` with header "Cost cap" and options `Continue` and `Stop`: `--option Continue` raises the cap
114
+ by another cap's worth and the turn goes on, `--option Stop` ends it `canceled` with `stopped_by: cost-cap`.
115
+
108
116
  ## Approvals
109
117
 
110
118
  A chi parent (the `delegate` tool) doesn't go through any of this: its