samagotchi 0.85.0 → 0.87.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 +76 -1
- data/bin/chi +39 -2
- data/docs/architecture.md +36 -5
- data/docs/bundle-authoring.md +125 -0
- data/docs/cli.md +51 -3
- data/docs/configuration.md +6 -5
- data/docs/guardrails.md +112 -39
- data/docs/hooks.md +19 -3
- data/docs/internals/anthropic.md +2 -2
- data/docs/internals/bundles.md +11 -2
- data/docs/internals/context-telemetry.md +19 -1
- data/docs/internals/engine.md +1 -1
- data/docs/memory.md +1 -1
- data/docs/plugins.md +59 -2
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +19 -7
- data/docs/sub-agent.md +39 -25
- data/docs/testing.md +7 -2
- data/lib/samagotchi/answer_command.rb +21 -25
- data/lib/samagotchi/bridge/client_event.rb +50 -0
- data/lib/samagotchi/bridge/sse_writer.rb +6 -2
- data/lib/samagotchi/bridge.rb +22 -11
- data/lib/samagotchi/bridge_client.rb +2 -2
- data/lib/samagotchi/bundle_command/check.rb +348 -0
- data/lib/samagotchi/bundle_command/scaffold.rb +135 -0
- data/lib/samagotchi/bundle_command/test_run.rb +89 -0
- data/lib/samagotchi/bundle_command.rb +125 -2
- data/lib/samagotchi/bundles/core/manifest.yml +2 -2
- data/lib/samagotchi/bundles/cost-cap/manifest.yml +4 -4
- data/lib/samagotchi/bundles/cost-cap/plugin.rb +4 -1
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -1
- data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +4 -2
- data/lib/samagotchi/bundles/guardrails/manifest.yml +1 -1
- data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +3 -3
- data/lib/samagotchi/bundles/known-names/manifest.yml +2 -2
- data/lib/samagotchi/bundles/ruby/manifest.yml +11 -0
- data/lib/samagotchi/bundles/ruby/plugin.rb +74 -0
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/self_map.md +2 -1
- data/lib/samagotchi/cli/parent_wait.rb +3 -1
- data/lib/samagotchi/client_id.rb +1 -1
- data/lib/samagotchi/config.rb +195 -115
- data/lib/samagotchi/context_status.rb +1 -1
- data/lib/samagotchi/continue_offer.rb +2 -1
- data/lib/samagotchi/engine.rb +4 -4
- data/lib/samagotchi/guardrail_wiring.rb +4 -0
- data/lib/samagotchi/guardrails/approval.rb +12 -2
- data/lib/samagotchi/guardrails/load_failures.rb +17 -3
- data/lib/samagotchi/guardrails/parent_approvals.rb +14 -70
- data/lib/samagotchi/guardrails/parent_policy.rb +327 -0
- data/lib/samagotchi/guardrails/verdict.rb +6 -3
- data/lib/samagotchi/hook_routes.rb +5 -3
- data/lib/samagotchi/hooks/bundle_loader.rb +3 -1
- data/lib/samagotchi/hooks/registry.rb +13 -6
- data/lib/samagotchi/hooks.rb +5 -0
- data/lib/samagotchi/kernel_loop.rb +5 -2
- data/lib/samagotchi/llm/anthropic_assembly.rb +1 -1
- data/lib/samagotchi/memory_bundle/shas.rb +127 -0
- data/lib/samagotchi/memory_bundle.rb +1 -0
- data/lib/samagotchi/parent_answers_command.rb +80 -0
- data/lib/samagotchi/parent_report.rb +122 -30
- data/lib/samagotchi/pending_question.rb +8 -4
- data/lib/samagotchi/plugin/api.rb +16 -0
- data/lib/samagotchi/plugin/context.rb +11 -5
- data/lib/samagotchi/plugin/loader.rb +5 -3
- data/lib/samagotchi/question_desk.rb +39 -25
- data/lib/samagotchi/queued_command.rb +5 -4
- data/lib/samagotchi/relay.rb +5 -1
- data/lib/samagotchi/relay_verifier.rb +4 -5
- data/lib/samagotchi/reply_wait.rb +14 -5
- data/lib/samagotchi/send_command.rb +103 -12
- data/lib/samagotchi/session.rb +26 -2
- data/lib/samagotchi/session_commands.rb +45 -8
- data/lib/samagotchi/session_setup.rb +10 -8
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +18 -8
- data/lib/samagotchi/terminal_ui/attached_loop.rb +25 -12
- data/lib/samagotchi/terminal_ui/thinking_line.rb +5 -9
- data/lib/samagotchi/terminal_ui.rb +20 -11
- data/lib/samagotchi/testing/boundary.rb +0 -3
- data/lib/samagotchi/testing/bundle.rb +7 -0
- data/lib/samagotchi/testing/host.rb +6 -4
- data/lib/samagotchi/testing/install.rb +119 -0
- data/lib/samagotchi/testing/recorder.rb +13 -4
- data/lib/samagotchi/testing.rb +1 -0
- data/lib/samagotchi/tools/builtins.rb +2 -1
- data/lib/samagotchi/tools/delegate.rb +4 -1
- data/lib/samagotchi/tools/delegate_wait.rb +16 -7
- data/lib/samagotchi/tools/task_runtime.rb +5 -3
- data/lib/samagotchi/turn_checkpoint.rb +64 -2
- data/lib/samagotchi/turn_flow.rb +10 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +48 -10
- data/lib/samagotchi/web/public/chunk_router.js +8 -19
- data/lib/samagotchi/web/public/ctx.js +12 -26
- data/lib/samagotchi/web/public/stream_handlers.js +1 -1
- data/lib/samagotchi/web/server.rb +4 -0
- data/lib/samagotchi/worker.rb +27 -5
- data/lib/samagotchi/worker_inbound.rb +4 -4
- metadata +12 -2
- data/lib/samagotchi/guardrails/parent_continue.rb +0 -43
data/docs/guardrails.md
CHANGED
|
@@ -67,8 +67,9 @@ Who answers:
|
|
|
67
67
|
answerable too, the first answer wins. The parent's model sees only the
|
|
68
68
|
outcome.
|
|
69
69
|
- A parent agent running chi as a sub-agent (`chi send --wait` exits 3 with
|
|
70
|
-
the approval): `chi answer` may deny it,
|
|
71
|
-
|
|
70
|
+
the approval): `chi answer` may deny it, give "Allow once" under
|
|
71
|
+
`guardrails.parent_answers: soft` (the default; never a wider scope), or
|
|
72
|
+
leave it for the user. See [Answers from a parent agent](#answers-from-a-parent-agent).
|
|
72
73
|
|
|
73
74
|
The model gets one line on a deny. A rule's or hook's deny reads
|
|
74
75
|
`[execute] Error: denied by guardrail (rule git-push, bundle guardrails): git push publishes commits. The user was not asked. Do not retry it or reach the same result another way; ask the user how to proceed.`
|
|
@@ -99,39 +100,105 @@ with one warning, and chi starts with no stored approvals (more asks, nothing lo
|
|
|
99
100
|
`/guardrails` lists the rules and approvals; `/guardrails revoke N` removes one.
|
|
100
101
|
The file tools can't write the store.
|
|
101
102
|
|
|
102
|
-
###
|
|
103
|
+
### Answers from a parent agent
|
|
103
104
|
|
|
104
|
-
A chi parent's `delegate` children don't need any of this
|
|
105
|
-
to the user on the parent's own card (the approval relay). This section is
|
|
106
|
-
path for other agents, and for a chi parent that can't host the relay (a
|
|
107
|
-
`--no-shared` REPL, `--non-interactive`): there the model gets the
|
|
105
|
+
A chi parent's `delegate` children don't need any of this for approvals: they
|
|
106
|
+
go to the user on the parent's own card (the approval relay). This section is
|
|
107
|
+
the path for other agents, and for a chi parent that can't host the relay (a
|
|
108
|
+
`--no-shared` REPL, `--non-interactive`): there the model gets the question
|
|
108
109
|
block and the rules below. With the relay, the same rules still hold for a
|
|
109
110
|
parent model that answers its own relay card (a piped or marked `chi --attach`
|
|
110
111
|
on the parent): that answer is a parent agent's, checked by the parent and
|
|
111
112
|
again by the child.
|
|
112
113
|
|
|
113
114
|
When another agent (Claude Code, Codex, a script) runs chi with
|
|
114
|
-
`chi send --wait`,
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
115
|
+
`chi send --wait`, a question comes back to it as exit 3 and `chi answer`
|
|
116
|
+
answers it. Every question's options carry an **effect**, set by whoever asks:
|
|
117
|
+
|
|
118
|
+
- `answer`: a plain choice (the model's own questions, an untagged plugin or
|
|
119
|
+
hook option);
|
|
120
|
+
- `deny`: Deny on an approval, Stop on the step-limit question or a plugin's
|
|
121
|
+
stop option;
|
|
122
|
+
- `spend`: more time and tokens, no new permission (Continue on the step-limit
|
|
123
|
+
question, cost-cap's Continue);
|
|
124
|
+
- `permit`: one tool call (an approval's "Allow once");
|
|
125
|
+
- `permit_wide`: more than one call (this session, this repo or directory,
|
|
126
|
+
the rule).
|
|
127
|
+
|
|
128
|
+
One setting says which effects a parent may pick:
|
|
121
129
|
|
|
122
130
|
```yaml
|
|
123
131
|
guardrails:
|
|
124
|
-
|
|
132
|
+
parent_answers: soft # hard | standard | soft (the default)
|
|
125
133
|
```
|
|
126
134
|
|
|
127
|
-
`
|
|
128
|
-
option
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
135
|
+
- `hard`: `deny` and `answer` only. A parent can deny an approval
|
|
136
|
+
(`--option Deny --text "why"`, `--text` alone or `--dismiss`), Stop a
|
|
137
|
+
step-limit question and answer plain questions.
|
|
138
|
+
- `standard`: also `spend`: a parent may Continue a turn (or cost-cap's cap).
|
|
139
|
+
- `soft` (the default): also `permit`: `chi answer --option "Allow once"`
|
|
140
|
+
goes through, picked by the option's scope, not its label.
|
|
141
|
+
|
|
142
|
+
A parent never picks `permit_wide`: the wider scopes stay with the web and the
|
|
143
|
+
terminal. An approval that doesn't offer "once" can't be allowed from a
|
|
144
|
+
parent, and neither can one whose offered scopes are missing or don't fit its
|
|
145
|
+
options: then only Deny goes. A refused pick exits 1 with what to do instead
|
|
146
|
+
(`only Allow once (guardrails.parent_answers: soft) can be given here: deny it
|
|
147
|
+
(--option Deny --text WHY), and tell your user`), never how to allow it. The
|
|
148
|
+
question block chi prints says what the parent may give: under `soft` an
|
|
149
|
+
approval's block lists the Allow once command next to the deny; under
|
|
150
|
+
`standard` and `hard` it says allowing is up to the user. An invalid value
|
|
151
|
+
warns and is `hard`.
|
|
152
|
+
|
|
153
|
+
#### A session's own level
|
|
154
|
+
|
|
155
|
+
A session may carry its own `parent_answers`, set when it is created (`chi
|
|
156
|
+
--parent-answers LEVEL`, `chi send --new --parent-answers LEVEL`) and kept by
|
|
157
|
+
its respawns, forks and continues; a `delegate` child starts with its parent's,
|
|
158
|
+
as it is then. It can only tighten: config.yml's value is the ceiling, and the
|
|
159
|
+
stricter of the two applies, in `chi answer`'s check (from the session file),
|
|
160
|
+
the worker's (from its session) and in what the question block offers. A refusal
|
|
161
|
+
then names it: `a parent agent may only stop this turn here (parent_answers:
|
|
162
|
+
hard (this session))`. A looser one is saved as given with a warning
|
|
163
|
+
(`soft is looser than config.yml's guardrails.parent_answers: standard;
|
|
164
|
+
standard applies`) and takes effect only if config.yml is loosened later. A
|
|
165
|
+
value that isn't a level reads as `hard`.
|
|
166
|
+
|
|
167
|
+
`/parent-answers` shows the level that applies, the session's own and
|
|
168
|
+
config.yml's; `/parent-answers LEVEL` sets the session's own, `/parent-answers
|
|
169
|
+
default` drops it (config.yml's applies). Anyone may tighten it, a parent too
|
|
170
|
+
(`chi send ID -m "/parent-answers hard"`). Loosening it (toward `soft`, or
|
|
171
|
+
`default`) is yours alone: the line must come from you at the REPL, an attached
|
|
172
|
+
terminal or the web composer. chi refuses it (`loosening parent_answers is the
|
|
173
|
+
user's: …`) from `chi send` (a parent's or yours: the worker can't tell them
|
|
174
|
+
apart), from a piped or marked `chi --attach` or REPL, from `chi -p
|
|
175
|
+
--non-interactive`, from a delegate's follow-up and from a plugin; `chi answer`
|
|
176
|
+
has no command route. `--parent-answers` on `--attach`/`--resume` is ignored,
|
|
177
|
+
never sent as the command.
|
|
178
|
+
|
|
179
|
+
config.yml is read each time an answer is checked; a session's own value stays
|
|
180
|
+
as saved and binds whenever it is the stricter of the two. A session saved as
|
|
181
|
+
`standard` under a `hard` config.yml follows `hard`, and `standard` again once
|
|
182
|
+
config.yml is loosened. A `delegate` child keeps the value it started with,
|
|
183
|
+
whatever its parent changes later.
|
|
184
|
+
|
|
185
|
+
A parent agent on a real terminal (a PTY: tmux, a kitty window) with no agent
|
|
186
|
+
marker set looks like you, and can loosen it; so can one that edits a session
|
|
187
|
+
file (`<id>.json`, read again by the next worker that wakes for it). That is
|
|
188
|
+
not a security boundary either: config.yml still caps the result.
|
|
189
|
+
|
|
190
|
+
A setting form waits for a running turn to end, as `/toolkit`'s does: a
|
|
191
|
+
tightening typed while a child's question is pending applies after that turn,
|
|
192
|
+
and a parent may answer the question meanwhile under the level before it.
|
|
193
|
+
|
|
194
|
+
`soft` suits a capable parent model: it can let a child run one more command
|
|
195
|
+
it asked about, still with every guardrail rule in force. For a weak or
|
|
196
|
+
unknown parent model, set `standard` or `hard` (and see
|
|
197
|
+
[chi-nah](#an-external-guard-chi-nah) for a deterministic guard on the calls
|
|
198
|
+
themselves).
|
|
132
199
|
|
|
133
200
|
A child that has to work in another folder (`write-outside-repo`,
|
|
134
|
-
`git-outside-repo`)
|
|
201
|
+
`git-outside-repo`) gets at most Allow once per call this way. Start it in that folder instead
|
|
135
202
|
(`chi send --new --dir PATH`), or have your user allow "rule … in this repo"
|
|
136
203
|
once on the web (it is stored for later sessions in that repo), or switch the
|
|
137
204
|
rules off: `guardrails: {disable: [write-outside-repo, git-outside-repo]}`.
|
|
@@ -140,17 +207,18 @@ Whatever the setting, a parent can't allow a call on chi's own files: a
|
|
|
140
207
|
`write`, `edit` or `execute` on the config dir (config.yml, installed bundles'
|
|
141
208
|
rules), the hooks dir or the approval store, by its rule (`chi-config`,
|
|
142
209
|
`chi-hooks`, `shell-touches-chi`), its paths or its command. "Allow once" on a
|
|
143
|
-
config.yml rewrite would allow everything from then on, so those
|
|
144
|
-
user: `chi answer` and the worker refuse the allow
|
|
145
|
-
allow it`.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
210
|
+
config.yml rewrite would allow everything from then on, so those questions are
|
|
211
|
+
the user's alone (`user_only`): `chi answer` and the worker refuse the allow
|
|
212
|
+
with `only the user can allow it`. The same holds for the guardrails bundle's
|
|
213
|
+
other user-only rules (`chi-context-cmd`, `chi-broadcast`,
|
|
214
|
+
`prompt-memory-write`, `model-overlay-write`, and `chi-answer-piped` /
|
|
215
|
+
`chi-answer-http`, which answer a grandchild's question as the user).
|
|
216
|
+
|
|
217
|
+
A plugin's or a hook's question (`ask_user`) tags its options the same way
|
|
218
|
+
(`effects:`, see [Plugins](plugins.md)); an untagged one is a plain `answer`.
|
|
219
|
+
|
|
220
|
+
The guardrails settings (`enabled`, `mode`, `small_models`, `parent_answers`) are
|
|
221
|
+
read from config.yml only (a session's own `parent_answers` can only tighten it): they have no environment variable, and a worker
|
|
154
222
|
chi starts unsets every `SAMAGOTCHI_GUARDRAILS_*` it would inherit. What a
|
|
155
223
|
parent's environment does choose is the config dir: `XDG_CONFIG_HOME` picks
|
|
156
224
|
which config.yml chi reads, and a worker the parent starts or wakes (`chi
|
|
@@ -159,11 +227,11 @@ points at (a relative one made absolute from the parent's directory). chi
|
|
|
159
227
|
doesn't record the config dir per session.
|
|
160
228
|
|
|
161
229
|
The worker checks it too. `chi answer` marks its answers as a parent's
|
|
162
|
-
(`client_id: "cli:answer"`), and the worker's Bridge refuses such
|
|
163
|
-
beyond what the *worker's* config.yml permits (`403
|
|
164
|
-
`chi answer` exits 1 with the
|
|
165
|
-
at that moment. Answers from the web and an attached
|
|
166
|
-
|
|
230
|
+
(`client_id: "cli:answer"`), and the worker's Bridge refuses such a pick
|
|
231
|
+
beyond what the *worker's* config.yml permits (`403 parent_answer_refused`
|
|
232
|
+
with its `reason`; `chi answer` exits 1 with the line), checked against the
|
|
233
|
+
question pending at that moment. Answers from the web and an attached
|
|
234
|
+
terminal keep every option.
|
|
167
235
|
|
|
168
236
|
A parent may also type its answer into chi itself: a piped `chi --attach ID`
|
|
169
237
|
or `printf '2\n' | chi --no-shared -p …`. So chi treats an answer as a
|
|
@@ -177,7 +245,11 @@ person at a terminal with no marker keeps every scope.
|
|
|
177
245
|
This guards an honest but eager parent, not a hostile one: it is not a
|
|
178
246
|
security boundary. The worker's Bridge and the web's answer route on localhost
|
|
179
247
|
take an answer from any local process, which can leave the marker out (or
|
|
180
|
-
post through the web) and get every scope.
|
|
248
|
+
post through the web) and get every scope. In the same way a parent can
|
|
249
|
+
loosen a session's own level by posting `/parent-answers` with a forged
|
|
250
|
+
`tui:<pid>` or `web:<tab>` client id to the Bridge or the web's command route,
|
|
251
|
+
by editing the session file, or by running `chi --attach` on a PTY with the
|
|
252
|
+
markers cleared. chi's instructions for parents ([chi as a
|
|
181
253
|
sub-agent](sub-agent.md)) tell them to use `chi answer` only.
|
|
182
254
|
|
|
183
255
|
## Protected paths
|
|
@@ -511,7 +583,8 @@ only by the user: these change a model's system prompt for later sessions (see
|
|
|
511
583
|
`shell-touches-chi`'s text heuristic); and answering chi's
|
|
512
584
|
questions around `chi answer`: `chi --attach` or `chi -p` with stdin from a
|
|
513
585
|
pipe, a here-string or a file, not `/dev/null` (`chi-answer-piped`), and `curl`/`wget` to a
|
|
514
|
-
session's `/answer` route (`chi-answer-http`), both once or for the session
|
|
586
|
+
session's `/answer` route (`chi-answer-http`), both once or for the session, and only by the
|
|
587
|
+
user (a parent agent allowing one would answer as the user).
|
|
515
588
|
`chi answer` itself isn't asked about. It denies writes into `.git/hooks`.
|
|
516
589
|
|
|
517
590
|
In strict mode (`guardrails: { mode: strict }`) it also asks before `git
|
data/docs/hooks.md
CHANGED
|
@@ -89,9 +89,12 @@ class Watchful
|
|
|
89
89
|
# A single-select question through the question flow (REPL, attached
|
|
90
90
|
# TUI, web); returns {selected: [...], freeform:, selected_indices:}
|
|
91
91
|
# or nil when there is no one to ask (--non-interactive), the
|
|
92
|
-
# question was dismissed, or the options
|
|
92
|
+
# question was dismissed, or the options (or effects) were invalid.
|
|
93
|
+
# effects: says what an option does for a parent agent's answer (see
|
|
94
|
+
# below): Run lets one call through, so it is :permit.
|
|
93
95
|
answer = event[:ask_user].call(question: "#{event[:call][:name]}: #{event[:params]}\nRun it?",
|
|
94
|
-
options: ["Run", "Deny"], header: "my guard", allow_freeform: false
|
|
96
|
+
options: ["Run", "Deny"], header: "my guard", allow_freeform: false,
|
|
97
|
+
effects: { "Run" => :permit, "Deny" => :deny })
|
|
95
98
|
event[:guardrail].deny!("the user said no") unless answer&.dig(:selected)&.first == "Run"
|
|
96
99
|
when :before_generation
|
|
97
100
|
# Cancel the running turn: a warn notice with the reason, then the
|
|
@@ -112,6 +115,19 @@ class Watchful
|
|
|
112
115
|
end
|
|
113
116
|
```
|
|
114
117
|
|
|
118
|
+
**Tag the options that spend or permit.** A question can be answered by a
|
|
119
|
+
parent agent (`chi answer`, a piped `chi --attach`), and
|
|
120
|
+
`guardrails.parent_answers` (or the session's own, stricter one) decides which options it may pick by their
|
|
121
|
+
`effects:` (label => effect): `:spend` (more time or money: a Continue),
|
|
122
|
+
`:permit` (one call), `:permit_wide` (more than one), `:deny` (a Stop or a
|
|
123
|
+
Deny); an option left out is a plain `:answer`, which a parent may always
|
|
124
|
+
pick. A label the options don't have, or an unknown effect, makes the call
|
|
125
|
+
nil (warned), like bad options. Text alone and a dismissal always pass for a
|
|
126
|
+
parent (the worker takes text alone even with `allow_freeform: false`), so
|
|
127
|
+
they must never spend or permit. See
|
|
128
|
+
[Guardrails](guardrails.md#answers-from-a-parent-agent). `effects:` needs a
|
|
129
|
+
chi newer than 0.85.0 (an older one raises on the keyword).
|
|
130
|
+
|
|
115
131
|
`event[:hook]` is the label the notices carry: `known_names.rb (bundle
|
|
116
132
|
known-names)` for a bundle hook, `audit.rb (config)` for a config hook,
|
|
117
133
|
`turn hook` for one registered at runtime.
|
|
@@ -592,7 +608,7 @@ Notes:
|
|
|
592
608
|
- Ordering: bundle hooks fire by `(priority, bundle_name, hook_name)` (lower priority first), then plain `config.yml` hooks in registration order.
|
|
593
609
|
- Settings: a hook class with `initialize(settings = {})` gets the bundle's section of `config.yml` `bundles:` (see [Settings](#settings)).
|
|
594
610
|
- 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)), `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)).
|
|
611
|
+
- 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)), `chi bundle install ruby` (a plugin: an RSpec run with no examples at all, a hint, see [Plugins](plugins.md#the-ruby-bundle)) and `chi bundle install skills` (a plugin: `/skill`, versions of skills, see [Plugins](plugins.md#the-skills-bundle)).
|
|
596
612
|
- 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
613
|
|
|
598
614
|
Lifecycle:
|
data/docs/internals/anthropic.md
CHANGED
|
@@ -97,8 +97,8 @@ index: `message_start` (served model, usage), `content_block_start` (text, think
|
|
|
97
97
|
back); a `tool_use` input is an object; a thinking block always has a `signature` (`""` when none came).
|
|
98
98
|
- `finish_reason` from `FINISH_REASONS`: `end_turn`/`stop_sequence` → `stop`, `max_tokens` and
|
|
99
99
|
`model_context_window_exceeded` → `length`, `tool_use` → `tool_calls`, `refusal` → `refusal`; anything else as it is.
|
|
100
|
-
- Usage in the OpenAI shape the rest of chi reads (`TokenUsage`, `SessionMetrics`, `ContextStatus`, the
|
|
101
|
-
`ctx.js`): Messages counts `input_tokens` without the cache, so `prompt_tokens` is input + cache reads + cache
|
|
100
|
+
- Usage in the OpenAI shape the rest of chi reads (`TokenUsage`, `SessionMetrics`, `ContextStatus`, and the
|
|
101
|
+
Bridge's chunk `counts` the web's `ctx.js` reads): Messages counts `input_tokens` without the cache, so `prompt_tokens` is input + cache reads + cache
|
|
102
102
|
writes, with `cached_tokens` and `cache_write_tokens` under `prompt_tokens_details`. Every field is optional and the
|
|
103
103
|
later value wins; all zeros is no usage (`#usage_payload`: Fireworks' `message_start` carries only zeros).
|
|
104
104
|
|
data/docs/internals/bundles.md
CHANGED
|
@@ -22,7 +22,11 @@ user-facing side (what a bundle owns, profiles, needs) is in [memory.md](../memo
|
|
|
22
22
|
|
|
23
23
|
| Piece | Role |
|
|
24
24
|
|---|---|
|
|
25
|
-
| `BundleCommand` (`lib/samagotchi/bundle_command.rb`) | `chi bundle install/upgrade/uninstall/status/diff/list/build/trash`; runs the Installer, prints its summary, runs the conflict agent step; refuses a source already installed under another name (`installed_under_another_name?`: a git URL bundle recorded under its repo's name before the manifest named it) |
|
|
25
|
+
| `BundleCommand` (`lib/samagotchi/bundle_command.rb`) | `chi bundle install/upgrade/uninstall/status/diff/list/build/trash/new/test/check`; runs the Installer, prints its summary, runs the conflict agent step; refuses a source already installed under another name (`installed_under_another_name?`: a git URL bundle recorded under its repo's name before the manifest named it) |
|
|
26
|
+
| `BundleCommand::Check` (bundle_command/check.rb) | `chi bundle check [PATH] [--fix] [--set K=V]`: a bundle being written, checked as an install takes it (docs/cli.md lists the codes); parses the manifest from `Shas.parseable` (a placeholder sha is a `sha` finding, not a YAML error), reads the user's installed bundles only for `clash`, and installs and loads through `Testing::Install` in a `Testing::Sandbox` with `force: true`, the log's echoes swallowed |
|
|
27
|
+
| `BundleCommand::Scaffold` (bundle_command/scaffold.rb) | `chi bundle new NAME`: the starting files (block-form `plugin:` entry, `requires_chi` at this chi, a kit test that passes as written); the manifest's shas are filled through `Shas.refresh` |
|
|
28
|
+
| `BundleCommand::TestRun` (bundle_command/test_run.rb) | `chi bundle test`: `RbConfig.ruby -I <chi's lib>` on `test/**/*_test.rb` (Minitest) or `spec/` (RSpec, as `rake bundles:specs`), under `Bundler.with_unbundled_env` when Bundler's runtime is loaded |
|
|
29
|
+
| `Shas` (shas.rb) | a manifest's `sha256:` lines: `.refresh(dir[, text])` rewrites them line by line (the layout kept; a `files:` entry it can't place raises `Shas::Error`), `.stale` → `[Stale(file, old, new)]` (`old` `""` for a placeholder, nil for a plugin / hook / script entry with no sha line), `.parseable` (a placeholder `sha256: sha256:` isn't YAML). Plain Ruby: `rake bundles:sha` / `bundles:check` (`script/release/release_tools.rb`) and `chi bundle check --fix` share it |
|
|
26
30
|
| `SourceNormalizer` (source.rb) | source → local dir; a git/zip/tar extract is *owned* and removed after the install |
|
|
27
31
|
| `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
32
|
| `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 |
|
|
@@ -47,7 +51,12 @@ and checks the recorded sha256 (and `requires_chi`) before loading.
|
|
|
47
51
|
The checks live in `Hooks::BundleLoader.load` and `Plugin::Loader.load_bundle`; the loading itself is
|
|
48
52
|
`BundleLoader.load_hook_file` (one hook file, into a given `namespace:`) and `Plugin::Loader.register_file` (one
|
|
49
53
|
plugin file: build, `register`, `commit!`), which raise instead of logging: the bundle test kit
|
|
50
|
-
(`Samagotchi::Testing::Bundle`) loads a bundle's source through them, unchecked.
|
|
54
|
+
(`Samagotchi::Testing::Bundle`) loads a bundle's source through them, unchecked. `Samagotchi::Testing::Install` is the
|
|
55
|
+
checked path in a sandbox: the Installer, then `ExtensionLoad` (bundle hooks, then `load_plugins`), returning the
|
|
56
|
+
Installer's warnings, this bundle's load failures and what it registered (`Install.registered`, also the parity
|
|
57
|
+
spec's shape). A load failure carries `LoadFailures::Failure#location` (the first backtrace frame inside the
|
|
58
|
+
bundle, or a SyntaxError's own line), which `Install` maps from the installed copy (`.bundles/<name>/plugin/`,
|
|
59
|
+
`hooks/`) back to the source dir.
|
|
51
60
|
|
|
52
61
|
## Installer#run, in order
|
|
53
62
|
|
|
@@ -30,7 +30,8 @@ Emission behavior:
|
|
|
30
30
|
synthetic char-based estimate (`src=estimate`). The guidance text is dynamic
|
|
31
31
|
and escalates with the bucket: healthy → proceed normally; moderate → prefer
|
|
32
32
|
targeted/range reads; elevated → be concise, avoid large re-reads; critical →
|
|
33
|
-
|
|
33
|
+
write a short handoff note (state, next step) so the work can go on from it,
|
|
34
|
+
and delegate broad work to subagents. With custom
|
|
34
35
|
`context.status_thresholds` the top bucket reads critical, the one below it
|
|
35
36
|
elevated, the next moderate, and the first bucket (and the rest below) healthy.
|
|
36
37
|
|
|
@@ -85,6 +86,23 @@ Configuration:
|
|
|
85
86
|
|
|
86
87
|
The `context.*` settings are read once, when the turn's `ContextStatus` is built, so a change counts from the next turn.
|
|
87
88
|
|
|
89
|
+
Per-chunk counts on the SSE stream: the Bridge sends each `generation_chunk` with chi's own `counts`
|
|
90
|
+
(`Bridge::ClientEvent`, from `ContextUsage.normalize` of the provider's payload) instead of the payload, and the
|
|
91
|
+
web's meter (`ctx.js#extractCtxPct`) counts their `total_tokens` against the window (their
|
|
92
|
+
`context_window_tokens` when the server reported one, else `generation_started`'s) or the budget when smaller, as
|
|
93
|
+
`ContextStatus` does. The rules:
|
|
94
|
+
|
|
95
|
+
- `ctx_pct` is not in `counts`: normalize computes it without the session's LLM context budget.
|
|
96
|
+
- Counts are cumulative for the request and appear only when the server reported any; a missing key means unknown,
|
|
97
|
+
never 0. Fireworks' all-zero `message_start` gets no usage (`AnthropicAssembly#usage_payload`), and other zeros
|
|
98
|
+
read as nil (`TokenUsage.first_positive`).
|
|
99
|
+
- `prompt_tokens` includes cached tokens: the Anthropic assembly adds the cache read and write in, and for llama.cpp
|
|
100
|
+
`TokenUsage.from_payload` reads `tokens_evaluated` (the whole prompt) before `timings.prompt_n` (the uncached part),
|
|
101
|
+
an order `spec/bridge/client_event_spec.rb` pins on a real fixture.
|
|
102
|
+
- `context_window_tokens` is there only when the payload reported a window (`n_ctx`, `context_window`); llama.cpp's
|
|
103
|
+
`/completion` chunks don't.
|
|
104
|
+
- A remote host that reports usage only in its last chunk moves the meter at the generation's end, as before.
|
|
105
|
+
|
|
88
106
|
Context window lookup (`ContextWindow.resolve`, the first that answers wins; the event's `window_source` / the line's
|
|
89
107
|
`window_src` names it):
|
|
90
108
|
|
data/docs/internals/engine.md
CHANGED
|
@@ -478,7 +478,7 @@ Turn events (the main ones; `Events` in `events.rb` keeps the shared sets):
|
|
|
478
478
|
| `:reminder_injected` | due reminders went into the turn | `{reminders}` |
|
|
479
479
|
| `:context_status` | every request (unless `context.status` is off), the current estimate before it; and outside any turn after `/model` (or when a session is loaded on another model than its last turn ran on), to the observers only (`Engine#announce_switched_context!`) | `{iteration, status, usage, bucket, source}`; the switch's form has no `iteration`: `{usage, bucket, context_window_tokens, context_window_source}`, with `usage.source: "switch"`, and `usage` nil when the new window doesn't resolve (internals/context-telemetry.md) |
|
|
480
480
|
| `:generation_started` | a request begins | `{iteration, context_window_tokens, context_window_source, price, model_ref}` (`price`: the turn's `ModelPrice#to_h`, nil without one; `model_ref`: the turn's model, asked `host:model`, added by the Engine's stream handler from the model `#generate` resolved after the `before_turn` hooks, so a hook's switch is the one named; native adds `profile`, `profile_source`) |
|
|
481
|
-
| `:generation_chunk` | each streamed chunk | `{iteration, content, text, thinking, payload, tool_call?}` |
|
|
481
|
+
| `:generation_chunk` | each streamed chunk | `{iteration, content, text, thinking, payload, tool_call?}` (SSE clients get the Bridge's projection instead: `{iteration, text, thinking, tool_call?, counts?}`, architecture.md) |
|
|
482
482
|
| `:generation_retrying` | the transport retries the request, or the chat loop asks again for a step whose stream dropped mid-answer (`restarted: true`: what the step streamed is void, and the web's live step, the TUI's activity line, the Bridge's TurnAccumulator and the stream hooks' watch start it over) | `{iteration, attempt, max_retries, next_delay, error_class, error_message, status, restarted?}` |
|
|
483
483
|
| `:generation_completed` | a generation ended, or was cut | `{iteration, content_length, thinking_chars, served_model, requested_model, finish_reason, ...}`; a cut adds `stopped_by`, `stop_reason`; the Engine adds `speed`, `tokens`, `model_segments` |
|
|
484
484
|
| `:generation_cancelled` | the turn was cancelled mid-loop | `{iteration, reason, stopped_by}` |
|
data/docs/memory.md
CHANGED
|
@@ -317,7 +317,7 @@ profiles, meta bundles that install a set of them:
|
|
|
317
317
|
| profile | bundles |
|
|
318
318
|
|---|---|
|
|
319
319
|
| `core` | loop-guard, check-in, guardrails, slow-commands, agents-md, cost-cap: the recommended safety set |
|
|
320
|
-
| `dev` | known-names, mcp, btw, skills, source-links, github-pr, coordinator, claude-peer, codex-peer |
|
|
320
|
+
| `dev` | known-names, mcp, btw, skills, source-links, github-pr, coordinator, claude-peer, codex-peer, ruby |
|
|
321
321
|
|
|
322
322
|
`chi bootstrap` installs `core` and, on a terminal, offers `dev`. An existing
|
|
323
323
|
install gets neither by itself: `chi bundle install core` (or `dev`).
|
data/docs/plugins.md
CHANGED
|
@@ -8,6 +8,9 @@ own.
|
|
|
8
8
|
This is the first version of the plugin API. More of it comes later: see
|
|
9
9
|
[Not yet](#not-yet).
|
|
10
10
|
|
|
11
|
+
Writing a bundle? Start with [bundle-authoring.md](bundle-authoring.md): the `chi bundle new` / `test` / `check`
|
|
12
|
+
loop, the event shapes, the kit, the pitfalls.
|
|
13
|
+
|
|
11
14
|
## A bundle with a plugin
|
|
12
15
|
|
|
13
16
|
```
|
|
@@ -742,7 +745,7 @@ full label, `plugin.rb (bundle my-bundle)`.
|
|
|
742
745
|
| `ctx.prompt_files` | the files whose text is in the session's system prompt now: the AGENTS.md chain (the work tree top down to the cwd) and the appended files (`append_system_prompt:`, `--append-system-prompt`); absolute paths, in prompt order, `[]` before the first build |
|
|
743
746
|
| `ctx.notify(text, level: :info, fallback_for: nil)` | one line to the user, labelled by the bundle (`my-bundle> …`). Every UI shows it, during a turn (a tool, a hook) or between turns (a command). `fallback_for: :display` marks a line that repeats the answer's display (`event[:present]`), which a UI that renders the display leaves out (`fallback_for: :editor`: a file the viewer could open from a ref in its editor): see [Hooks](hooks.md#what-a-hook-can-do-the-runtime). The keyword needs chi 0.35.0 (it raises on an older chi) |
|
|
744
747
|
| `ctx.card(title:, body: "", actions: [], level: :info, id: nil)` | a card in every UI, returning its id: see [Cards](#cards) |
|
|
745
|
-
| `ctx.ask_user(question:, options:, header: nil, allow_freeform: false)` | a single-select question through the question flow: `{selected:, freeform:, selected_indices:}`, or nil (no one to ask, cancelled, bad options) |
|
|
748
|
+
| `ctx.ask_user(question:, options:, header: nil, allow_freeform: false, effects: nil)` | a single-select question through the question flow: `{selected:, freeform:, selected_indices:}`, or nil (no one to ask, cancelled, bad options or effects). `effects:` tags the options a parent agent's answer is held to (label => `:spend`, `:permit`, `:permit_wide` or `:deny`; untagged = `:answer`): tag every option that spends or permits, and never let text alone or a dismissal do either ([hooks.md](hooks.md#what-a-hook-can-do-the-runtime)). It needs a chi newer than 0.85.0 |
|
|
746
749
|
| `ctx.cancelled?` | whether the running turn was cancelled (a long tool should stop) |
|
|
747
750
|
| `ctx.steer(text)` | put text into the running turn: its own user message at the loop's next boundary, shown as `my-bundle> nudged: …`. The model reads it with a header naming your bundle (`[Steer from the my-bundle plugin, mid-task. …]`); the session keeps the raw text. Returns true when queued, false with no turn running (it never starts one; that is `ctx.sessions`' send). Dropped (logged) if the model answers or the turn ends first. Safe from any thread: an anytime command, a hook (false from `:after_turn`), your own |
|
|
748
751
|
| `ctx.stop_turn(reason)` | stop the running turn after a warn notice with the reason; true when it stopped one now. In a `:before_tool_call` block it also denies that call |
|
|
@@ -1157,6 +1160,7 @@ bundle.tool("my_tool", path: "a.txt") # → the text the mode
|
|
|
1157
1160
|
verdict = bundle.tool_call({ name: "execute", content: "rm -rf /" })
|
|
1158
1161
|
verdict.deny? # a real Guardrails::Verdict (votes: verdict.votes)
|
|
1159
1162
|
bundle.notices.map(&:text) # also cards, steers, stops, generation_stops, questions
|
|
1163
|
+
bundle.questions.last.effects # an ask_user's tags as given: { "Continue" => :spend }
|
|
1160
1164
|
bundle.answer({ selected: "yes" }) # the next ask_user's answer
|
|
1161
1165
|
bundle.model_answers << "a summary" # the next ask_model's answer
|
|
1162
1166
|
bundle.sessions.on(:children) { |all:| [] } # ctx.sessions, scripted and recorded
|
|
@@ -1205,6 +1209,16 @@ bundle.shutdown
|
|
|
1205
1209
|
own sandbox, its bundles shut down after it (chi's shipped bundles keep theirs in `<bundle>/spec/`,
|
|
1206
1210
|
docs/testing.md). **Minitest:** `require "samagotchi/testing/minitest"` and subclass
|
|
1207
1211
|
`Samagotchi::Testing::Case` (the same helpers).
|
|
1212
|
+
- **Installed, not loaded from source:** `Samagotchi::Testing::Install.run(dir, settings: {})` (or
|
|
1213
|
+
`bundle.install`) installs the bundle into the sandbox as `chi bundle install` does (sha256 and `requires_chi`
|
|
1214
|
+
checked) and loads the installed copy with chi's own loaders. It returns `warnings` (the Installer's),
|
|
1215
|
+
`plugin_failures` / `hook_failures` (each with the `location` in your source, `plugin.rb:12`) and `registered`
|
|
1216
|
+
(commands, tools, hooks per event, inits, toolkits, tool and peer sources). A name chi ships raises
|
|
1217
|
+
`MemoryBundle::Installer::InstallError` unless `force: true`.
|
|
1218
|
+
- **From the command line:** `chi bundle test [PATH]` runs a bundle's `test/**/*_test.rb` (or `spec/`) on the
|
|
1219
|
+
Ruby and lib chi runs on, and `chi bundle check [PATH] [--fix]` runs `Install` (and the manifest, sha and
|
|
1220
|
+
`requires_chi` checks) in a sandbox ([cli.md](cli.md#writing-a-bundle-new-test-check)). `chi bundle new NAME`
|
|
1221
|
+
writes a test to start from.
|
|
1208
1222
|
|
|
1209
1223
|
## The btw bundle
|
|
1210
1224
|
|
|
@@ -1595,7 +1609,10 @@ bundles:
|
|
|
1595
1609
|
- **At the cap** the turn pauses on a question (header "Cost cap"): `Cost cap reached: $3.04 spent of $3.00.
|
|
1596
1610
|
Continue raises it to $6.00.` with **Continue** (another cap's worth: $3 → $6 → $9, as many steps as it takes to
|
|
1597
1611
|
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.
|
|
1612
|
+
card shows the spend, reported, estimated and side). With no one to ask (`--non-interactive`) it stops. A parent
|
|
1613
|
+
agent answering it (`chi answer`) may Continue under `guardrails.parent_answers: soft` or `standard`, and only Stop
|
|
1614
|
+
under `hard` (config.yml's, or the session's own when stricter) (Continue is tagged `:spend`; see [Guardrails](guardrails.md#answers-from-a-parent-agent)). `/cap N` is
|
|
1615
|
+
a command, not an answer: a parent that sends it (`chi send ID -m '/cap 10'`) raises the cap under any strategy.
|
|
1599
1616
|
- **The cap of a session:** the one `/cap` or a Continue set for it, else its own `cap:` (`chi --set
|
|
1600
1617
|
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
1618
|
delegate child, else `default:`. Delegates and continued links each have their own cap (a continued link keeps
|
|
@@ -2180,6 +2197,46 @@ skill install card) works the same.
|
|
|
2180
2197
|
and `VERDICT OK` wakes chi once to close the round. Needs chi 0.67.0 (the closing
|
|
2181
2198
|
wake: the chi released with this bundle).
|
|
2182
2199
|
|
|
2200
|
+
## The ruby bundle
|
|
2201
|
+
|
|
2202
|
+
`chi bundle install ruby` (or the `dev` profile) installs the bundle shipped
|
|
2203
|
+
with chi (`lib/samagotchi/bundles/ruby/plugin.rb`): one `chi.on` hook
|
|
2204
|
+
(`:after_tool_call`), and no memory file.
|
|
2205
|
+
|
|
2206
|
+
An RSpec run that finds no examples prints `0 examples, 0 failures`, which
|
|
2207
|
+
reads green: the model takes the suite as passing and moves on with code no
|
|
2208
|
+
test ever loaded. It usually means a path that matched no file, a filter
|
|
2209
|
+
(`-e`, `--tag`) that matched no example, or a `.rspec --pattern` that hid
|
|
2210
|
+
the specs (a nested project's `.rspec` overrides one). The bundle notices
|
|
2211
|
+
that run and steers a hint into the running turn, which the user sees as
|
|
2212
|
+
`ruby> nudged: …`:
|
|
2213
|
+
|
|
2214
|
+
```
|
|
2215
|
+
rspec ran 0 examples: check the path, the filter (-e/--tag) or the .rspec --pattern
|
|
2216
|
+
```
|
|
2217
|
+
|
|
2218
|
+
It is a nudge for the model, not a denial: the turn goes on, and the model
|
|
2219
|
+
can say why the run was empty. It never runs anything.
|
|
2220
|
+
|
|
2221
|
+
- **The command.** Only an `execute` counts, and its command must run
|
|
2222
|
+
rspec: one of its simple commands (`&&`, `||`, `;`, `|`, a newline, so
|
|
2223
|
+
after a leading `cd x &&` too) starts with `rspec`, a path ending in
|
|
2224
|
+
`/rspec` (`bin/rspec`, `./bin/rspec`), or `parallel_rspec`, once a leading
|
|
2225
|
+
`X=…` assignment, `bundle exec` and `time` are left out. A command that
|
|
2226
|
+
only names rspec (`grep rspec README`, `echo "bundle exec rspec"`) is not
|
|
2227
|
+
a run.
|
|
2228
|
+
- **The output.** Every `<N> examples, <M> failures` line in the execute's
|
|
2229
|
+
output counts (RSpec's summary, with or without `, N pending`), and the
|
|
2230
|
+
hint comes when all of them are 0. `parallel_rspec` prints one per
|
|
2231
|
+
process, so a single process with examples leaves its 0 out of the total.
|
|
2232
|
+
- **No summary line, no hint.** A spec that fails to load, a path that
|
|
2233
|
+
doesn't exist, a command killed at its limit: the output already says what
|
|
2234
|
+
is wrong, and the hint would only repeat it.
|
|
2235
|
+
|
|
2236
|
+
Nothing is remembered, so the next 0-example run hints again: it can be
|
|
2237
|
+
the same mistake on another command, or a different one. A hint the model
|
|
2238
|
+
answers before joining the turn is dropped, as any steer is.
|
|
2239
|
+
|
|
2183
2240
|
## Shutdown
|
|
2184
2241
|
|
|
2185
2242
|
When the REPL exits, or a session's worker exits (an idle exit, `/exit`, a
|
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, cost-cap, 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, ruby, 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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Sessions are plain files — no DB. Each session is `~/.local/state/samagotchi/sessions/<uuid>.json` (XDG-aware via `XDG_STATE_HOME`) plus a sidecar dir `<uuid>/` with `input/`/`notes/`/`output/`/`owner.lock`/`bridge.json` (`Session.session_dir`), and `tool_output/` once a long `execute` result was cut: its whole output, which the cut result names (`ToolOutputFiles`, the newest 50 kept; see [Tool output guardrails](internals/tool-guardrails.md#the-runners-cut-max_tool_output_chars)). Besides the messages and turn state, the JSON keeps `used_memory_names` (what the session read), `preloaded_memory_names` (`--memory`) and `muted_memory_names` (`--mute`), the last two written before the worker starts so a respawn builds the same prompt, `prompt_notes`, the model notes the system prompt carried at its last build (`[{name, scope, chars, digest}]`, see [Memory](memory.md#model-notes)), `parent_id`, the session that delegated this one (see *Delegating*; `null` otherwise), and `continues`, the session this one continues (the previous link of a chain, see *Session chains*; `null` otherwise); files from before those keys read as empty lists, no parent and no previous link. `model_name` is the resolved model ref (an alias applied, `host:id` when it names a host), so a resumed session keeps its model when an alias is retargeted; `model_typed` is the name it was given as when that differs (an alias), for the `models:` lookup after a resume (`null` otherwise).
|
|
4
4
|
|
|
5
|
-
**Input files:** a message for a session's worker waits in `input/<timestamp>.json` (`SessionInbox.write_input`: `prompt`, `client_id`, `enqueued_id`, and `images`, `no_interrupt` and `delivery` when they apply; `delivery` is written only for `"queue"`). While a turn runs, the worker merges them at each step boundary in name order, except a queued one or one with images: those wait for the turn's end and run one turn each, in name order, and a file after them still merges (`Worker#pending_input_drain`). A queue is sent only to a worker whose `bridge.json` names the `delivery` feature; an older one would steer it, so `SessionManager.deliver_turn` refuses it (409 `delivery_unavailable`), and any other 4xx the Bridge answers is a refusal too, never a fallback file (except a 413, a body over its size limit: that one still goes in as a file, its `delivery` kept). **Reply files:** a turn that ends with visible text leaves its reply in `output/<timestamp>.txt`, or `output/<timestamp>-<enqueued_id>.txt` when the turn ran an input file with an id (`SessionInbox.write_output`), and the session's `last_turn` keeps that `enqueued_id`; so a wait for one queued message (`chi send --queue --wait`, `ReplyWait`'s `turn:`) finds its own turn's reply or end, never another turn's. The names still sort by time, and every reader takes `*.txt`.
|
|
5
|
+
**Input files:** a message for a session's worker waits in `input/<timestamp>.json` (`SessionInbox.write_input`: `prompt`, `client_id`, `enqueued_id`, and `images`, `no_interrupt` and `delivery` when they apply; `delivery` is written only for `"queue"`). While a turn runs, the worker merges them at each step boundary in name order, except a queued one or one with images: those wait for the turn's end and run one turn each, in name order, and a file after them still merges (`Worker#pending_input_drain`). A queue is sent only to a worker whose `bridge.json` names the `delivery` feature; an older one would steer it, so `SessionManager.deliver_turn` refuses it (409 `delivery_unavailable`), and any other 4xx the Bridge answers is a refusal too, never a fallback file (except a 413, a body over its size limit: that one still goes in as a file, its `delivery` kept). A session command recognized in a file runs without a turn; its completed outcome is kept at `input/settled/<enqueued_id>` so `chi send --wait` ends instead of waiting for a turn that will not run. **Reply files:** a turn that ends with visible text leaves its reply in `output/<timestamp>.txt`, or `output/<timestamp>-<enqueued_id>.txt` when the turn ran an input file with an id (`SessionInbox.write_output`), and the session's `last_turn` keeps that `enqueued_id`; so a wait for one queued message (`chi send --queue --wait`, `ReplyWait`'s `turn:`) finds its own turn's reply or end, never another turn's. The names still sort by time, and every reader takes `*.txt`.
|
|
6
6
|
|
|
7
7
|
**Removing a queued message:** a message queued for after the running turn (`chi send --queue`, the web's Alt+Enter, `/queue TEXT` in a terminal), or one with images (it waits for the turn's end too), can be taken back while it still waits: the worker's Bridge (`POST /session/:id/turn/dequeue {enqueued_id}`, the `unqueue` feature) deletes its input file and every UI drops it (`:turn_dequeued`). It is a race with the turn's end, decided by the same rename the worker uses to take the file: once a turn took it the answer is `409 already_started` (it runs), and an id the worker no longer knows is `404 unknown_enqueued_id`. A plain message sent without `--queue` is not removable (`409 not_queued`): a running turn's next step may take it at any moment. A wait for its turn (`chi send --queue --wait`, a delegate follow-up's wait) ends at once with `removed` (the tombstone `input/removed/<enqueued_id>` says so), and a `chi -p` run whose queued prompt went ends with exit 1. The `200` carries the removed `prompt`: the web and the attached terminal put it back into the composer to edit (not its images). The images a removed message named stay in the session's `images/`. Not removable: a queued session command (`/queue /model x`: it waits in the worker, not in `input/`), a message in a stopped worker's inbox (the next worker runs it), and the plain REPL's (`--no-shared`) own queue.
|
|
8
8
|
|
|
@@ -27,7 +27,7 @@ A session is deleted if **expired by age OR overflow by count** (unless `keep_st
|
|
|
27
27
|
|
|
28
28
|
**A worker outlives whoever started it:** a spawned worker leaves its spawner's process tree (it is forked again, in a session of its own, and reparented to init), so closing the terminal of a `chi web`, or a tool tearing down a background `chi send --wait` and its children, no longer takes the worker and its turn with it.
|
|
29
29
|
|
|
30
|
-
**A worker killed mid-turn:** while a turn runs, its worker keeps it on disk (`TurnCheckpoint`): the session file has the turn so far (the prompt from the turn's start, then the finished tool steps and lines merged into it) and `interrupted_turn: {id, started_at, at, iteration, enqueued_id}` (`enqueued_id`: the input the turn runs, so any process can tell which turn is running; a continue turn carries its offer turn's), written as the turn starts, at the loop's first iteration boundary and then at most every 10 s (`updated_at` moves with it). The turn's end saves the real messages without the mark. A worker that died without ending its turn (SIGKILL, a crash of the process) leaves the mark, and loses what came after its last written boundary: at most the last 10 s of steps, plus the step in flight. Each checkpoint write also saves the turn's metrics so far to `analytics.json` (tokens, speeds, timings, its finished tool calls), as the record a killed turn leaves: canceled, `cancellation_reason: worker_stopped`; the turn's own end writes over it. The next worker to start for the session (a `chi send`, `--resume`, `--attach`, the web), or a `--no-shared` REPL resuming it, settles the turn before anything else (`TurnCheckpoint.settle`, log `interrupted_turn_settled`): a note `the previous turn was cancelled (worker-stopped) after Ns` goes at the tail, the status goes idle and the mark goes; `last_turn` stays the previous turn's (a new one saved before the turn a `chi send --wait` woke the worker for would end that wait at once). The session shows the turn's prompt and steps with that note; the dead turn is not run again, and the model reads on its next turn that it was cut.
|
|
30
|
+
**A worker killed mid-turn:** while a turn runs, its worker keeps it on disk (`TurnCheckpoint`): the session file has the turn so far (the prompt from the turn's start, then the finished tool steps and lines merged into it) and `interrupted_turn: {id, started_at, at, iteration, enqueued_id}` (`enqueued_id`: the input the turn runs, so any process can tell which turn is running; a continue turn carries its offer turn's), written as the turn starts, at the loop's first iteration boundary and then at most every 10 s (`updated_at` moves with it). The turn's end saves the real messages without the mark. A worker that died without ending its turn (SIGKILL, a crash of the process) leaves the mark, and loses what came after its last written boundary: at most the last 10 s of steps, plus the step in flight. Each checkpoint write also saves the turn's metrics so far to `analytics.json` (tokens, speeds, timings, its finished tool calls), as the record a killed turn leaves: canceled, `cancellation_reason: worker_stopped`; the turn's own end writes over it. The next worker to start for the session (a `chi send`, `--resume`, `--attach`, the web), or a `--no-shared` REPL resuming it, settles the turn before anything else (`TurnCheckpoint.settle`, log `interrupted_turn_settled`): a note `the previous turn was cancelled (worker-stopped) after Ns` goes at the tail, the status goes idle and the mark goes; `last_turn` stays the previous turn's (a new one saved before the turn a `chi send --wait` woke the worker for would end that wait at once); the dead turn's end is recorded under its `enqueued_id` instead, in `<id>/input/settled/<enqueued_id>` (a `last_turn` record: canceled, `cancel_reason: worker_stopped`), which only a wait for that very turn reads (`chi send --queue --wait`, `chi send --wait ID --turn`, a continue's wait): it ends `canceled` with `cancel_reason: worker_stopped`, detail "its worker exited mid-turn", instead of timing out. A reply the turn wrote before its worker died is still its answer. The session shows the turn's prompt and steps with that note; the dead turn is not run again, and the model reads on its next turn that it was cut.
|
|
31
31
|
|
|
32
32
|
**Stopping a worker:** `chi sessions stop ID` marks the session stopped (an empty `<id>/stopped` file, not a session.json field, which a worker saving at that moment would write over; a resume removes it), sends its worker TERM and waits (up to 10 s) until the worker has let go of `owner.lock`. A running turn is canceled over the Bridge first, with reason `session_stopped` (the turn's line ends "(session stopped)", its note says `cancelled (session-stopped)`, and a command it was running says "stopped as the session was stopped"; a bare signal to the worker makes it "stopped by a signal to chi"; an older worker takes the reason as `manual`); the TERM stops whatever still runs as a Stop with reason `signal` and saves it (a spawned worker traps TERM, HUP and INT: `WorkerSignals`; a turn not ended within 3 s, 0.5 s after a stop or a second signal, is cut and the session saved as it was), and a stopped worker still saves its kept turn, since the marker, not the saved status, says stopped. Any TERM, HUP or INT does the same to a worker outside a stop: it leaves (log `worker stop reason=signal`) and the next send wakes a new one, so a `chi --resume ID` right after spawns a fresh worker. That is also how to restart a worker still running an older chi, which the attached terminal reports when a command or a question dismiss gets a 404, and the web when an answer, a dismiss, a command or a cancel does (`501 not_supported`; `BridgeClient.stale_worker_message`). A command this chi has that the older worker doesn't (`/llm-context`, the web's `llm ctx` chip, against a 0.39.0 worker) says the same, not `not a session command` (`SessionCommands.builtin_name`); a typo keeps its `400 unknown_command`. The web's `POST /api/sessions/:id/stop` waits the same way, for 2 s. A command sent during a turn that waits for its end (`/model X`, `!cmd`; see *Typing during a turn* in [cli.md](cli.md)) and hasn't run when the worker leaves (a stop, a crash) is answered `command_ran {status: "dropped"}`, so no UI keeps showing it waiting.
|
|
33
33
|
|
|
@@ -209,10 +209,11 @@ chi send --new --continues last:3fa2 -m "new day" # the next link of 3fa2's
|
|
|
209
209
|
- `--new` prints `<full id> started` on stdout. With `--wait` that line (and the `sent` line for an existing session) goes to stderr, so stdout is exactly the answer: the reply of the turn the message started, in full. It is the worker's `output/` file, the same one `delegate` reads; a turn that ends with only tool calls and no text has none.
|
|
210
210
|
- `--wait` takes one session: `--new` or one id. A session with a running turn is refused (`busy: a turn is running; wait or attach`, exit 1): the message would merge into it, and its reply would come back as the answer. `--queue --wait` is the way in: the message runs as a turn of its own after the running one, and the wait is for that turn alone (the ack's `enqueued_id` names its reply file and its `last_turn`), so the running turn's reply and end, and a question it asks meanwhile, are not the answer (it keeps waiting through them; its `--timeout` still applies); a question its own turn asks is (exit 3). Two queued waits each get their own turn's answer. A session waiting for an answer is still refused. `--format json` adds `delivery`: `queue`, or `next_step` when the running turn ended before the message went in (it then ran at once, and the answer is still its own).
|
|
211
211
|
- 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.
|
|
212
|
-
- `--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`, `removed` (a `--queue --wait` message taken back before it ran), `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.
|
|
212
|
+
- `--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`, `removed` (a `--queue --wait` message taken back before it ran), `no_answer`, `error`, `worker_gone` or `stopped` with a `detail` line (exit 1); `failed` adds `error_kind` and `retryable`, `canceled` adds `cancel_reason` (`worker_stopped`: the turn waited for died with its worker and the next worker settled it) 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.
|
|
213
213
|
- `--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).
|
|
214
214
|
- `--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`.
|
|
215
|
-
- `--
|
|
215
|
+
- `--parent-answers LEVEL` (with `--new`) gives the new session its own `guardrails.parent_answers` (`hard`, `standard`, `soft`), tighten-only: config.yml's applies when it is stricter, with `chi send: warning: --parent-answers soft is looser than config.yml's …`. See *Parent answers* below.
|
|
216
|
+
- `--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, bundle settings and parent answers, so `--dir`, `--llm-context*`, `--thinking`, `--toolkit`, `--tool`, `--no-tool`, `--set` and `--parent-answers` 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: …`.
|
|
216
217
|
- 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.
|
|
217
218
|
- 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.
|
|
218
219
|
|
|
@@ -246,6 +247,17 @@ declares every tool (full). The file stores only the name: a kit's tools and pro
|
|
|
246
247
|
`/toolkit` changes it, saved at once, from the next turn's start. See
|
|
247
248
|
[Toolkits](configuration.md#toolkits).
|
|
248
249
|
|
|
250
|
+
## Parent answers
|
|
251
|
+
|
|
252
|
+
A session file's `parent_answers` (`hard`, `standard`, `soft`, or `null` for none) is the session's own
|
|
253
|
+
`guardrails.parent_answers`: what a parent agent (`chi answer`, a relayed answer, a piped or marked attach) may
|
|
254
|
+
answer on its questions. It may only tighten config.yml's: the stricter of the two applies, in the worker's check and
|
|
255
|
+
in `chi answer`'s, and in what `chi send --wait`, `delegate_result` and the delegate reports tell a parent it may pick.
|
|
256
|
+
It is set at creation (`chi --parent-answers`, `chi send --new --parent-answers`); a respawned worker, a continue and a
|
|
257
|
+
fork keep it, and a `delegate` child starts with its parent's as it is then (a later change on the parent doesn't reach
|
|
258
|
+
it). A value that isn't a level reads as `hard`. `/parent-answers` changes it; loosening it is yours alone (see
|
|
259
|
+
[Guardrails](guardrails.md#answers-from-a-parent-agent)).
|
|
260
|
+
|
|
249
261
|
## Bundle settings
|
|
250
262
|
|
|
251
263
|
A session file's `bundle_settings` (`{"cost-cap": {"cap": 3}}`, `{}` for none and for a session from before them)
|
|
@@ -265,11 +277,11 @@ The `delegate` tool hands a task to a **child session**: an ordinary chi session
|
|
|
265
277
|
- `delegate_result(session:, timeout:)`: waits for a child's next reply: the one named, or the newest running child (`timeout` as for `delegate`: 0 checks once and returns at once). The reply comes back once; a later call waits for the next one. After `wait: false` it isn't needed: the report brings the reply.
|
|
266
278
|
- 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`.
|
|
267
279
|
- 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.
|
|
268
|
-
- **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 `
|
|
269
|
-
- **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
|
|
280
|
+
- **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 `guardrails.parent_answers`, or the child's own when stricter (`hard`: Stop only; see [chi as a sub-agent](sub-agent.md#the-step-limit)).
|
|
281
|
+
- **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` (under `guardrails.parent_answers: hard`, config.yml's or the child's own, a parent's Continue is refused: Stop only). 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`.
|
|
270
282
|
- **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.
|
|
271
283
|
- 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.
|
|
272
|
-
- 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.
|
|
284
|
+
- 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_answers` (each session's own, when stricter) by the parent and again by the child.
|
|
273
285
|
- A Stop of the parent's turn closes its card; the child's approval stays open there. A child's worker that goes closes the card (`… worker is gone`); a parent's worker that goes clears the child's "waiting in parent" line within 2 s. A later `delegate_result` relays it again.
|
|
274
286
|
- No relay where the parent can't host one (a `--no-shared` REPL, `--non-interactive`) or isn't waiting (`wait: false` until the next `delegate_result`): the approval waits on the child, with the web's bell, and the result's question block says to deny it or leave it for the user. The model's own questions and hooks' questions come back to the model as before. A grandchild's (a plugin's `ctx.sessions.fork` can make one) is relayed hop by hop: `delegate ab12 → cd34 asks:`.
|
|
275
287
|
- **Delegate reports: a child's reply comes back by itself.** When a child ends a turn that was its parent's (its task, a follow-up, a continue the parent answered, a reminder: not a turn its user typed into it directly), it rings the parent: a small file in the parent's `children/` dir, written after the child's turn is saved. So does a question the parent's model decides (the child's own `ask_user_question`, the step-limit continue; not an approval or a hook's question, which the user answers on the child's card), the turn after such a question whoever starts it, and the child's worker crashing. The parent reads the child as `delegate_result` would and hands its model a **report**: the same text (`session:`, `status:`, the reply), under its own header (`[Delegate report, delivered by chi …; not your user's message. Tell your user what it found …]`), as an input message with `source: "delegate_report"`.
|