samagotchi 0.153.0 → 0.154.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 +28 -1
- data/README.md +2 -3
- data/bin/chi +23 -24
- data/docs/architecture.md +30 -33
- data/docs/broadcast.md +5 -5
- data/docs/bundles.md +9 -12
- data/docs/cli.md +80 -96
- data/docs/configuration.md +27 -26
- data/docs/context.md +3 -3
- data/docs/desktop.md +1 -1
- data/docs/guardrails.md +8 -10
- data/docs/hooks.md +9 -9
- data/docs/internals/attached-context.md +2 -2
- data/docs/internals/background-tasks.md +3 -4
- data/docs/internals/block-renderers.md +4 -0
- data/docs/internals/bridge.md +3 -3
- data/docs/internals/context-telemetry.md +1 -1
- data/docs/internals/engine.md +33 -7
- data/docs/internals/objectives.md +6 -5
- data/docs/internals/participants.md +7 -7
- data/docs/internals/prompt-caching.md +1 -1
- data/docs/internals/remote-sessions.md +7 -7
- data/docs/internals/web-push.md +1 -1
- data/docs/memory.md +2 -2
- data/docs/plugins.md +23 -28
- data/docs/sessions.md +24 -24
- data/docs/sub-agent.md +2 -2
- data/docs/system-one.md +1 -1
- data/docs/testing.md +16 -6
- data/lib/samagotchi/answer_text.rb +34 -0
- data/lib/samagotchi/bridge.rb +43 -32
- data/lib/samagotchi/bridge_client.rb +10 -9
- data/lib/samagotchi/broadcast/recipients.rb +2 -2
- data/lib/samagotchi/broadcast/triage.rb +1 -1
- data/lib/samagotchi/broadcast_command.rb +5 -5
- data/lib/samagotchi/bundle_command/try/command.rb +1 -1
- data/lib/samagotchi/bundle_command/try/run.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +2 -2
- data/lib/samagotchi/bundles/decision-card/manifest.yml +4 -4
- data/lib/samagotchi/bundles/decision-card/plugin.rb +1 -9
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +3 -4
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/child_move.rb +3 -3
- data/lib/samagotchi/commands/registry.rb +6 -6
- data/lib/samagotchi/composer_route.rb +4 -5
- data/lib/samagotchi/config.rb +14 -12
- data/lib/samagotchi/context_candidate_choice.rb +1 -1
- data/lib/samagotchi/context_candidates.rb +4 -3
- data/lib/samagotchi/continue_offer.rb +1 -1
- data/lib/samagotchi/debug_log.rb +1 -1
- data/lib/samagotchi/degrade.rb +75 -0
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +2 -2
- data/lib/samagotchi/engine/cards.rb +87 -0
- data/lib/samagotchi/engine/context_judge.rb +138 -0
- data/lib/samagotchi/engine/events.rb +131 -0
- data/lib/samagotchi/engine/guardrails.rb +87 -0
- data/lib/samagotchi/engine/hooks.rb +119 -0
- data/lib/samagotchi/engine/messages.rb +124 -0
- data/lib/samagotchi/engine/model_settings.rb +127 -0
- data/lib/samagotchi/engine/models.rb +261 -0
- data/lib/samagotchi/engine/objectives.rb +116 -0
- data/lib/samagotchi/engine/participants.rb +86 -0
- data/lib/samagotchi/engine/plugins.rb +208 -0
- data/lib/samagotchi/engine/questions.rb +98 -0
- data/lib/samagotchi/engine/recap.rb +130 -0
- data/lib/samagotchi/engine/session.rb +131 -0
- data/lib/samagotchi/engine/side_requests.rb +135 -0
- data/lib/samagotchi/engine/steering.rb +134 -0
- data/lib/samagotchi/engine/toolkit.rb +269 -0
- data/lib/samagotchi/engine/turn_end.rb +180 -0
- data/lib/samagotchi/engine/turn_helpers.rb +160 -0
- data/lib/samagotchi/engine/verify.rb +153 -0
- data/lib/samagotchi/engine.rb +62 -2646
- data/lib/samagotchi/grants.rb +575 -0
- data/lib/samagotchi/guardrail_wiring.rb +2 -2
- data/lib/samagotchi/guardrails/context.rb +2 -3
- data/lib/samagotchi/guardrails/parent_approvals.rb +2 -2
- data/lib/samagotchi/guardrails/parent_policy.rb +2 -2
- data/lib/samagotchi/hook_routes.rb +3 -3
- data/lib/samagotchi/idle_recap.rb +4 -5
- data/lib/samagotchi/idle_reminders.rb +3 -4
- data/lib/samagotchi/iteration_limit.rb +2 -2
- data/lib/samagotchi/kernel_loop.rb +10 -11
- data/lib/samagotchi/launch_mode.rb +13 -32
- data/lib/samagotchi/level_setting_command.rb +3 -2
- data/lib/samagotchi/llm_context_command.rb +3 -2
- data/lib/samagotchi/log.rb +7 -7
- data/lib/samagotchi/log_line.rb +1 -1
- data/lib/samagotchi/log_path.rb +1 -1
- data/lib/samagotchi/note_command.rb +1 -1
- data/lib/samagotchi/note_delivery.rb +18 -8
- data/lib/samagotchi/objectives.rb +24 -3
- data/lib/samagotchi/objectives_check.rb +8 -27
- data/lib/samagotchi/objectives_command.rb +4 -69
- data/lib/samagotchi/objectives_edit.rb +87 -0
- data/lib/samagotchi/output_formatter.rb +7 -1
- data/lib/samagotchi/owner_lock.rb +7 -1
- data/lib/samagotchi/parent_answers_command.rb +5 -4
- data/lib/samagotchi/participant_exposure.rb +42 -0
- data/lib/samagotchi/participant_mode.rb +1 -1
- data/lib/samagotchi/participant_name.rb +1 -1
- data/lib/samagotchi/participants.rb +17 -17
- data/lib/samagotchi/participants_watch.rb +1 -1
- data/lib/samagotchi/pending_input_queue.rb +2 -2
- data/lib/samagotchi/plugin/api.rb +3 -2
- data/lib/samagotchi/plugin/context.rb +21 -7
- data/lib/samagotchi/plugin/peer.rb +2 -1
- data/lib/samagotchi/plugin/sessions.rb +6 -4
- data/lib/samagotchi/plugin_tasks.rb +1 -2
- data/lib/samagotchi/private_files.rb +29 -0
- data/lib/samagotchi/project_env.rb +2 -2
- data/lib/samagotchi/question_desk.rb +8 -7
- data/lib/samagotchi/recap_command.rb +2 -1
- data/lib/samagotchi/recap_setup.rb +1 -1
- data/lib/samagotchi/recap_store.rb +1 -1
- data/lib/samagotchi/redact.rb +86 -0
- data/lib/samagotchi/reminder_queue.rb +2 -2
- data/lib/samagotchi/remote_attach_command.rb +1 -1
- data/lib/samagotchi/reply_wait/poll.rb +2 -2
- data/lib/samagotchi/review_start.rb +1 -1
- data/lib/samagotchi/run_options.rb +6 -6
- data/lib/samagotchi/self_report.rb +42 -39
- data/lib/samagotchi/send_command/busy.rb +1 -1
- data/lib/samagotchi/send_command.rb +4 -1
- data/lib/samagotchi/session.rb +11 -2
- data/lib/samagotchi/session_access/contract.rb +3 -3
- data/lib/samagotchi/session_access/local.rb +9 -9
- data/lib/samagotchi/session_access/remote.rb +17 -7
- data/lib/samagotchi/session_access.rb +4 -4
- data/lib/samagotchi/session_archive_command.rb +2 -2
- data/lib/samagotchi/session_chain.rb +8 -6
- data/lib/samagotchi/session_commands.rb +45 -48
- data/lib/samagotchi/session_continue.rb +1 -1
- data/lib/samagotchi/session_delete_command.rb +4 -4
- data/lib/samagotchi/session_manager.rb +59 -36
- data/lib/samagotchi/session_metrics.rb +2 -2
- data/lib/samagotchi/session_model_settings.rb +3 -2
- data/lib/samagotchi/session_participants.rb +1 -1
- data/lib/samagotchi/session_retention.rb +3 -3
- data/lib/samagotchi/sessions_command.rb +4 -4
- data/lib/samagotchi/share_command.rb +10 -10
- data/lib/samagotchi/steer.rb +1 -1
- data/lib/samagotchi/system_prompt.rb +102 -109
- data/lib/samagotchi/task_watch.rb +1 -1
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +72 -10
- data/lib/samagotchi/terminal_ui/attached_loop/composer.rb +12 -9
- data/lib/samagotchi/terminal_ui/attached_loop/exit_commands.rb +16 -0
- data/lib/samagotchi/terminal_ui/attached_loop/questions.rb +2 -3
- data/lib/samagotchi/terminal_ui/attached_loop.rb +17 -8
- data/lib/samagotchi/terminal_ui/attached_view.rb +2 -3
- data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -5
- data/lib/samagotchi/terminal_ui/formatting.rb +7 -7
- data/lib/samagotchi/terminal_ui/input_support.rb +1 -7
- data/lib/samagotchi/terminal_ui/line_reader.rb +2 -2
- data/lib/samagotchi/terminal_ui/live_region.rb +2 -2
- data/lib/samagotchi/terminal_ui/log_tail.rb +135 -0
- data/lib/samagotchi/terminal_ui/one_shot_sink.rb +2 -2
- data/lib/samagotchi/terminal_ui/plain_surface.rb +1 -1
- data/lib/samagotchi/terminal_ui/question_prompt.rb +4 -5
- data/lib/samagotchi/terminal_ui/reline_seam.rb +3 -3
- data/lib/samagotchi/terminal_ui/status_row.rb +7 -8
- data/lib/samagotchi/terminal_ui.rb +142 -1387
- data/lib/samagotchi/title_command.rb +2 -2
- data/lib/samagotchi/tool_runner.rb +14 -16
- data/lib/samagotchi/toolkit_command.rb +2 -1
- data/lib/samagotchi/tools/delegate.rb +3 -1
- data/lib/samagotchi/tools/delegate_cursor.rb +3 -3
- data/lib/samagotchi/tools/delegate_wait.rb +1 -1
- data/lib/samagotchi/tools/objectives_tool.rb +2 -2
- data/lib/samagotchi/tools/send_note.rb +2 -1
- data/lib/samagotchi/turn_endings.rb +2 -2
- data/lib/samagotchi/turn_flow.rb +4 -5
- data/lib/samagotchi/update_hint.rb +2 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +39 -50
- data/lib/samagotchi/web/participant_access.rb +2 -2
- data/lib/samagotchi/web/participant_review.rb +5 -5
- data/lib/samagotchi/web/participant_stream_filter.rb +8 -8
- data/lib/samagotchi/web/participant_view.rb +2 -82
- data/lib/samagotchi/web/participants_admin.rb +1 -1
- data/lib/samagotchi/web/public/block_host.js +35 -8
- data/lib/samagotchi/web/public/block_renderers.css +9 -5
- data/lib/samagotchi/web/public/chains.js +2 -2
- data/lib/samagotchi/web/public/claim_link.js +4 -4
- data/lib/samagotchi/web/public/composer_mode.js +1 -1
- data/lib/samagotchi/web/public/format.js +3 -3
- data/lib/samagotchi/web/push/store.rb +1 -1
- data/lib/samagotchi/web/session_access_routes.rb +5 -4
- data/lib/samagotchi/web/session_control.rb +12 -8
- data/lib/samagotchi/web/session_create.rb +1 -1
- data/lib/samagotchi/web/session_hub.rb +2 -2
- data/lib/samagotchi/web/session_show.rb +2 -2
- data/lib/samagotchi/web/session_summary.rb +1 -1
- data/lib/samagotchi/worker.rb +100 -9
- data/lib/samagotchi/worker_link.rb +3 -3
- data/lib/samagotchi/working_set_command.rb +3 -2
- metadata +29 -6
- data/lib/samagotchi/terminal_ui/between_turns.rb +0 -138
- data/lib/samagotchi/terminal_ui/repl_input.rb +0 -152
- data/lib/samagotchi/web/grants.rb +0 -575
- data/lib/samagotchi/web/participant_exposure.rb +0 -44
- data/lib/samagotchi/web/private_files.rb +0 -31
data/docs/plugins.md
CHANGED
|
@@ -96,9 +96,8 @@ This adds a slash command. `name` is `/name`: a lowercase letter first, then a
|
|
|
96
96
|
block returns the text to show (a String), or nil to show nothing. If the
|
|
97
97
|
block raises, the user sees `/name: <error>`.
|
|
98
98
|
|
|
99
|
-
A plugin command works in
|
|
99
|
+
A plugin command works in both UIs, and in a `chi -p "/hello" --non-interactive` one-shot:
|
|
100
100
|
|
|
101
|
-
- **REPL** (`chi --no-shared`): typed at the prompt, and Tab completes it.
|
|
102
101
|
- **Attached TUI** (the default `chi`): the worker's snapshot names the
|
|
103
102
|
session's commands, so the TUI sends `/hello` to the worker and Tab
|
|
104
103
|
completes it. It needs a worker started after the bundle was installed.
|
|
@@ -107,7 +106,7 @@ A plugin command works in all three UIs:
|
|
|
107
106
|
runs it too.
|
|
108
107
|
|
|
109
108
|
A line that is not a known command keeps its old meaning: in the terminal
|
|
110
|
-
|
|
109
|
+
`/foo` goes to the model as a prompt; the web refuses it and names the
|
|
111
110
|
commands it knows.
|
|
112
111
|
|
|
113
112
|
Any whitespace may follow the name, a newline too: `/hello` on its own line
|
|
@@ -117,7 +116,7 @@ blank lines and indentation as typed; only the ends are stripped).
|
|
|
117
116
|
#### `anytime: true`
|
|
118
117
|
|
|
119
118
|
A plugin's normal command waits for its turn: typed while a turn runs, it is
|
|
120
|
-
refused as busy (the
|
|
119
|
+
refused as busy (the web puts it back into an empty composer; a plugin command can't
|
|
121
120
|
ask to run after the turn, as the built-in `/model X` does). An `anytime: true` command
|
|
122
121
|
runs **at once, on its own thread, beside the running turn**, and the turn
|
|
123
122
|
goes on:
|
|
@@ -128,9 +127,6 @@ goes on:
|
|
|
128
127
|
then its cards and notices **as it shows them**, and its output (the
|
|
129
128
|
`command_ran`) when it finishes. So a card can say "working…" first and
|
|
130
129
|
be replaced by the result.
|
|
131
|
-
- In the REPL, typed while a turn runs, it starts on a thread. Its cards
|
|
132
|
-
print as it shows them, above the live region; its output prints there
|
|
133
|
-
too while the turn runs, or at the prompt once the turn has ended.
|
|
134
130
|
|
|
135
131
|
Between turns an anytime command runs like any other, except that its
|
|
136
132
|
cards print as it shows them rather than after its output.
|
|
@@ -142,8 +138,8 @@ running turn: they are not rows of the turn's step, and their events carry
|
|
|
142
138
|
Its block runs on another thread than the turn, so it must be thread-safe:
|
|
143
139
|
|
|
144
140
|
- Read the conversation through `ctx.messages`, a frozen copy. While a turn
|
|
145
|
-
runs, a worker's holds that turn so far;
|
|
146
|
-
**before** that turn (`ctx.messages_partial?` says so).
|
|
141
|
+
runs, a worker's holds that turn so far; a `--non-interactive` one-shot's
|
|
142
|
+
is the conversation **before** that turn (`ctx.messages_partial?` says so).
|
|
147
143
|
- Show things only through `ctx` (`ctx.card`, `ctx.notify`), and return
|
|
148
144
|
the text to show.
|
|
149
145
|
- Keep your own state (instance variables) behind a `Mutex` if two
|
|
@@ -171,8 +167,7 @@ thinking is dropped). The TUI prefills `line --yes`, the web shows an Apply butt
|
|
|
171
167
|
then runs again with `--yes` in `args` (drop it from your words). Outside a command (a hook, an init task, your
|
|
172
168
|
own thread) it is always nil. A plain String or nil return is unchanged.
|
|
173
169
|
|
|
174
|
-
A command that asks must not be `anytime: true`:
|
|
175
|
-
be lost. Needs chi 0.132.0.
|
|
170
|
+
A command that asks must not be `anytime: true`: its Apply would be lost. Needs chi 0.132.0.
|
|
176
171
|
|
|
177
172
|
```ruby
|
|
178
173
|
chi.command "/tone", "show or set this session's tone" do |args, ctx|
|
|
@@ -191,8 +186,8 @@ end
|
|
|
191
186
|
`/help` (itself an anytime command) lists every command the session knows:
|
|
192
187
|
chi's own, each bundle's with the bundle's name, and the terminal UIs' own
|
|
193
188
|
(`/stats`, `/exit`, `/detach` …), marked where they run: `terminal only`,
|
|
194
|
-
`
|
|
195
|
-
It works in
|
|
189
|
+
`web only`, or `terminal and web only` (`/cut`).
|
|
190
|
+
It works in both UIs.
|
|
196
191
|
|
|
197
192
|
### `chi.annotation(label, command:, template: "{quote}", send: false)`
|
|
198
193
|
|
|
@@ -559,8 +554,8 @@ end
|
|
|
559
554
|
so far run, and the next `value` tries again.
|
|
560
555
|
- `service.running?`, `service.state` (`:idle`, `:running`, `:stopped`) and
|
|
561
556
|
`service.stop`.
|
|
562
|
-
- The services stop when chi leaves: the
|
|
563
|
-
|
|
557
|
+
- The services stop when chi leaves: the session's worker exits (an idle
|
|
558
|
+
exit, `/exit`, a crash, TERM), or a `--non-interactive` one-shot ends. See
|
|
564
559
|
[Shutdown](#shutdown). A stopped service never starts again; `value`
|
|
565
560
|
raises `Samagotchi::Plugin::Service::Stopped`.
|
|
566
561
|
- A plugin whose load fails after it started services has them stopped.
|
|
@@ -576,7 +571,7 @@ repo, logging in, starting a server for the first time. `register` itself
|
|
|
576
571
|
should return at once (everything in it runs before the session's UI is
|
|
577
572
|
up), so it hands the slow part to `chi.init`. The block runs **on its own
|
|
578
573
|
thread** once the session can show it: in a worker right after its Bridge
|
|
579
|
-
is up (so a new web chat opens at once), in
|
|
574
|
+
is up (so a new web chat opens at once), in a `--non-interactive` one-shot as its turn starts.
|
|
580
575
|
|
|
581
576
|
```ruby
|
|
582
577
|
class Plugin
|
|
@@ -600,7 +595,7 @@ end
|
|
|
600
595
|
|
|
601
596
|
- **What the UIs show.** Every UI shows a running task (web: a spinner line
|
|
602
597
|
over the composer, `mcp · Starting MCP server chrome (first run, saving
|
|
603
|
-
its tools)…`; the attached TUI: its activity row
|
|
598
|
+
its tools)…`; the attached TUI: its activity row) and a
|
|
604
599
|
line when it is done: `✓` and what the block returned, a short summary
|
|
605
600
|
(`chrome ready, 3 tools`), or `<label>: done` for anything else. A UI that
|
|
606
601
|
joins while it runs sees it too.
|
|
@@ -819,7 +814,7 @@ A cheap System 1 question this bundle asks with `ctx.judge` or `ctx.judge_later`
|
|
|
819
814
|
The plugin's context (the `ctx` its handlers get), for `register` itself:
|
|
820
815
|
its settings, log and data_dir, for example. A `ctx.notify` or `ctx.card`
|
|
821
816
|
while chi starts (inside `register`) is shown once the session's UI can show
|
|
822
|
-
it (a worker's Bridge is up
|
|
817
|
+
it (a worker's Bridge is up), after the plugins'
|
|
823
818
|
load warnings; a UI that joins later still gets it.
|
|
824
819
|
|
|
825
820
|
### Names
|
|
@@ -848,7 +843,7 @@ full label, `plugin.rb (bundle my-bundle)`.
|
|
|
848
843
|
| `ctx.scratch?` | whether the session is a `chi scratch` one (deleted when it ends) |
|
|
849
844
|
| `ctx.delegate?` | whether the session is a delegate child: a task another session handed over |
|
|
850
845
|
| `ctx.review?` | whether the session is a review session (`chi review`): one its participants read, not the owner's working session |
|
|
851
|
-
| `ctx.frontend` | what runs the session, read now: `:
|
|
846
|
+
| `ctx.frontend` | what runs the session, read now: `:one_shot` (`-p --non-interactive`: one turn, then exit) or `:worker` (a session worker: the web, an attached TUI). There is no `:repl` any more: a bundle that checks for it never matches. Only a worker's answers reach the web while it runs; a one-shot's show there once the session is opened. Until the host sets it (while the plugins load, inside `Engine.new`) it reads `:one_shot`, so read it in hooks and init tasks, not in `register` |
|
|
852
847
|
| `ctx.continues` | the id of the session this one continues (a [continue](sessions.md#session-chains)'s new link), else nil |
|
|
853
848
|
| `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 |
|
|
854
849
|
| `ctx.model` | the model the session runs on now: its resolved ref (`host:id`), right after `/model` too |
|
|
@@ -861,8 +856,9 @@ full label, `plugin.rb (bundle my-bundle)`.
|
|
|
861
856
|
| `ctx.data_dir` | `$XDG_STATE_HOME/samagotchi/plugins/<bundle>/`, created on first use: what the bundle keeps across sessions |
|
|
862
857
|
| `ctx.session_dir` | this session's own folder there, created on use, read now; nil with no session yet. `ctx.session_dir(create: false)` is the same path with no folder made, for a read, so a session that saved nothing leaves none (a `File.read` of what isn't there raises `Errno::ENOENT`); needs chi newer than 0.143.0. chi removes it when the session is deleted, discarded or pruned, so don't build the path yourself. A `sessions/<id>.json` an older version of your bundle kept moves in as `state.json` (chi still removes one left there). Needs chi newer than 0.142.0 |
|
|
863
858
|
| `ctx.log` | `ctx.log.info(:event, key: value)`: debug-log records tagged `plugins`, with `bundle=<bundle>` |
|
|
864
|
-
| `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);
|
|
865
|
-
| `ctx.
|
|
859
|
+
| `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); a `--non-interactive` one-shot's is the conversation before that turn. An entry may carry `at`, when it happened (docs/sessions.md, Message times) |
|
|
860
|
+
| `ctx.last_answer` | the newest answer's text as the user saw it: the newest answer bubble of `ctx.messages`, its thoughts (one cut off before it closed too) and tool-call markup taken out, the rule chi's continue note uses. nil when there is none or it was a thought alone; mid-turn, a worker's can be the running turn's interim text. Needs chi newer than 0.153.0 |
|
|
861
|
+
| `ctx.messages_partial?` | whether `ctx.messages` leaves out a running turn (a `--non-interactive` one-shot mid-turn), so a plugin can say what its answer is about |
|
|
866
862
|
| `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 |
|
|
867
863
|
| `ctx.objectives` | the session's [objectives](internals/objectives.md), read now, as a frozen value: `empty?`, `items` (each `id`, `text`, `status` `"active"`, `"done"` or `"dropped"`, `done_when`), `find(id)`, `text` (the lines the model is shown) and `moves` (the latest move per id; mid-turn, a worker's leaves the running turn's out). An edit not yet absorbed counts. Empty when the session has none. Use its methods, not its class. Needs chi newer than 0.148.0 |
|
|
868
864
|
| `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) |
|
|
@@ -908,7 +904,7 @@ keeps JSON numbers and booleans. A name that is a core session setting is always
|
|
|
908
904
|
## Cards
|
|
909
905
|
|
|
910
906
|
A card is a small framed message with buttons: a title, a body and
|
|
911
|
-
actions. Core draws it in
|
|
907
|
+
actions. Core draws it in both UIs (the attached TUI and the web); a plugin has no JS or CSS of its
|
|
912
908
|
own.
|
|
913
909
|
|
|
914
910
|
```ruby
|
|
@@ -946,7 +942,6 @@ Where it shows:
|
|
|
946
942
|
|
|
947
943
|
| | during a turn (a tool, a hook) | between turns (a command) |
|
|
948
944
|
|---|---|---|
|
|
949
|
-
| REPL | where it happens, above the live region | at the prompt, after the command's output |
|
|
950
945
|
| attached TUI | where it happens | as it arrives |
|
|
951
946
|
| web | a row of the running step | between the turns |
|
|
952
947
|
|
|
@@ -1231,7 +1226,7 @@ ctx.sessions.review(subjects: ["docs/plan.md"], seed: handoff) # => a review se
|
|
|
1231
1226
|
prefix); it runs as a turn, and a stopped session is woken. It waits up to
|
|
1232
1227
|
5 s for the session's worker, so call it from an anytime command or a
|
|
1233
1228
|
thread of your own, never from a tool or hook of a running turn. A
|
|
1234
|
-
session
|
|
1229
|
+
session a `--non-interactive` one-shot has can't take it.
|
|
1235
1230
|
- A running turn takes it at its next step. `delivery: :queue` runs it as
|
|
1236
1231
|
a turn of its own after that turn instead (as `/queue TEXT` does; to an
|
|
1237
1232
|
idle session it simply runs). A plugin never cuts the running turn's
|
|
@@ -1595,8 +1590,8 @@ docs/testing.md). **Minitest:** `require "samagotchi/testing/minitest"` and subc
|
|
|
1595
1590
|
|
|
1596
1591
|
## Shutdown
|
|
1597
1592
|
|
|
1598
|
-
When
|
|
1599
|
-
|
|
1593
|
+
When a session's worker exits (an idle exit, `/exit`, a crash, TERM), or a
|
|
1594
|
+
`--non-interactive` one-shot ends, chi shuts the session's Engine down:
|
|
1600
1595
|
|
|
1601
1596
|
1. The idle jobs (reminders, the recap) stop.
|
|
1602
1597
|
2. The plugins' init tasks are cancelled (`ctx.cancelled?` turns true).
|
|
@@ -1607,8 +1602,8 @@ crash, TERM), chi shuts the session's Engine down:
|
|
|
1607
1602
|
|
|
1608
1603
|
## Loading, and when it fails
|
|
1609
1604
|
|
|
1610
|
-
Plugins load when a session starts (`Engine.new`, in
|
|
1611
|
-
|
|
1605
|
+
Plugins load when a session starts (`Engine.new`, in a worker or a
|
|
1606
|
+
`--non-interactive` one-shot), after the bundle hooks. The steps are:
|
|
1612
1607
|
|
|
1613
1608
|
1. The file must match the sha256 recorded at install (not checked for a bundle installed with `--dev`).
|
|
1614
1609
|
2. chi must meet `requires_chi`. A bundle whose `requires_chi` this chi doesn't
|
data/docs/sessions.md
CHANGED
|
@@ -10,7 +10,7 @@ Sessions are plain files — no DB. Each session is `~/.local/state/samagotchi/s
|
|
|
10
10
|
|
|
11
11
|
**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`). Such a file, with a prompt, from the user, `chi send` or a delegating parent, also ends a running `task_wait` or `delegate_result` and hands a running `execute` over as a task after 3 s (`SessionInbox.steer_waiting?`, a peek the worker's turn polls through `Engine#input_waiting?`; never in a participant's turn): the file stays for the 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 outcome is kept at `input/settled/<enqueued_id>` (`completed`, `failed` for a command error, `canceled` with `cancel_reason` `busy` or `dropped` for one refused mid-turn or dropped as the worker left; `origin: command`) so `chi send --wait` ends instead of waiting for a turn that will not run, its detail saying `the command failed` or `the command was refused (busy)` rather than a turn's words. **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`.
|
|
12
12
|
|
|
13
|
-
**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)
|
|
13
|
+
**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/`), and a message in a stopped worker's inbox (the next worker runs it).
|
|
14
14
|
|
|
15
15
|
**Retention (file-based, opt-out via env):**
|
|
16
16
|
|
|
@@ -25,11 +25,11 @@ A session is deleted if **expired by age OR overflow by count** (unless `keep_st
|
|
|
25
25
|
|
|
26
26
|
**Worker idle exit:** a background worker (plain `chi`, Web UI sessions) exits after `SAMAGOTCHI_SESSION_IDLE_EXIT_MINUTES` (`session.idle_exit_minutes`, default `30`, `0`=never) with no turn running or queued, no client on its stream (an open web tab or attached terminal keeps it), no reminder registered, no background task it watches for a [finish wake](internals/background-tasks.md#finish-wake) still running or waiting for its wake turn (with `session.task_wakes: wake`; not one whose turn you stopped) and no continue offer (the step-limit question) waiting for an answer. It removes `bridge.json` and frees `owner.lock`; the next prompt or `--attach` wakes a new worker (`WorkerIdleExit`, `SessionManager.run_session_loop`). As its Bridge stops, the worker marks the lock leaving (`OwnerLock#mark_leaving`: `leaving_at`, and `leave_wait`, its `recap.timeout`), since it keeps the lock while it writes the recap on leave: an `--attach` or `--resume` meanwhile (here, or a remote one through chi web's wake) prints `(waiting for the session to finish its recap…)` (`LaunchMode::RECAP_WAIT`) and waits for the release, up to that budget plus `OwnerLock::LEAVE_MARGIN` (5 s), before starting the new worker (`OwnerLock.await_release`). A lock held by a worker that isn't leaving (a live one, a stuck one) is left as before. A client can also ask the worker to exit right away (Bridge `POST /session/:id/exit`; `/exit` in an attached terminal does, `/detach` doesn't): the same rules apply without the timeout (even with `0`), and the asker's own stream doesn't count. The reply names what keeps it up (for background tasks, which ones and how to stop them). When every hold is one the user can stop — reminders, a watched task still running, a delegate with a live worker — the 409 also carries `holds: [{kind:, items: [{id:, label:, detail:}]}]` (a reminder's detail is its schedule, `every 20m`/`every 1h`, `, due now` when it is already due), and an attached terminal asks what to do with them; its "stop them and exit" posts `{"stop": "all"}`, which clears the reminders, stops those tasks (`TaskRuntime.stop_task`, as `task_stop`) and stops those children (`SessionManager.stop_session`) before leaving, re-checking first that nothing the user can't stop appeared meanwhile (`HoldStop`). A delegate counts only for that ask: it never holds the idle exit, and an exit that deletes or archives the session does not ask about it. The session's status stays as it was, not `stopped`. With `"restart": true` (a worker whose sidecar lists the `restart` feature) the worker hands the session to a new worker on the newest chi installed: it leaves, then starts its successor once its lock is free, keeping even an empty session and writing no recap. Clients' streams don't hold a restart (a web tab and an attached terminal reconnect to the new worker); what would be lost does: a turn running or queued, a continue offer, a question or approval waiting, a `/btw` (or another anytime command) still running, a delegate's approval relay open or settled in the last 10 minutes, and background tasks this conversation started that still run (`WorkerIdleExit#hold_for_restart`; 409 `held` with `reason`). Reminders go along: as it leaves, the worker writes them to `<id>/reminders.json` (0600: `session_id`, `written_at`, and each reminder's id, name, description, interval and next fire as wall-clock time; `ReminderCarry`), and its successor reads and deletes the file as it starts, before its idle scheduler runs. A restored reminder keeps its id and schedule; one that came due meanwhile fires once at once, and one further ahead than its interval (a clock that jumped) is due one interval from now. The file counts only for the same session within 2 minutes of `written_at`, and not once the session is stopped: otherwise it is deleted unread (log `reminders_carry_dropped`, at warn unless the session was stopped). A stop that lands while the worker leaves for a restart wins: the worker writes no file, and no successor starts (`SessionManager.run_session_loop`, log `restart_dropped`); an archive deletes the file of each session it archives. Once an exit or restart is taken (claimed before a stop-all stops anything), the first one decides how the worker leaves: a later request of the same kind is answered as that exit, one of another kind (a stop-all from another terminal after a restart, chi web's "Restart workers" after a `/exit`) gets 409 `held` with `reason` `leaving_exit`, `leaving_delete` or `leaving_restart`, and a restart says it was refused rather than "restarted". A write that fails keeps the worker up with its reminders (log `reminders_carry_failed`), and the caller reports no new worker. A reminder already due when the restart is asked runs its turn first, so the restart's wait (10 s) may report no new worker yet though the hand-off follows. A worker from before this still refuses a restart for its reminders (409 `reminders`). Only an asked restart carries them: the fallback's TERM (a worker that can't be asked, below), a stop, an idle exit (which reminders hold anyway) and a crash drop them as before. What lives only in the worker's memory goes: the event history a late client replays, the kept `/btw` answers, a plugin's in-memory state.
|
|
27
27
|
|
|
28
|
-
**Empty sessions:** a session nothing happened in is deleted when it is left, so `chi` → `/exit` leaves no trace. Empty means: no messages, no turn tried (a failed turn leaves a `last_prompt` and keeps it), no memory used, no pending question, nothing in `input/`, `notes/` or `images/`, nothing in its dir beyond the worker's skeleton, mode `assist`, and the default model. A session on another model (`/model`, `--model`) counts as prepared and stays (`SessionManager.discardable?`). The worker checks as it idle-exits or leaves on a client's request and deletes it once its lock is free, checking again first. The attached `/exit` then says `Detached; the session is empty: it will be discarded if nothing arrives before its worker stops.` (a note or prompt landing first keeps it). The
|
|
28
|
+
**Empty sessions:** a session nothing happened in is deleted when it is left, so `chi` → `/exit` leaves no trace. Empty means: no messages, no turn tried (a failed turn leaves a `last_prompt` and keeps it), no memory used, no pending question, nothing in `input/`, `notes/` or `images/`, nothing in its dir beyond the worker's skeleton, mode `assist`, and the default model. A session on another model (`/model`, `--model`) counts as prepared and stays (`SessionManager.discardable?`). The worker checks as it idle-exits or leaves on a client's request and deletes it once its lock is free, checking again first. The attached `/exit` then says `Detached; the session is empty: it will be discarded if nothing arrives before its worker stops.` (a note or prompt landing first keeps it). The retention sweep catches those killed first: empty sessions over an hour old with no live owner, whatever `DAYS`/`MAX`, and skeleton-only dirs with no `*.json`. `session.keep_empty: true` (env `SAMAGOTCHI_SESSION_KEEP_EMPTY`) turns all of it off.
|
|
29
29
|
|
|
30
30
|
**Participants:** a session its owner invited participants to review (`chi share`, see [CLI: Participants](cli.md#participants)) has two more files in its dir, neither in the session JSON: `participants.json` (0600), who is or was invited (`{participants: [{id, kind, name, role, added_at, expires_at, removed_at}]}`, never a token), which chi web and `chi share` write and the worker reads (`Participants.read`); and `participants.jsonl` (0600, append-only), the audit trail (grants issued, revoked and expired, each new peer, refused routes). A session nobody was invited to has neither. A continue, a fork or a delegate starts with no participants. Archiving or deleting the session revokes its links. See [internals/participants.md](internals/participants.md).
|
|
31
31
|
|
|
32
|
-
**
|
|
32
|
+
**Always attached:** every interactive `chi` (plain, `-p`, `--resume ID`, `--attach ID`, `chi scratch`) runs its session in a background worker and attaches to it; there is no setting for it. The worker runs in the session's `working_directory`, so `!commands` and tools see the directory the session was started in. Only `chi -p TEXT --non-interactive` runs its one turn in this process; while it does, the session can't be attached ("in use by a one-turn chi --non-interactive run or an older chi's terminal"). See [CLI: Sharing a session](cli.md#sharing-a-session).
|
|
33
33
|
|
|
34
34
|
**Which chi a worker runs:** a new or woken worker runs the newest chi installed for this Ruby (`gem 'samagotchi', '>= 0.a'`, prereleases count), whoever starts it: a `chi web` or terminal still on an older chi starts it on the newest one, so a stale process doesn't spread old code. A worker already running keeps its version until it is restarted. Should the newest install fail to load, the worker runs the starter's own version and logs `boot_fallback` (its stdio is `/dev/null`). Under `bundle exec` the worker drops Bundler's env first, so it activates the newest gem too. Two consequences: a wrapper pin (`chi _0.17.0_ web`) pins only that process, not the workers it starts; and a source checkout (`bin/chi` in a worktree, `bundle exec`) starts its workers on its own `lib/`, but a session it made that is later woken by an installed chi (`chi web`, `chi send`) runs the installed code. The names an older starter hands a newer worker (the boot line, its env keys, `input/` files, the session JSON fields and the Bridge routes sessions use on each other) are pinned in `spec/worker_contract_spec.rb`. A chi newer than 0.130.0 keeps the top-level keys it doesn't know (a newer chi's fields) when it saves a session, after its own, and doesn't act on them; `written_by` names the chi that saved the file last, and a worker woken on a file a newer chi saved logs `newer_writer`. chi 0.130.0 and older drop those keys: a session a newer chi made loses its newer fields when an older chi wakes or saves it (for example `chi --attach` from a project whose Ruby has an older chi). Keep the chi in every Ruby you run it from newer than 0.130.0.
|
|
35
35
|
|
|
@@ -37,13 +37,13 @@ A new session field (the rule in `Session::FIELDS`' comment): only top-level key
|
|
|
37
37
|
|
|
38
38
|
**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.
|
|
39
39
|
|
|
40
|
-
**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
|
|
40
|
+
**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 `chi --resume ID -p TEXT --non-interactive` run, 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`, a `chi send --wait -m` to an idle session or `--new`, `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 wait with no turn id that saw the turn running ends the same way when the settle comes (`chi send --wait ID` with no message on a turn with no id, such as a session's first prompt or a reminder turn; a message merged into the turn as a steer), unless an input still waits or was claimed since: an unmerged message runs as its own turn next, and that turn's end is the answer. 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.
|
|
41
41
|
|
|
42
42
|
**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, then CONT (a stopped worker, SIGSTOP or Ctrl-Z, runs its TERM handler only once continued, so it leaves at once instead of outliving the wait), 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 ⚙ panel, 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 (the page's Stop shows the stop at once), or for the body's `wait` up to the same 10 s (`SessionManager::STOP_WAIT`): `chi --remote NAME sessions stop` sends it, so it waits as long as here. Before that stop, the page's `stop session` button asks what the worker still has that the user can stop: `GET /api/sessions/:id/holds` (owner-only) answers the worker's `holds` — the same list the exit ask's 409 carries — always an array, empty when there is no live worker, one from before the route, or one that did not answer. With something, a sheet (a bottom sheet on a phone, a popover at the button otherwise — `stop_sheet.js`) lists them, at most three of a kind named and `+N more`, and offers **Stop all** (`POST /api/sessions/:id/holds/stop` → `HoldStop#stop_all`, then the stop), **Stop session only** (the stop exactly as it was) or **Cancel**. Nothing to ask about is the plain confirm, one tap as before. Both buttons stay because a stop only TERMs the worker: its reminders die with it (a stop also deletes a restart's `reminders.json` no successor took yet, so a later resume doesn't bring them back), while a background task and a delegate are their own processes and keep running. 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.
|
|
43
43
|
|
|
44
|
-
**Deleting a session:** `chi sessions delete ID...`, `/exit --delete` in a terminal and the Web UI's delete all go through `SessionManager.delete_session`: it resolves a unique prefix, removes `<id>.json` and the whole `<id>/` dir, and returns what it removed. A session a
|
|
44
|
+
**Deleting a session:** `chi sessions delete ID...`, `/exit --delete` in a terminal and the Web UI's delete all go through `SessionManager.delete_session`: it resolves a unique prefix, removes `<id>.json` and the whole `<id>/` dir, and returns what it removed. A session a one-turn `chi --non-interactive` run has (`owner.lock` kind `tui`) is always refused. One a worker runs is refused unless the caller asks to stop it: then it stops the worker as `chi sessions stop` does and deletes once `owner.lock` is free (`--force` on the CLI, 10 s; the web page always, 2 s; `/exit --delete` after the worker agreed to exit, 10 s). A worker that outlives the wait leaves the session in place. The web's route is `DELETE /api/sessions/:id?force=true` (the page sends `force`; without it a live worker's session is 409 `worker_running`, as the CLI without `--force`; 200 `{status: "deleted", session_id, stopped}`; 409 `owned_by_tui` or `still_stopping`; 404).
|
|
45
45
|
|
|
46
|
-
<a id="archiving-a-session"></a>**Archiving a session:** `chi sessions archive ID...`, `/archive` in a terminal and the Web UI's `archive` go through `SessionManager.archive_session`: it writes `<id>/archived` (`{"archived_at": …}`; not a session.json field, which every save rewrites) for the session and its delegated children. `Session.list` leaves archived sessions out unless `include_archived:`, and everything built on it follows: `chi sessions list` (`--archived` shows them, `[archived]`), the summaries (`--live`, json/tsv, the desktop panel, the agent's `list_sessions`) and the retention prune, which neither deletes nor counts them against `max_count`. `children_of` (max_children, `delegate_result`) still sees them, and `--resume`/`--attach ID`, `chi send`/`chi note ID` reach them as before. Refused: a turn running in the session or in one of its children (naming the child; 409 `busy` on the web), a prompt queued in `input/` (it would run on the next resume) or a live worker's step-limit question still open, in it or a child (409 `busy` too), a
|
|
46
|
+
<a id="archiving-a-session"></a>**Archiving a session:** `chi sessions archive ID...`, `/archive` in a terminal and the Web UI's `archive` go through `SessionManager.archive_session`: it writes `<id>/archived` (`{"archived_at": …}`; not a session.json field, which every save rewrites) for the session and its delegated children. `Session.list` leaves archived sessions out unless `include_archived:`, and everything built on it follows: `chi sessions list` (`--archived` shows them, `[archived]`), the summaries (`--live`, json/tsv, the desktop panel, the agent's `list_sessions`) and the retention prune, which neither deletes nor counts them against `max_count`. `children_of` (max_children, `delegate_result`) still sees them, and `--resume`/`--attach ID`, `chi send`/`chi note ID` reach them as before. Refused: a turn running in the session or in one of its children (naming the child; 409 `busy` on the web), a prompt queued in `input/` (it would run on the next resume) or a live worker's step-limit question still open, in it or a child (409 `busy` too), a one-turn `chi --non-interactive` run owning it (`owned_by_tui`), a `chi scratch` session (`scratch`). A live idle worker is stopped first, and the marker is written even if it is still shutting down. Input a human typed brings a session back (`{"unarchived_at": …}`): a prompt from the web, an attached terminal or `chi send` (origin `web:*`, `tui:*`, `cli:send`, none), steering merged into a running turn, a `/continue` answer or a question answered (the step-limit question too); a delegate's follow-up, a plugin's turn, a reminder, the idle recap, a note and just opening it don't. A child's input brings only the child back. The retention sweep ages an unarchived session from when it was unarchived, so unarchiving an old session doesn't let the next sweep take it. `/archive` leaves the session first, as `/exit` does, then archives it: in an attached terminal the worker is asked to exit (one that stays up for another UI is stopped by the archive; a running turn, a queued prompt or an open step-limit question refuses it), and an empty session is discarded instead; `chi scratch` refuses it. Typed while a turn runs, `/archive` waits for the turn's end (an attached terminal for the commands queued behind it too) and then archives. `chi sessions unarchive ID...` and the web's `unarchive` bring back the whole family. Web routes: `POST /api/sessions/:id/archive` (200 `{status, session_id, archived, stopped, discarded}`, ids) and `POST /api/sessions/:id/unarchive` (200 `{status, session_id, unarchived}`). The session hub and `GET /api/sessions` keep archived sessions, `archived: true` in their summaries: the page leaves them out of the strip, its count and the list at render, and the `archived` toggle beside the all-sessions search shows them, dimmed with an `archived` badge. The info bar marks a session whose live worker runs an older chi than the newest installed (`worker_version` in its summary): `chi 0.18.1 · Restart`, which asks to confirm and calls `POST /api/sessions/:id/restart` (a refusal's reason shows in a toast); a worker from before restarts shows `chi sessions stop ID` instead. The info bar reads `continue → · archive · stop · delete` (`unarchive` on an archived one; no `continue →` on a link continued already, see *Session chains*; `continue →` is a plain button like `archive`, in the accent on an archived session, where it is the thing to do); archiving the open session stays on it, with an Undo toast.
|
|
47
47
|
|
|
48
48
|
**System 1's saved runs** last as long as sessions: the sweep ages them by day file, and deleting a session purges its runs ([system-one.md](system-one.md)).
|
|
49
49
|
|
|
@@ -93,7 +93,7 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
|
|
|
93
93
|
|
|
94
94
|
**Projects:** a session belongs to the git project it was started in: the repository, whichever worktree or subfolder of it (the same project root memories use). `chi sessions list` (with `--live` and `--format` too) and `chi web` show the current folder's project; `--scope=all` shows every session, and so does a folder in no repo (`~`). `--cwd PATH` is a folder filter instead of the project. The project is stored with the session (`project_root` in its JSON, `project` in `--format json`), so a session keeps it after its worktree is deleted; sessions older than that are placed by their folder, and one whose folder is gone shows in `--scope=all` only. Retention, `--resume`/`--attach ID`, `chi send`/`chi note ID` and `chi note --all` (every live session) are not scoped. The agent's `list_sessions` lists its own project's sessions; `cwd: "/"` lists every one.
|
|
95
95
|
|
|
96
|
-
`--live`, `--cwd` and `--format` make `list` a picker for scripts (`SessionManager.session_summaries`): `--live` keeps the sessions a worker runs now (the owner lock, not the saved status; a session
|
|
96
|
+
`--live`, `--cwd` and `--format` make `list` a picker for scripts (`SessionManager.session_summaries`): `--live` keeps the sessions a worker runs now (the owner lock, not the saved status; a session a one-turn `chi --non-interactive` run has is left out), `--cwd PATH` those in PATH or below, and test runs are left out unless the list is itself run as one (`SAMAGOTCHI_ENV=test`, `RACK_ENV=test` or `CI` set: then they show, marked `[test]`; `chi note --all` likewise). `--live` shows 10 unless `--limit` says otherwise; filters apply before the limit. `--format json` prints `[{id, short_id, desc, cwd, project, updated_at, live, busy, owner, recap, parent_id, continues, delegate, archived, scratch, ctx_pct}]` (`owner`: `"worker"`, `"tui"` for a one-turn `chi --non-interactive` run, which takes no notes or messages, or null; `recap`: the first sentence of the session's recap, or null; `delegate`: true for a child the `delegate` tool started, false for a fork, which has a `parent_id` too), `--format tsv` one `id<TAB>desc` line per session, where `desc` is `<folder> · <last prompt>` cut to 60 characters (the text form of `--live`/`--cwd` shows `<folder> · <recap>` when there is one; tsv and json keep `desc`).
|
|
97
97
|
|
|
98
98
|
**Ordering:**
|
|
99
99
|
|
|
@@ -110,12 +110,12 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
|
|
|
110
110
|
|
|
111
111
|
- New sessions set `test_run:true` when `SAMAGOTCHI_ENV=test` or `RACK_ENV=test` or `CI` is set (explicit flag, `metadata_version` 2). Old sessions without the flag load as `test_run:false`.
|
|
112
112
|
- Test runs are tagged and obey the same retention. `chi sessions clean` deletes every test session whatever its age (`--days N`: only those older than N days); a live worker or a `keep_status` status still keeps one. `chi sessions prune --test-only` applies the usual age and count rules to test sessions only.
|
|
113
|
-
- A `chi scratch` session (`"scratch": true`) is deleted when its
|
|
113
|
+
- A `chi scratch` session (`"scratch": true`) is deleted when its terminal leaves (its worker deletes it too once its last client is gone); one left behind when both died is deleted by the next sweep, `prune` or `clean`, whatever its age or status, once nobody owns it. `list` shows it as `[scratch]` (every text form; `scratch: true` in `--format json`); `chi web` never shows one. See [CLI: Scratch sessions](cli.md#scratch-sessions).
|
|
114
114
|
- For ad-hoc manual QA use `SAMAGOTCHI_ENV=test XDG_STATE_HOME=/tmp/chi-test-$USER chi ...` to isolate from real state; the flag also marks sessions that `chi web` or an attached `chi` spawn (their workers inherit the environment), so `clean` finds them if they land in the real state.
|
|
115
115
|
|
|
116
116
|
## Session file
|
|
117
117
|
|
|
118
|
-
`Session::FIELDS` is the table of `<uuid>.json`, in the order `Session#save` writes it (`Session.from_h` and `#to_h` both go by it; 41 keys). A key a file lacks reads as the default below; `review`, `first_turn_id`, `verdicts` and `verify_fresh` are written only when set (`OPTIONAL_FIELDS`). A key this chi doesn't know is kept and written back as it was (see the rule above). "Who writes it": the Engine runs in the session's worker (or
|
|
118
|
+
`Session::FIELDS` is the table of `<uuid>.json`, in the order `Session#save` writes it (`Session.from_h` and `#to_h` both go by it; 41 keys). A key a file lacks reads as the default below; `review`, `first_turn_id`, `verdicts` and `verify_fresh` are written only when set (`OPTIONAL_FIELDS`). A key this chi doesn't know is kept and written back as it was (see the rule above). "Who writes it": the Engine runs in the session's worker (or a `--non-interactive` run); the rest are named.
|
|
119
119
|
|
|
120
120
|
| Key | Meaning | Written by |
|
|
121
121
|
|-----|---------|------------|
|
|
@@ -139,7 +139,7 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
|
|
|
139
139
|
| `preloaded_memory_names`, `muted_memory_names` | `--memory` and `--mute`, written before the worker starts so a respawn builds the same prompt. | creation |
|
|
140
140
|
| `prompt_notes` | The model notes the system prompt carried at its last build (`[{name, scope, chars, digest}]`, see [Memory](memory.md#model-notes)). | the Engine, at each prompt build |
|
|
141
141
|
| `parent_id` | The session that delegated this one or forked it (`ctx.sessions.fork`); `null` otherwise. See [Delegating](#delegating). | `SessionManager.spawn_session`; `ChildMove` writes a `parent.json` beside it instead of this file |
|
|
142
|
-
| `scratch` | A `chi scratch` session, deleted when its
|
|
142
|
+
| `scratch` | A `chi scratch` session, deleted when its terminal leaves. | creation |
|
|
143
143
|
| `last_turn` | How the last turn ended (`LastTurn`), for a parent's wait and the web's notifications. | the Engine, at a turn's end |
|
|
144
144
|
| `verdicts` | Verdict cards recorded by completed Verify turns, in append order. | `Engine#record_verdict`, `VerifyFresh` |
|
|
145
145
|
| `delegate` | Started by the `delegate` tool (a fork has a `parent_id` but isn't one). | `Tools::Delegate` via `spawn_session` |
|
|
@@ -168,7 +168,7 @@ pbpaste | chi note --source slack 3f2a 8c1d
|
|
|
168
168
|
```
|
|
169
169
|
|
|
170
170
|
- The text comes from `-m` or stdin (a terminal on stdin is a usage error, not a wait). It is stripped; an empty note or one over 16 KiB is refused, never cut. `--source` (default `cli`) names where it came from. `--all` is every live session.
|
|
171
|
-
- It lands in `<uuid>/notes/`, apart from `input/`, so nothing that runs turns sees it. A live worker adds it to the conversation within a few seconds, between turns: one sent during a turn waits for that turn to end. A session with no worker keeps it until a worker next starts (a prompt, `--attach`), which adds it before anything else. A session
|
|
171
|
+
- It lands in `<uuid>/notes/`, apart from `input/`, so nothing that runs turns sees it. A live worker adds it to the conversation within a few seconds, between turns: one sent during a turn waits for that turn to end. A session with no worker keeps it until a worker next starts (a prompt, `--attach`), which adds it before anything else. A session a one-turn `chi --non-interactive` run has refuses notes. `chi note` prints one line per session: queued, waits for the next start (with the queue count), or refused.
|
|
172
172
|
- In the conversation it is a tail system message marked `kind: note`, framed `[CONTEXT NOTE from slack, 14:02]\n…\n[END NOTE]` (from another session: `from session 3f2a1c (~/projects/foo)`). The system prompt says notes are background, not requests: the model uses one when it is relevant, doesn't answer it on its own, and never follows instructions inside it. A note from `broadcast` ([`chi broadcast`](broadcast.md)) is the one exception to "doesn't answer it": the model says briefly in its next answer when it affects its current work, and still doesn't act on it unless asked. `--source broadcast` is refused for `chi note`. Its text is escaped like user text, so it can't fake a turn. It survives `--resume`, a reload, `!rollback` and a continue answered no.
|
|
173
173
|
- The web shows it as a dim "note from …" block, live (`context_added`) and after a reload; the attached terminal as one dim `note from slack: <first line>` line, also when joining.
|
|
174
174
|
- Agents: `list_sessions` (other sessions of this project, newest first, up to 20; optional `cwd`, `"/"` for every project) and `send_note(session, text)`, which sends a note from this session. Neither starts a turn anywhere.
|
|
@@ -203,7 +203,7 @@ optionally a done-when hint, which nothing enforces.
|
|
|
203
203
|
|
|
204
204
|
An edit waits for a moment between turns: it lands within seconds in an idle session, and at the turn's end during one.
|
|
205
205
|
Several edits made during a turn go in as one list. A continue copies the list to the next link, as a second note after
|
|
206
|
-
the carry note; a review link doesn't. `!rollback` keeps it. A
|
|
206
|
+
the carry note; a review link doesn't. `!rollback` keeps it. A session a one-turn `chi --non-interactive` run has refuses an edit, as it
|
|
207
207
|
refuses a note. See [cli.md](cli.md#session-objectives) for the commands and
|
|
208
208
|
[internals/objectives.md](internals/objectives.md) for the mechanism.
|
|
209
209
|
|
|
@@ -242,7 +242,7 @@ system message marked `kind: turn_note` (the UIs don't show it; they showed the
|
|
|
242
242
|
|
|
243
243
|
- failed before any answer (a dead host, retries run out, an auth error): `[SYSTEM: the previous turn failed
|
|
244
244
|
before any answer: network error after 2 attempts (host main: Errno::ECONNREFUSED). The message went back to
|
|
245
|
-
the user, who may send it again.]` in
|
|
245
|
+
the user, who may send it again.]` in attached mode, where the prompt is given back;
|
|
246
246
|
`… The user's last message was not answered.]` after `-p`, where the prompt stays in the session;
|
|
247
247
|
`… The wake turn for the change in attached context pr-7 was not answered; chi starts no other wake turn until
|
|
248
248
|
the user writes.]` after a wake turn chi started (a context source's change, a delegate's report, a background task that finished), not the user
|
|
@@ -296,7 +296,7 @@ pbpaste | chi send 3fa2 # the clipboard is the messa
|
|
|
296
296
|
|
|
297
297
|
- With both stdin and `-m`, stdin is context: each line becomes a `> ` quote (as web annotations quote a selection), then a blank line, then the message. With only one, it goes in as is. A terminal on stdin is ignored. Both empty is a usage error; over 16 KiB is refused, never cut.
|
|
298
298
|
- It goes through the same path as the web composer (`SessionManager.deliver_turn`): the worker's Bridge when it is up, so every attached UI shows it as your message (client id `cli:send`, shown like any user message); the input file when the worker is on its way out. During a running turn it is merged into that turn at the next step, like a message typed then, and cuts nothing. `--cut` asks for a cut, as `/cut TEXT` in a terminal does: a generation that has streamed only thinking for `steer.cut_after` seconds (per host/model: `steer_cut_after`) is cut (now, or once its thinking passes that), and the step starts again with the message. To an idle session `--cut` sends normally. `--cut --wait` into a running turn is refused like any `--wait` there (`busy: a turn is running`). `--queue` (not with `--cut`, a usage error) keeps the message for after the running turn: it runs as a turn of its own then (as `/queue TEXT` does), and a plain message sent after it still goes in at the running turn's next step. To an idle session it runs at once (it still goes as a queue, so a turn another client starts first never takes it as a steer). A worker older than queueing would steer it instead, so it is refused (to an idle session too) (`refused: this session's worker predates queueing, …; restart it`, exit 1) and nothing is sent; `chi sessions restart ID` and send again.
|
|
299
|
-
- A session with no worker gets one started (like `--attach` or the web composer). A session
|
|
299
|
+
- A session with no worker gets one started (like `--attach` or the web composer). A session a one-turn `chi --non-interactive` run has refuses it. Bridges listen on 127.0.0.1: another machine's sessions are reached through its chi web, `chi --remote NAME send …` ([Remote sessions](cli.md#remote-sessions)).
|
|
300
300
|
- Fire and forget: it returns once the message is queued and never prints the answer; that shows in whatever is attached. A guardrail "ask" waits for a UI to answer it, so with nothing attached the turn stalls there until one attaches (`chi --attach ID`).
|
|
301
301
|
- `--image PATH` (repeatable, up to 20) sends images with the message, as the web composer's chips do. Each file is read once before anything is sent (bmp, tiff and heic converted to png, large ones downscaled, as for `@path`); a missing file or one that isn't an image stops the send (exit 1; with `--wait --format json`, an `error` line) and nothing is sent; more than 20 is a usage error (exit 2). Each session gets its own copy in `<session>/images/`. Text is still required (`-m` or stdin; context alone counts), also with `--wait`. The session's model must see images: one known not to (the host's model list says text-only, `vision: false` in `models:` or `hosts:`, a native llama.cpp without a vision model) is refused before anything is sent, with the reason (`<id> refused: gemma can't take images (…); send text only or switch the model (/model)`); the other sessions of the same send still get it, and the exit is 1. With `--new` nothing is started. When chi can't tell (a native host whose `/props` doesn't answer, a list that doesn't say), the message is sent as before and a text-only model fails the turn in the session. A running turn doesn't take images mid-turn: the message runs as the next turn, and the line says `(runs after the current turn)`. With `--new` the session starts idle with the message as its preview, then the message goes in as its first turn once its worker is up (`<id> started with 1 image`); if the worker doesn't come up in 5 s the session is kept, with its id on the `failed:` line.
|
|
302
302
|
- A session command runs as the command, as typed in a terminal: `chi send -m "/model qwen" 3fa2` switches the model (`sent as a session command`), and the model never sees the line. What counts is what the session's worker knows (the built-ins, `!cmd`, its bundles' commands); an unknown `/word` (`/usr/bin/env is missing`) goes in as a message. A UI-only command (`/stats`, `/exit`, `/recap`, …) is refused instead: the worker doesn't run it, so the line would reach the model as a prompt (`<id> refused: /stats is a terminal and web command; use chi sessions stats <id8>`; exit 1, and nothing is sent). With `--new` a line starting with `/` or `!` starts the session idle and goes to its worker once it is up, which decides. With `--wait` a command has no reply to wait for: exit 0 at once, `<id> sent as a session command: no reply to wait for …` on stderr (`{"status":"command",…,"detail":…}` with `--format json`); its output shows where the session is open.
|
|
@@ -335,11 +335,11 @@ chi send --new --continues last:3fa2 -m "new day" # the next link of 3fa2's
|
|
|
335
335
|
|
|
336
336
|
## Session titles
|
|
337
337
|
|
|
338
|
-
A session is named by its first message until you give it a title: `/title Coordinator` (in
|
|
338
|
+
A session is named by its first message until you give it a title: `/title Coordinator` (in a terminal, the web composer or `chi send -m "/title Coordinator" ID`; a parent agent may name its delegates the same way). The title is saved in the session file (`title`), whitespace collapsed and cut at 80 characters, and runs mid-turn too, at once. `/title` alone shows it, and without one says what the card shows — `title: none (shows the auto title: Parser tab handling)`, `title: none (shows the recap: Fixing the login page.)` when the recap has no title, or `title: none (shows the first message: hey chi …)` with no recap; `/title --clear` drops the title, and what the lists show comes back. A session whose first line was the command itself (`chi send --new -m "/title X"`) takes its preview from its first real message.
|
|
339
339
|
|
|
340
340
|
Without a title, a session is named by its **auto title** once its recap has one, else by its recap's first sentence (see [Session recap](cli.md#session-recap); its subject phrase stripped), else by its first message: on its card (which leaves out its recap line when the name is that same sentence), the families and chain rows, the top bar and the browser tab, the delete confirm and the search, the tab's and the phone's notifications, and `chi sessions list` (plain and `--live`; json has `auto_title`, tsv and json's `desc` keep the first message). The auto title is 2-6 words the recap's model writes on the line before the recap, in the same request (`Title: …`, split off by `RecapTitle`; the recap text never holds it). It is saved in the session's `recap.json` (`title`), so it goes when a stale recap is dropped; each later recap is shown the earlier title and keeps it unless the focus moved, and a reply without a usable one (not 2-6 words, or naming the user or the assistant) keeps the earlier title. A hand title always wins and is never overwritten. With the recap off no new auto title is written; a saved one still shows. The auto title is for display only: never in a model request (the `list_sessions` tool, `ctx.sessions`, a parent's children, `chi broadcast` keep the title, else the first message), nor on the terminal's status row, nor on a participant's page (the title or the review's subjects).
|
|
341
341
|
|
|
342
|
-
The title names the session wherever its first message did: `chi sessions list` (plain, `--live`, tsv and json's `desc`, before the recap; json has `title` too), `chi sessions delete`'s line, the `list_sessions` tool, `chi broadcast`, the desktop helper, a parent's children (the coordinator bundle's `/children` card, `ctx.sessions.children`; `ctx.sessions.read` too), and in the web the cards, the families and chain rows, the top bar and the browser tab, the notifications and the search; the terminal's status row too (
|
|
342
|
+
The title names the session wherever its first message did: `chi sessions list` (plain, `--live`, tsv and json's `desc`, before the recap; json has `title` too), `chi sessions delete`'s line, the `list_sessions` tool, `chi broadcast`, the desktop helper, a parent's children (the coordinator bundle's `/children` card, `ctx.sessions.children`; `ctx.sessions.read` too), and in the web the cards, the families and chain rows, the top bar and the browser tab, the notifications and the search; the terminal's status row too (first, see [Status Line](cli.md#status-line)). In the web, `✎` (Rename this session) sits next to the session's title, on its active strip card or after the top bar's title: a small input over the title, prefilled with it; Enter or a click elsewhere sends `/title TEXT` when it changed (empty: `/title --clear`), Esc closes it unsent, and so does the title leaving the screen (All sessions opened, the card swiped out of a phone's strip). A session held by a one-turn `chi --non-interactive` run has no `✎`, nor one that shows no title yet (`/title` still works from the composer). A continue's new link keeps the title (see [Session chains](#session-chains)). An older `chi web` (or any chi from before the field) that saves a session drops its title: restart `chi web` after upgrading.
|
|
343
343
|
|
|
344
344
|
## Session chains
|
|
345
345
|
|
|
@@ -351,12 +351,12 @@ A session can **continue** an earlier one: "the next run of the same routine" (a
|
|
|
351
351
|
- **The carry note:** the new link starts with a context note from `session chain` (shown in its transcript as `note from session chain`), queued before its worker spawns, so its first turn sees it: `This session continues 3fa2c1d0 (2026-10-08 09:12), the previous link of its chain. Its recap, as of 2026-10-08 18:02:` and the previous link's recap (`recap.json`; cut at 8000 characters). No recap (fewer user turns than a recap needs, recaps off, none written in time): `… It left no recap.` Then `Its last answer:` and the previous link's last answer, read after the recap wait, so its exact shas, paths and next steps survive: a longer one keeps its last 4000 characters, after a `…` (a wrap-up closes on what comes next). There is none when the newest bubble was a step that asked for tools, when a later turn failed or was cut before it answered, and on a review link (a continue doesn't copy the grants, so the next link's participants could differ); `session.carry_last_answer: false` leaves it out of every note. No other transcript. The note never makes a continue fail: it stays within the 16 KiB a note may hold, the answer taking what the head and recap leave, and a session file that can't be read gives no answer. An idle new link holding only the note is kept, not discarded as empty.
|
|
352
352
|
- **Its attached context:** the previous link's movable sources (a command source by default; a pushed one stays) are copied to the new link before its worker starts, with their last text and the removed and muted markers, and a second note (`attached context`, after the carry note) names what moved and what stayed. A push into the archived link is then refused with the new link's id. See [Movable context](context.md#movable-context).
|
|
353
353
|
- **The recap first:** a previous link with a live worker is asked for a recap (the Bridge's `POST /recap`) and given up to 30 s to write it; one with none wrote its recap when its worker left.
|
|
354
|
-
- **The previous link is archived** before the new one starts, by the archive's own rules (see *Archiving a session*): a turn running, a prompt queued or the step-limit question refuse it, a live idle worker is stopped, a
|
|
355
|
-
- **Its open delegates move to the new link** instead: each direct delegate that is running, waiting for an answer, has a live worker or a reply its parent wasn't given, or has such a delegate below it (checked again after the recap wait, so one delegated meanwhile moves too). It moves with its own delegates, stays unarchived, and from then on is the new link's: its card folds under the new link, its reports ring the new link (an idle one runs a wake turn for them, as for its own delegates), `delegate_result` and the `⑂` chip count it there, and what its parent was already told (`delegates.json`) and any report still waiting come along, so nothing is reported twice. A moved delegate gets a context note from `session chain`, `Your parent session is now 4b7e9a01 (…), the next link of its session chain: …`, as its system prompt keeps naming the parent it started with. The move writes `<child>/parent.json` (`{parent_id, from, at}`) beside the child's session file, never the file itself, so a live child worker saving its own copy changes nothing; every reader applies it (a later move overwrites it; it is never removed). A continue is **refused for an open delegate that can't move** (`SessionContinue::Refused`, naming it): one whose live worker runs an older chi (`older chi worker 0.46.1; restart it`: `chi sessions restart <id>`, or stop it) or that
|
|
354
|
+
- **The previous link is archived** before the new one starts, by the archive's own rules (see *Archiving a session*): a turn running, a prompt queued or the step-limit question refuse it, a live idle worker is stopped, a session a one-turn `chi --non-interactive` run has is refused. Its finished delegates are archived with it, as any archive does.
|
|
355
|
+
- **Its open delegates move to the new link** instead: each direct delegate that is running, waiting for an answer, has a live worker or a reply its parent wasn't given, or has such a delegate below it (checked again after the recap wait, so one delegated meanwhile moves too). It moves with its own delegates, stays unarchived, and from then on is the new link's: its card folds under the new link, its reports ring the new link (an idle one runs a wake turn for them, as for its own delegates), `delegate_result` and the `⑂` chip count it there, and what its parent was already told (`delegates.json`) and any report still waiting come along, so nothing is reported twice. A moved delegate gets a context note from `session chain`, `Your parent session is now 4b7e9a01 (…), the next link of its session chain: …`, as its system prompt keeps naming the parent it started with. The move writes `<child>/parent.json` (`{parent_id, from, at}`) beside the child's session file, never the file itself, so a live child worker saving its own copy changes nothing; every reader applies it (a later move overwrites it; it is never removed). A continue is **refused for an open delegate that can't move** (`SessionContinue::Refused`, naming it): one whose live worker runs an older chi (`older chi worker 0.46.1; restart it`: `chi sessions restart <id>`, or stop it) or that a one-turn `chi --non-interactive` run has (`in use by a one-turn chi --non-interactive run or an older chi's terminal; let it finish`). An older `chi web` or CLI still running shows a moved delegate under the archived link until it is restarted.
|
|
356
356
|
- **If the continue fails part-way:** a new link that fails to start gives its moved delegates back before it is deleted (each gets a note saying so; its worker is stopped first, if one started, and a worker that won't stop is reported, the link left), with any report one of them rang it with meanwhile; the previous link, back in the lists, is woken for reports waiting. A continue that dies in the middle leaves a `move.json` in the new link's folder: the new link's next worker finishes that move (only the delegates it names, and without a second note to one already told) as it starts. Until the new link's own worker holds it, a ring from a moved delegate doesn't start another (a `starting` marker in its folder, which that worker removes; a stale one, over 60 s old or from a process that is gone, is ignored). An open delegate below an archived delegate refuses the continue (`under archived …`): unarchive that one, or archive this one.
|
|
357
357
|
- **Refused too:** a link that is continued already (naming the link that continues it; continue that one, or `last:<id>`), a previous link whose folder is gone, and one whose model's host can't run it when no default model is configured to fall back to. A refusal starts nothing and archives nothing; a new link that fails to start puts back what was archived. Every refusal that can be known up front (the archive's ones included) comes before the recap wait. One continue of a session runs at a time (a lock in its folder; other chains don't wait), so a double click finds the first one's link and is refused, while a `last:<id>` continue that waited follows the chain to its new end.
|
|
358
358
|
- **Web:** `POST /api/sessions` takes `continues: "<id, prefix or last:id>"`, with `idle: true` (a new link waiting for its first message, previewed by the previous link's, or `preview`) or a `prompt`, and `model` (or `model_name`) for the new link's model, but not `dir`, `llm_context`, `thinking`, `settings`, `toolkit`, `tools`, `no_tools` or `parent_answers` (400 `invalid_continues`: the previous link's are taken). 201 is the new session's summary, `continues` set, with `moved`: the ids of the delegates that moved to it (`[]` for none), and `model_warning`: the default-model fallback's warning, then any about the model id, joined by `; ` (`null` for none), and `opener`: the previous link's first typed message, in full, when that link continues one itself (`null` otherwise: a chain's first link's first message is its task), and `moved_context`: `{moved: [names], stayed: [{name, why}]}`, the attached context it took (`why` `pushed` or `not movable`); 404 `not_found`; 400 `ambiguous_id` for a prefix of several sessions, `invalid_model` for a given model whose host config.yml doesn't have (or a fallback with no default model); 409 `continued` with `next_id` (the link that continues it already, which the page opens instead), `open_children` with `ids` (open delegates that can't move), `folder_gone`, `busy`, `scratch` or `owned_by_tui`. Every session summary carries `continues`.
|
|
359
|
-
- **The page:** the info bar's `continue →` continues the open session (`continues: <id>, idle: true`; it reads `continuing…` through the recap wait) and opens the new link with an opener in the composer, not sent (a draft written in this session stays instead; one carried over from another session or the start page gives way to the opener, and the toast's **Restore draft** brings it back): the previous link's own first typed message when that link continues one (the 201's `opener`; a `!shell` line isn't taken), else `Continue where we left off.`. So whatever you sent on the last link comes back on the next one, and editing it once changes it for every later link; the previous link is archived, a toast says so (`Continued 3fa2c1d0 in 4b7e9a01; 3fa2c1d0 is archived; 1 open delegate moved`), and a moved delegate's card folds under the new link. When the previous link's model's host is gone the link starts on the default model and the toast carries the server's `model_warning` too, and then stays until closed. A fresh link (one with no turn yet, however it was made: `continue →`, `chi send --new --continues`, a reload) shows the model picker in its composer with the link's model, until its first turn starts (a failed or canceled first turn ends it too); a pick runs `/model <name>` in the link (a command bubble; the info bar follows) and changes neither the start page's remembered model nor its Recent list, and a Send right after it waits for the `/model` to be taken. A link a chi
|
|
359
|
+
- **The page:** the info bar's `continue →` continues the open session (`continues: <id>, idle: true`; it reads `continuing…` through the recap wait) and opens the new link with an opener in the composer, not sent (a draft written in this session stays instead; one carried over from another session or the start page gives way to the opener, and the toast's **Restore draft** brings it back): the previous link's own first typed message when that link continues one (the 201's `opener`; a `!shell` line isn't taken), else `Continue where we left off.`. So whatever you sent on the last link comes back on the next one, and editing it once changes it for every later link; the previous link is archived, a toast says so (`Continued 3fa2c1d0 in 4b7e9a01; 3fa2c1d0 is archived; 1 open delegate moved`), and a moved delegate's card folds under the new link. When the previous link's model's host is gone the link starts on the default model and the toast carries the server's `model_warning` too, and then stays until closed. A fresh link (one with no turn yet, however it was made: `continue →`, `chi send --new --continues`, a reload) shows the model picker in its composer with the link's model, until its first turn starts (a failed or canceled first turn ends it too); a pick runs `/model <name>` in the link (a command bubble; the info bar follows) and changes neither the start page's remembered model nor its Recent list, and a Send right after it waits for the `/model` to be taken. A link a one-turn `chi --non-interactive` run has, or one continued already, shows none. A link continued already has no `continue →`: its `→` link goes to the next link (a stale page's continue answered 409 `continued` opens `next_id`). A continue refused for open delegates that can't move shows a toast that names each one (its preview and what the server says to do: `older chi worker 0.46.1; restart it`, `in use by a one-turn chi --non-interactive run or an older chi's terminal; let it finish`), each a link to its page; any other refusal shows the server's words. The info bar of a link carries `← 2026-10-08` (the previous link's day) and, on an earlier link, `2026-10-10 →`, each a link with its id, its full start (`2026-10-10 14:32`) and its preview in the tooltip; a neighbour that started on the open link's own day shows its time instead (`← 09:12`, `14:32 →`). A link made by `continue →` inherits the previous link's preview (and its title, if it has one) when it is created, so its card keeps its chain's title whatever opener it is sent, and its top bar and browser tab show its card's title. A link with a recap of its own reads its auto title, else its recap's first sentence, instead (a link without one reads its chain's name, and a first link's recap names its chain). A link whose title is the opener and nothing else (a `chi send --new --continues` link whose first message is `Continue where we left off.`) shows its chain's first link's title instead, on its card, in the popover and in its info bar's title line, display only (its messages and data keep the opener; its own title when the first link is gone). The lists fold a chain into the card of its latest listed link, whatever the earlier links' archived state (typing in an archived link brings it back, folded there still), their delegates with them: the card's `↩ #N` chip (`#N of M` when later links aren't listed; its tooltip `Session chain: link N of M`) opens a popover of the other links, newest first (`#N`, its date, with its time (`2026-10-17 14:32`) when another link of the chain started the same day, its own title when it differs from the card's (a link inherits its chain's, so mostly it doesn't), else its recap's first sentence, else its preview, `archived`), a click opens one; the chip shows a live dot or `waiting` for a link that has a worker or waits on the user, and a waiting link puts its chain's card first. All sessions folds nothing while a search is typed or the `archived` toggle is on: every link is a card of its own then, its chip `#N of M`, with its own delegates folded in, so a search finds an earlier link and its delegates.
|
|
360
360
|
- **CLI:** `chi send --new --continues (ID|PREFIX|last:ID) [--model M] [-m TEXT]` (see *Starting a session*).
|
|
361
361
|
- **Which link:** an id, a unique prefix, or `last:<id or prefix>`, the latest link of the chain that session is in. It walks the links that still exist: a deleted link in the middle (`chi sessions delete`, the retention sweep of an unarchived one) ends the walk before it, so `last:` of an earlier link then names that earlier link, and continuing it starts a second line beside the links after the gap; name a link after the gap instead. A trigger names the chain this way: `chi send` to an archived link would bring it back (a human's input), not continue the chain.
|
|
362
362
|
|
|
@@ -463,7 +463,7 @@ mise.toml pins Ruby 3.4 gets that `ruby`, `gem` and `bundle` in its tools while
|
|
|
463
463
|
(`SAMAGOTCHI_PROJECT_ENV_ORIG`) and that `chi` takes them back, so project A's PATH, `[env]` values, GEM_* and
|
|
464
464
|
library paths never reach a session in B.
|
|
465
465
|
- **Context sources** run in the project env of the worker that fetches them: the same source can read differently
|
|
466
|
-
from another session's worker
|
|
466
|
+
from another session's worker.
|
|
467
467
|
- **Pair turns** (a participant's, see [Participants](internals/participants.md)) run their `execute` without the
|
|
468
468
|
project env. MCP servers are the session's, started once in the project env, so a pair turn's MCP calls do use
|
|
469
469
|
it. A participant's command that runs `mise` or `direnv`, and any write to mise's config (`mise.toml`,
|
|
@@ -488,7 +488,7 @@ mise.toml pins Ruby 3.4 gets that `ruby`, `gem` and `bundle` in its tools while
|
|
|
488
488
|
run in that folder shows it, with the fix, until a later worker there applies the env.
|
|
489
489
|
- `chi self` run in a tool shows what the worker computed (`project env`: `mise (ruby PATH)` or `none (why)`) and the
|
|
490
490
|
chi home (`home`).
|
|
491
|
-
-
|
|
491
|
+
- A `--non-interactive` run has no worker: its tools keep the shell's env.
|
|
492
492
|
|
|
493
493
|
## Delegating
|
|
494
494
|
|
|
@@ -498,7 +498,7 @@ The `delegate` tool hands a task to a **child session**: an ordinary chi session
|
|
|
498
498
|
- `cwd:` starts the child in another folder of the parent's repository: a linked worktree (`git worktree add ../<repo>-<task> -b <branch>`, which the model runs with `execute` first, under guardrails; chi runs no git here) or a subfolder; a relative path is taken from the parent's folder. The folder must exist, have the parent's project root (the same common git dir, `MemoryPaths.project_root`) and be in a work tree: a git dir (`.git`, `.git/objects`, a bare layout's `.bare`) or a bare layout's container is refused (`git rev-parse --is-inside-work-tree`, read-only, decides). Outside git only a subfolder of the parent's folder is allowed. Otherwise the tool returns `Error: cwd must be a folder of this session's repository …` or `… is not in a work tree …`. A follow-up (`session:`) refuses `cwd:`: the child keeps its folder. The child's guardrails measure from its own worktree, and project memories, `list_sessions` and the web's project are the same as the parent's. With no `cwd:` and a task that names another checkout of the same repository (a path in the task belonging to a linked worktree, read from the files with `Guardrails::GitInfo.checkouts`, no git process), the result carries `Warning: this child runs in <folder>, but its task names <checkout>. If it should work there, stop it (chi sessions stop <id>) and call delegate again without session, with cwd: <checkout>.` One line however many are named; nothing for a path of the child's own checkout, a folder that isn't one, a path with spaces, or the main checkout when the child runs in a linked worktree of it (the work belongs in the worktree, so naming a main-checkout file is not a forgotten `cwd:`). The child starts as it would anyway, and with `wait: true` (the default) the line comes after the child ran, so it only helps with `wait: false`. A `Warning:` for an unknown model id comes first, then this one. `Log.info(:worker, "delegate_other_checkout", parent:)` records each line.
|
|
499
499
|
- `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.
|
|
500
500
|
- A wait also ends early when a person's message waits for the parent's turn's next step (the `task_wait` rule: `Engine#input_waiting?`, `SessionInbox.steer_waiting?`, never in a participant's turn): `ReplyWait`'s `message_waiting:` probe gives `:message_waiting`, checked after every outcome the child can arrive at (a reply, a question, an end, a gone worker) and a cancel, before the deadline, and the result says `no reply yet: a message from the user is waiting; the child keeps running. Read the message first.` with the same last line as a timeout's. The child is never touched, the cursor doesn't move (the reply still comes, as a report or at the next `delegate_result`), a relayed approval's outcome line stays in the result, and a reserved report reads as for a timeout. A delegate child's report doesn't end a wait. This holds for `delegate`'s own wait too (both go through `DelegateWait`).
|
|
501
|
-
- 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`,
|
|
501
|
+
- 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`, `--non-interactive`) is told `delegate_result <id> waits again` instead, and `wait: false`'s result says `delegate_result waits for its reply`.
|
|
502
502
|
- 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.
|
|
503
503
|
- **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)).
|
|
504
504
|
- **A child at its cost cap** (the [cost-cap bundle](bundles.md#the-cost-cap-bundle), the child's own cap: `child:` in its settings, or `/cap`) asks a `hook` question, "Cost cap", `Continue` or `Stop`. Like every hook's question on a `delegate` child, it goes where it can be decided, under the child's `guardrails.parent_answers` (config.yml's, or the child's own when stricter): when the parent may pick an option other than a deny (Stop) there, `standard` and `soft` here, it comes back to the parent's model as `status: question` with the `chi answer` commands; when it may not, `hard` here (a parent's Continue is refused), it goes to the parent's user as a card, as approvals do (the relay below), and the model sees one line: `hook question relayed to your user: Cost cap → Stop` (or `dismissed`, which the hook reads as it likes, cost-cap as Stop; `refused by the child …`, `answered on the child`, `still open (your turn was stopped)`). Free text alone doesn't count as an option the parent may pick. A **background** child (`wait: false`) paused at its cap rings its parent as a question does (delegate reports, below): the parent's model gets it as a report when it may answer; otherwise the parent's worker relays it to its user at its next step boundary, or in a wake turn's first boundary when idle (before the model is called), and the report the model gets is the outcome line. While that card is open the parent's turn waits on it; a Stop closes it and leaves the question for your next input (or a `delegate_result`): no wake turn offers that question again meanwhile, while other children's reports and attached-context wakes go on. Under the default `soft` that means a background parent's model now hears its child's cost cap, and may Continue it.
|
|
@@ -506,10 +506,10 @@ The `delegate` tool hands a task to a **child session**: an ordinary chi session
|
|
|
506
506
|
- 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.
|
|
507
507
|
- 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.
|
|
508
508
|
- 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.
|
|
509
|
-
- No relay where the parent can't host one (a `--
|
|
509
|
+
- No relay where the parent can't host one (a `--non-interactive` run) or, for an approval, isn't waiting (`wait: false` until the next `delegate_result`; a hook's question it may only deny is relayed from a delegate report instead, above): 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 the hooks' questions the parent may answer, come back to the model as before. A hook's question already answered on one of the parent's relay cards (a refused answer: it waits for the user on the child) is not relayed again; one whose card closed (a Stop, the child gone) is, and an approval always is, as before. A grandchild's (a plugin's `ctx.sessions.fork` can make one) is relayed hop by hop: `delegate ab12 → cd34 asks:`.
|
|
510
510
|
- **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 decides (the child's own `ask_user_question`, the step-limit continue, a hook's question, relayed to the parent's user first when the parent may only deny it; not an approval, which the relay or the user on the child's card answers, nor a card the child relays from its own child), 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"`.
|
|
511
511
|
- Parent mid-turn: the report joins the turn at its next iteration boundary (a "delegate report" bubble in the web, `delegate report> ab12cd34 answered: …` in the attached terminal).
|
|
512
|
-
- Parent idle: it runs a turn for the report (a *wake turn*, `last_turn.origin: "delegate_report"`) within the worker's 5 s tick, or wakes a worker for it when the parent's has idle-exited. Several reports that wait together are one turn and one message. A stopped, archived or scratch parent, or one
|
|
512
|
+
- Parent idle: it runs a turn for the report (a *wake turn*, `last_turn.origin: "delegate_report"`) within the worker's 5 s tick, or wakes a worker for it when the parent's has idle-exited. Several reports that wait together are one turn and one message. A stopped, archived or scratch parent, or one a one-turn `chi --non-interactive` run has, is not woken: the rings wait on disk and join the next turn it runs (the user's message first). An archived parent that a later link continues wakes that link instead, which takes the rings its moved delegates left there (see *Session chains*). A worker just started (`chi send` to a stopped parent) runs no wake turn in its first 2 s, so the message it was started for goes first.
|
|
513
513
|
- The parent's cursor per child (the newest reply it was given and the child as it was then) is in its `delegates.json`, shared with `delegate_result` and `wait: true`: a reply the model already had is never reported again, also after the parent's worker restarts. A ring is deleted only when the turn that took its report stays in the conversation (completed, canceled, or failed but kept: one that got to tool steps, or a reminder or continue turn, which keep what they read); a turn rolled back (a failed one that got nowhere, a canceled continue turn) leaves it for the next one (and the report isn't handed back to the composer). So the model reads each report once, unless a later `!rollback` erases a kept turn (what it read goes with it).
|
|
514
514
|
- Limits: `session.max_wakes` (default `10`) wake turns in a row with no human input; past it the reports wait for the next message and the UIs show `N delegate reports waiting; they join your next message`. A wake turn that fails, or is stopped before the model call (a Stop, a `before_turn` hook's cancel), pauses wakes until the next human input; its reports' rings stay, so they join that input's turn, and a failure's note tells the model so. A continue offer waiting in the parent takes the reports into its continue turn instead.
|
|
515
515
|
- `session.delegate_reports`: `wake` (default), `queue` (no wake turns: reports join the parent's next turn) or `off` (no rings; only `delegate_result` asks, as before). Env `SAMAGOTCHI_SESSION_DELEGATE_REPORTS`, `SAMAGOTCHI_SESSION_MAX_WAKES`.
|
data/docs/sub-agent.md
CHANGED
|
@@ -138,7 +138,7 @@ children's approvals go to its own user as its own approvals, and its model
|
|
|
138
138
|
sees only how each was answered. See
|
|
139
139
|
[Sessions: Delegating](sessions.md#delegating). What follows is for other
|
|
140
140
|
parents (Claude Code, Codex, a script), and for a chi parent that can't host
|
|
141
|
-
the relay (a `--
|
|
141
|
+
the relay (a `--non-interactive` run).
|
|
142
142
|
|
|
143
143
|
An approval is a guardrail rule with `verdict: ask`: its user said "ask me". A
|
|
144
144
|
parent may always deny it (`--option Deny --text "why"`, `--text` alone, or
|
|
@@ -162,7 +162,7 @@ Start a child in the folder it should change (`chi send --new --dir PATH`):
|
|
|
162
162
|
writes and mutating git outside its own repo ask (`write-outside-repo`,
|
|
163
163
|
`git-outside-repo`), which a parent can at most allow once per call.
|
|
164
164
|
|
|
165
|
-
An answer typed into `chi --attach`
|
|
165
|
+
An answer typed into the attached TUI (`chi`, `chi --attach`) counts as a parent's too when
|
|
166
166
|
stdin isn't a terminal or an agent marker (`CLAUDECODE`, `AI_AGENT`,
|
|
167
167
|
`CODEX_THREAD_ID`, `SAMAGOTCHI_PARENT_SESSION`) is set.
|
|
168
168
|
|
data/docs/system-one.md
CHANGED
|
@@ -55,7 +55,7 @@ per-message judge on an objective didn't work as a gate on the pod (AUC about .5
|
|
|
55
55
|
- The state: `objective` (its text and done-when, no id or status) and `message` (2000 characters: a message's head, an answer's tail).
|
|
56
56
|
**That text goes to the System 1 host**, a hosted one too, and into the run. A participant's turn, a review session and a scratch session are never asked about.
|
|
57
57
|
- At most 6 objectives per message. Each direction has one ask running: what comes meanwhile is a run with `asked: false`, `skipped: busy`.
|
|
58
|
-
- The asks run on `SideJudgments`' threads beside the turn, so a turn doesn't wait; on a one-slot server shared with the session's model, a request can still queue behind or ahead of them. They share `/v1/systemone` with approvals: on a slow one-slot host an approval can wait behind up to 2 in-flight asks and time out to the user. A failed ask is a run with its `failure` and one log line (`system_one_failed`)
|
|
58
|
+
- The asks run on `SideJudgments`' threads beside the turn, so a turn doesn't wait; on a one-slot server shared with the session's model, a request can still queue behind or ahead of them. They share `/v1/systemone` with approvals: on a slow one-slot host an approval can wait behind up to 2 in-flight asks and time out to the user. A failed ask is a run with its `failure` and one log line (`system_one_failed`).
|
|
59
59
|
|
|
60
60
|
## The store
|
|
61
61
|
|