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.
Files changed (205) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -1
  3. data/README.md +2 -3
  4. data/bin/chi +23 -24
  5. data/docs/architecture.md +30 -33
  6. data/docs/broadcast.md +5 -5
  7. data/docs/bundles.md +9 -12
  8. data/docs/cli.md +80 -96
  9. data/docs/configuration.md +27 -26
  10. data/docs/context.md +3 -3
  11. data/docs/desktop.md +1 -1
  12. data/docs/guardrails.md +8 -10
  13. data/docs/hooks.md +9 -9
  14. data/docs/internals/attached-context.md +2 -2
  15. data/docs/internals/background-tasks.md +3 -4
  16. data/docs/internals/block-renderers.md +4 -0
  17. data/docs/internals/bridge.md +3 -3
  18. data/docs/internals/context-telemetry.md +1 -1
  19. data/docs/internals/engine.md +33 -7
  20. data/docs/internals/objectives.md +6 -5
  21. data/docs/internals/participants.md +7 -7
  22. data/docs/internals/prompt-caching.md +1 -1
  23. data/docs/internals/remote-sessions.md +7 -7
  24. data/docs/internals/web-push.md +1 -1
  25. data/docs/memory.md +2 -2
  26. data/docs/plugins.md +23 -28
  27. data/docs/sessions.md +24 -24
  28. data/docs/sub-agent.md +2 -2
  29. data/docs/system-one.md +1 -1
  30. data/docs/testing.md +16 -6
  31. data/lib/samagotchi/answer_text.rb +34 -0
  32. data/lib/samagotchi/bridge.rb +43 -32
  33. data/lib/samagotchi/bridge_client.rb +10 -9
  34. data/lib/samagotchi/broadcast/recipients.rb +2 -2
  35. data/lib/samagotchi/broadcast/triage.rb +1 -1
  36. data/lib/samagotchi/broadcast_command.rb +5 -5
  37. data/lib/samagotchi/bundle_command/try/command.rb +1 -1
  38. data/lib/samagotchi/bundle_command/try/run.rb +1 -1
  39. data/lib/samagotchi/bundle_command.rb +2 -2
  40. data/lib/samagotchi/bundles/decision-card/manifest.yml +4 -4
  41. data/lib/samagotchi/bundles/decision-card/plugin.rb +1 -9
  42. data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
  43. data/lib/samagotchi/bundles/system/config_modification_protocol.md +3 -4
  44. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  45. data/lib/samagotchi/bundles/system/self_map.md +1 -1
  46. data/lib/samagotchi/child_move.rb +3 -3
  47. data/lib/samagotchi/commands/registry.rb +6 -6
  48. data/lib/samagotchi/composer_route.rb +4 -5
  49. data/lib/samagotchi/config.rb +14 -12
  50. data/lib/samagotchi/context_candidate_choice.rb +1 -1
  51. data/lib/samagotchi/context_candidates.rb +4 -3
  52. data/lib/samagotchi/continue_offer.rb +1 -1
  53. data/lib/samagotchi/debug_log.rb +1 -1
  54. data/lib/samagotchi/degrade.rb +75 -0
  55. data/lib/samagotchi/desktop/macos/ChiRunner.swift +2 -2
  56. data/lib/samagotchi/engine/cards.rb +87 -0
  57. data/lib/samagotchi/engine/context_judge.rb +138 -0
  58. data/lib/samagotchi/engine/events.rb +131 -0
  59. data/lib/samagotchi/engine/guardrails.rb +87 -0
  60. data/lib/samagotchi/engine/hooks.rb +119 -0
  61. data/lib/samagotchi/engine/messages.rb +124 -0
  62. data/lib/samagotchi/engine/model_settings.rb +127 -0
  63. data/lib/samagotchi/engine/models.rb +261 -0
  64. data/lib/samagotchi/engine/objectives.rb +116 -0
  65. data/lib/samagotchi/engine/participants.rb +86 -0
  66. data/lib/samagotchi/engine/plugins.rb +208 -0
  67. data/lib/samagotchi/engine/questions.rb +98 -0
  68. data/lib/samagotchi/engine/recap.rb +130 -0
  69. data/lib/samagotchi/engine/session.rb +131 -0
  70. data/lib/samagotchi/engine/side_requests.rb +135 -0
  71. data/lib/samagotchi/engine/steering.rb +134 -0
  72. data/lib/samagotchi/engine/toolkit.rb +269 -0
  73. data/lib/samagotchi/engine/turn_end.rb +180 -0
  74. data/lib/samagotchi/engine/turn_helpers.rb +160 -0
  75. data/lib/samagotchi/engine/verify.rb +153 -0
  76. data/lib/samagotchi/engine.rb +62 -2646
  77. data/lib/samagotchi/grants.rb +575 -0
  78. data/lib/samagotchi/guardrail_wiring.rb +2 -2
  79. data/lib/samagotchi/guardrails/context.rb +2 -3
  80. data/lib/samagotchi/guardrails/parent_approvals.rb +2 -2
  81. data/lib/samagotchi/guardrails/parent_policy.rb +2 -2
  82. data/lib/samagotchi/hook_routes.rb +3 -3
  83. data/lib/samagotchi/idle_recap.rb +4 -5
  84. data/lib/samagotchi/idle_reminders.rb +3 -4
  85. data/lib/samagotchi/iteration_limit.rb +2 -2
  86. data/lib/samagotchi/kernel_loop.rb +10 -11
  87. data/lib/samagotchi/launch_mode.rb +13 -32
  88. data/lib/samagotchi/level_setting_command.rb +3 -2
  89. data/lib/samagotchi/llm_context_command.rb +3 -2
  90. data/lib/samagotchi/log.rb +7 -7
  91. data/lib/samagotchi/log_line.rb +1 -1
  92. data/lib/samagotchi/log_path.rb +1 -1
  93. data/lib/samagotchi/note_command.rb +1 -1
  94. data/lib/samagotchi/note_delivery.rb +18 -8
  95. data/lib/samagotchi/objectives.rb +24 -3
  96. data/lib/samagotchi/objectives_check.rb +8 -27
  97. data/lib/samagotchi/objectives_command.rb +4 -69
  98. data/lib/samagotchi/objectives_edit.rb +87 -0
  99. data/lib/samagotchi/output_formatter.rb +7 -1
  100. data/lib/samagotchi/owner_lock.rb +7 -1
  101. data/lib/samagotchi/parent_answers_command.rb +5 -4
  102. data/lib/samagotchi/participant_exposure.rb +42 -0
  103. data/lib/samagotchi/participant_mode.rb +1 -1
  104. data/lib/samagotchi/participant_name.rb +1 -1
  105. data/lib/samagotchi/participants.rb +17 -17
  106. data/lib/samagotchi/participants_watch.rb +1 -1
  107. data/lib/samagotchi/pending_input_queue.rb +2 -2
  108. data/lib/samagotchi/plugin/api.rb +3 -2
  109. data/lib/samagotchi/plugin/context.rb +21 -7
  110. data/lib/samagotchi/plugin/peer.rb +2 -1
  111. data/lib/samagotchi/plugin/sessions.rb +6 -4
  112. data/lib/samagotchi/plugin_tasks.rb +1 -2
  113. data/lib/samagotchi/private_files.rb +29 -0
  114. data/lib/samagotchi/project_env.rb +2 -2
  115. data/lib/samagotchi/question_desk.rb +8 -7
  116. data/lib/samagotchi/recap_command.rb +2 -1
  117. data/lib/samagotchi/recap_setup.rb +1 -1
  118. data/lib/samagotchi/recap_store.rb +1 -1
  119. data/lib/samagotchi/redact.rb +86 -0
  120. data/lib/samagotchi/reminder_queue.rb +2 -2
  121. data/lib/samagotchi/remote_attach_command.rb +1 -1
  122. data/lib/samagotchi/reply_wait/poll.rb +2 -2
  123. data/lib/samagotchi/review_start.rb +1 -1
  124. data/lib/samagotchi/run_options.rb +6 -6
  125. data/lib/samagotchi/self_report.rb +42 -39
  126. data/lib/samagotchi/send_command/busy.rb +1 -1
  127. data/lib/samagotchi/send_command.rb +4 -1
  128. data/lib/samagotchi/session.rb +11 -2
  129. data/lib/samagotchi/session_access/contract.rb +3 -3
  130. data/lib/samagotchi/session_access/local.rb +9 -9
  131. data/lib/samagotchi/session_access/remote.rb +17 -7
  132. data/lib/samagotchi/session_access.rb +4 -4
  133. data/lib/samagotchi/session_archive_command.rb +2 -2
  134. data/lib/samagotchi/session_chain.rb +8 -6
  135. data/lib/samagotchi/session_commands.rb +45 -48
  136. data/lib/samagotchi/session_continue.rb +1 -1
  137. data/lib/samagotchi/session_delete_command.rb +4 -4
  138. data/lib/samagotchi/session_manager.rb +59 -36
  139. data/lib/samagotchi/session_metrics.rb +2 -2
  140. data/lib/samagotchi/session_model_settings.rb +3 -2
  141. data/lib/samagotchi/session_participants.rb +1 -1
  142. data/lib/samagotchi/session_retention.rb +3 -3
  143. data/lib/samagotchi/sessions_command.rb +4 -4
  144. data/lib/samagotchi/share_command.rb +10 -10
  145. data/lib/samagotchi/steer.rb +1 -1
  146. data/lib/samagotchi/system_prompt.rb +102 -109
  147. data/lib/samagotchi/task_watch.rb +1 -1
  148. data/lib/samagotchi/terminal_ui/attach_launcher.rb +72 -10
  149. data/lib/samagotchi/terminal_ui/attached_loop/composer.rb +12 -9
  150. data/lib/samagotchi/terminal_ui/attached_loop/exit_commands.rb +16 -0
  151. data/lib/samagotchi/terminal_ui/attached_loop/questions.rb +2 -3
  152. data/lib/samagotchi/terminal_ui/attached_loop.rb +17 -8
  153. data/lib/samagotchi/terminal_ui/attached_view.rb +2 -3
  154. data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -5
  155. data/lib/samagotchi/terminal_ui/formatting.rb +7 -7
  156. data/lib/samagotchi/terminal_ui/input_support.rb +1 -7
  157. data/lib/samagotchi/terminal_ui/line_reader.rb +2 -2
  158. data/lib/samagotchi/terminal_ui/live_region.rb +2 -2
  159. data/lib/samagotchi/terminal_ui/log_tail.rb +135 -0
  160. data/lib/samagotchi/terminal_ui/one_shot_sink.rb +2 -2
  161. data/lib/samagotchi/terminal_ui/plain_surface.rb +1 -1
  162. data/lib/samagotchi/terminal_ui/question_prompt.rb +4 -5
  163. data/lib/samagotchi/terminal_ui/reline_seam.rb +3 -3
  164. data/lib/samagotchi/terminal_ui/status_row.rb +7 -8
  165. data/lib/samagotchi/terminal_ui.rb +142 -1387
  166. data/lib/samagotchi/title_command.rb +2 -2
  167. data/lib/samagotchi/tool_runner.rb +14 -16
  168. data/lib/samagotchi/toolkit_command.rb +2 -1
  169. data/lib/samagotchi/tools/delegate.rb +3 -1
  170. data/lib/samagotchi/tools/delegate_cursor.rb +3 -3
  171. data/lib/samagotchi/tools/delegate_wait.rb +1 -1
  172. data/lib/samagotchi/tools/objectives_tool.rb +2 -2
  173. data/lib/samagotchi/tools/send_note.rb +2 -1
  174. data/lib/samagotchi/turn_endings.rb +2 -2
  175. data/lib/samagotchi/turn_flow.rb +4 -5
  176. data/lib/samagotchi/update_hint.rb +2 -2
  177. data/lib/samagotchi/version.rb +1 -1
  178. data/lib/samagotchi/web/app.rb +39 -50
  179. data/lib/samagotchi/web/participant_access.rb +2 -2
  180. data/lib/samagotchi/web/participant_review.rb +5 -5
  181. data/lib/samagotchi/web/participant_stream_filter.rb +8 -8
  182. data/lib/samagotchi/web/participant_view.rb +2 -82
  183. data/lib/samagotchi/web/participants_admin.rb +1 -1
  184. data/lib/samagotchi/web/public/block_host.js +35 -8
  185. data/lib/samagotchi/web/public/block_renderers.css +9 -5
  186. data/lib/samagotchi/web/public/chains.js +2 -2
  187. data/lib/samagotchi/web/public/claim_link.js +4 -4
  188. data/lib/samagotchi/web/public/composer_mode.js +1 -1
  189. data/lib/samagotchi/web/public/format.js +3 -3
  190. data/lib/samagotchi/web/push/store.rb +1 -1
  191. data/lib/samagotchi/web/session_access_routes.rb +5 -4
  192. data/lib/samagotchi/web/session_control.rb +12 -8
  193. data/lib/samagotchi/web/session_create.rb +1 -1
  194. data/lib/samagotchi/web/session_hub.rb +2 -2
  195. data/lib/samagotchi/web/session_show.rb +2 -2
  196. data/lib/samagotchi/web/session_summary.rb +1 -1
  197. data/lib/samagotchi/worker.rb +100 -9
  198. data/lib/samagotchi/worker_link.rb +3 -3
  199. data/lib/samagotchi/working_set_command.rb +3 -2
  200. metadata +29 -6
  201. data/lib/samagotchi/terminal_ui/between_turns.rb +0 -138
  202. data/lib/samagotchi/terminal_ui/repl_input.rb +0 -152
  203. data/lib/samagotchi/web/grants.rb +0 -575
  204. data/lib/samagotchi/web/participant_exposure.rb +0 -44
  205. data/lib/samagotchi/web/private_files.rb +0 -31
data/docs/guardrails.md CHANGED
@@ -23,7 +23,7 @@ The UI shows the tool line first, then the approval under it.
23
23
 
24
24
  ## Ask
25
25
 
26
- The REPL, the attached TUI and the web show what would run, where and why:
26
+ The attached TUI and the web show what would run, where and why:
27
27
 
28
28
  ```
29
29
  Approve tool call?
@@ -65,8 +65,7 @@ card comes back through `ApprovalFacts.from_h` (string or symbol keys, missing f
65
65
 
66
66
  Who answers:
67
67
 
68
- - REPL (`chi --no-shared`, with or without `-p`): at the `? ` prompt.
69
- - A shared session's worker (plain `chi` and `chi -p …` while `session.shared` is on, the default): any attached TUI or web page. With none attached,
68
+ - A session's worker (plain `chi`, `chi -p …`, the web): any attached TUI or web page. With none attached,
70
69
  the approval waits (in the session file) and shows on attach.
71
70
  - `-p … --non-interactive`: nobody; the call is denied ("No one to approve it
72
71
  (non-interactive run)").
@@ -134,7 +133,7 @@ others come back to the parent's model with the block below. A dismissal
134
133
  always passes (the hook decides what no answer means): the strategy is a
135
134
  convention for an honest parent, not a barrier. This section is
136
135
  the path for other agents, and for a chi parent that can't host the relay (a
137
- `--no-shared` REPL, `--non-interactive`): there the model gets the question
136
+ `--non-interactive` one-shot): there the model gets the question
138
137
  block and the rules below. With the relay, the same rules still hold for a
139
138
  parent model that answers its own relay card (a piped or marked `chi --attach`
140
139
  on the parent): that answer is a parent agent's, checked by the parent and
@@ -197,10 +196,10 @@ value that isn't a level reads as `hard`.
197
196
  config.yml's; `/parent-answers LEVEL` sets the session's own, `/parent-answers
198
197
  default` drops it (config.yml's applies). Anyone may tighten it, a parent too
199
198
  (`chi send ID -m "/parent-answers hard"`). Loosening it (toward `soft`, or
200
- `default`) is yours alone: the line must come from you at the REPL, an attached
199
+ `default`) is yours alone: the line must come from you at an attached
201
200
  terminal or the web composer. chi refuses it (`loosening parent_answers is the
202
201
  user's: …`) from `chi send` (a parent's or yours: the worker can't tell them
203
- apart), from a piped or marked `chi --attach` or REPL, from `chi -p
202
+ apart), from a piped or marked `chi --attach`, from `chi -p
204
203
  --non-interactive`, from a delegate's follow-up and from a plugin; `chi answer`
205
204
  has no command route. `--parent-answers` on `--attach`/`--resume` is ignored,
206
205
  never sent as the command.
@@ -277,7 +276,7 @@ no running turn, pending question or approval, queued prompt, or running
277
276
  background task; when one exists, stop it explicitly and then send a prompt.
278
277
 
279
278
  A parent may also type its answer into chi itself: a piped `chi --attach ID`
280
- or `printf '2\n' | chi --no-shared -p …`. So chi treats an answer as a
279
+ or `printf '2\n' | chi -p …`. So chi treats an answer as a
281
280
  parent's, held to the same setting, when its input isn't a terminal or an
282
281
  agent marker is set: `CLAUDECODE` (Claude Code), `AI_AGENT`, `CODEX_THREAD_ID`
283
282
  (Codex CLI), or `SAMAGOTCHI_PARENT_SESSION`,
@@ -1327,9 +1326,8 @@ allows), and how many got no judgment. `--threshold` overrides
1327
1326
  for this run; `--dry-run` judges and prints without writing a line. Without a
1328
1327
  client (`mode: off`, or no `host`) it exits 2 saying what to set.
1329
1328
 
1330
- Two limits on what the replay can see: `CardStore` keeps only the last 20
1331
- questions per session, and a `--no-shared` REPL session never writes a
1332
- `cards.json` at all, so its asks are not replayable.
1329
+ A limit on what the replay can see: `CardStore` keeps only the last 20
1330
+ questions per session, so older asks are not replayable.
1333
1331
 
1334
1332
  ## The guardrails bundle
1335
1333
 
data/docs/hooks.md CHANGED
@@ -97,14 +97,14 @@ class Watchful
97
97
  def call(event)
98
98
  case event[:type]
99
99
  when :after_generation
100
- # One line in the REPL, the attached TUI and the web ("<bundle>> text",
100
+ # One line in the attached TUI and the web ("<bundle>> text",
101
101
  # or "hook> text" for a config hook); level: :warn colours it.
102
102
  # fallback_for: :display marks a line a UI that renders the answer's
103
103
  # display may leave out (below).
104
104
  event[:notify].call("the model repeated itself", level: :warn)
105
105
  when :before_tool_call
106
- # A single-select question through the question flow (REPL, attached
107
- # TUI, web); returns {selected: [...], freeform:, selected_indices:}
106
+ # A single-select question through the question flow (the attached
107
+ # TUI, the web); returns {selected: [...], freeform:, selected_indices:}
108
108
  # or nil when there is no one to ask (--non-interactive), the
109
109
  # question was dismissed, or the options (or effects) were invalid.
110
110
  # effects: says what an option does for a parent agent's answer (see
@@ -156,7 +156,7 @@ that a UI that already shows it can leave the line out. The main value is
156
156
  links), for a UI that doesn't render that display. The hook never names a UI;
157
157
  each decides by what it renders. The web with markdown on (`web.markdown` and
158
158
  the commonmarker gem) leaves such a line out, live and after a reload; with
159
- markdown off it shows, as it does in the REPL, the attached TUI and
159
+ markdown off it shows, as it does in the attached TUI and
160
160
  `chi -p`, which print the answer as text (in the web its http(s) URLs are
161
161
  links, as in any hook's notice line). The log keeps the line with
162
162
  `fallback_for=display`. `:editor` marks a line that names a file the viewer
@@ -304,8 +304,8 @@ end
304
304
  kept), on a live turn and after a reload. Its copy button copies the
305
305
  display text. The page learns about it from an `answer_display` event
306
306
  that comes after `turn_completed` (the hooks run after the turn ended).
307
- - **Terminals** (the REPL, the attached TUI) have printed the answer by then
308
- and do not change it; use `event[:notify]` for something they should show.
307
+ - **The terminal** (the attached TUI, a `--non-interactive` one-shot) has printed the answer by then
308
+ and does not change it; use `event[:notify]` for something it should show.
309
309
 
310
310
  A plugin gets the same from `chi.on(:after_turn) { |event, ctx| event[:present].call { … } }`.
311
311
 
@@ -569,7 +569,7 @@ the voter's own (a guard that wants the model to retry a corrected call:
569
569
  `Retry with "…".`).
570
570
 
571
571
  `event[:context]` is `{cwd:, repo_root:, branch:, session_id:, interface:, origin:}`
572
- (`interface` is `:repl`, `:worker` or `:non_interactive`). `event[:targets]` is
572
+ (`interface` is `:worker` or `:non_interactive`). `event[:targets]` is
573
573
  what the call acts on, resolved as the tools resolve it:
574
574
  `{command:, paths:, cwd:, repo_root:, outside_repo:, git_dirs:}`. `outside_repo`
575
575
  is measured from the session's repo, not the call's `cwd:`; `git_dirs` lists where
@@ -855,8 +855,8 @@ qualified ref:
855
855
 
856
856
  **Where `{repo}` and `{host}` come from.** Without a named group that took
857
857
  part, they come from `git remote get-url <remote>` run in the session's
858
- working directory (the worker's; the in-process REPL's is the terminal's
859
- current directory). `remote:` picks the remote, default `origin` — a fork
858
+ working directory (the worker's; a `--non-interactive` one-shot's is the
859
+ terminal's current directory). `remote:` picks the remote, default `origin` — a fork
860
860
  sets `remote: upstream`. Git applies `insteadOf` rewrites and includes, and a
861
861
  worktree reports its main repo's remote. The URL forms understood are
862
862
  `https://`, `http://`, `ssh://`, `git://` (credentials and port dropped) and
@@ -125,11 +125,11 @@ name (or whose hint is its URL) is attached, *declined* while the declined marke
125
125
  "PR", branch names ≥ 6 chars with `/ - _`, exact URLs, fenced code blanked). Only `OwnerPrompt` texts count
126
126
  (`owner_texts`), so a participant's message, a steer, a note or a reminder never attaches anything; a review
127
127
  session skips all of it.
128
- - **Where it runs.** `before_turn` (from `Worker#run_prompt` before the absorb, and the REPL's turn start): attach
128
+ - **Where it runs.** `before_turn` (from `Worker#run_prompt` before the absorb, and a `--non-interactive` one-shot's turn start, `TerminalUI`): attach
129
129
  each proposed candidate the owner's prompts so far mention (`attach` writes a session `Source` through
130
130
  `source_for`: a URL resolves through `ContextProviders`, else the candidate's own `cmd`), then `first_fetch` them
131
131
  together within `FIRST_FETCH_SECONDS` (10) so the note lands in this turn; cut short, the fetch records nothing and
132
- the poller tries soon. `after_propose` does the same for a late proposal. The REPL has no absorber running, so
132
+ the poller tries soon. `after_propose` does the same for a late proposal. A one-shot has no absorber running, so
133
133
  `deliver_notes` runs one.
134
134
  - Forks and continues take no candidates.
135
135
 
@@ -36,8 +36,7 @@ Behavior:
36
36
  ends the wait when a person's message waits for the turn's next step (`Engine#input_waiting?`, run_turn's
37
37
  `input_waiting:` probe through the kernel's `PeerView`): in a worker an input file that the next boundary merges
38
38
  from the user, `chi send` or a delegating parent (`SessionInbox.steer_waiting?`; never a queued, image or
39
- participant's one, never a plugin's steer or a child's report, never during a participant's turn), in the REPL
40
- (`--no-shared`) a line in its steering queue. The probe only peeks: the drain still merges the message at the
39
+ participant's one, never a plugin's steer or a child's report, never during a participant's turn). The probe only peeks: the drain still merges the message at the
41
40
  next boundary. The result says `note: a message from the user is waiting; the task is STILL RUNNING.`, how to wait
42
41
  again or stop it (`TaskWait::MESSAGE_WAITING_NOTE`), and `waited: Ns`; the task keeps running and stays watched
43
42
  (below). A task that finished meanwhile gives its finished result, as after a Stop. A `chi -p` turn has no probe. The same probe ends a blocking `delegate_result` (and `delegate`'s wait) early; see [Sessions: Delegating](../sessions.md).
@@ -51,7 +50,7 @@ Behavior:
51
50
 
52
51
  A task the model leaves running (it ends its turn: "I'll check when it's finished") is reported when it ends, so the result doesn't wait for the user's next message. `session.task_wakes` (env `SAMAGOTCHI_SESSION_TASK_WAKES`): `wake` (default), `queue` or `off` (no watch, as before).
53
52
 
54
- - **The watch** (`TaskWatch`, `task_watch.rb`) is the Worker's (`WorkerTaskWakes`, `worker_task_wakes.rb`, which writes the notes; the Worker runs the wake turn), in memory: the in-process REPL (`--no-shared`) has none, and a crash loses it (those tasks behave as before). The Worker's Engine observer feeds it the raw `:tool_call_completed` events, the input `Bridge#own_task_ids` reads too. A `task_create` result and an `execute` hand-over add the task (`TaskRuntime.created_id`: a `task_id:` the command printed doesn't count), the hand-over's limit kept for the note. What shows the model the end removes it: a `task_wait` with a final status, or with `wait_result: pattern_matched` (the model chose its own "done" marker); a `task_get` with a final status; any `task_stop`. `task_list` and a `read` of `output.log` don't: a model that read the log still gets the wake once. Tasks a tool starts inside an anytime command (`/btw` and the like) reach the same observer, so they are watched too.
53
+ - **The watch** (`TaskWatch`, `task_watch.rb`) is the Worker's (`WorkerTaskWakes`, `worker_task_wakes.rb`, which writes the notes; the Worker runs the wake turn), in memory: a `--non-interactive` one-shot has none, and a crash loses it (those tasks behave as before). The Worker's Engine observer feeds it the raw `:tool_call_completed` events, the input `Bridge#own_task_ids` reads too. A `task_create` result and an `execute` hand-over add the task (`TaskRuntime.created_id`: a `task_id:` the command printed doesn't count), the hand-over's limit kept for the note. What shows the model the end removes it: a `task_wait` with a final status, or with `wait_result: pattern_matched` (the model chose its own "done" marker); a `task_get` with a final status; any `task_stop`. `task_list` and a `read` of `output.log` don't: a model that read the log still gets the wake once. Tasks a tool starts inside an anytime command (`/btw` and the like) reach the same observer, so they are watched too.
55
54
  - **The idle tick**: with something watched, each idle pass (after delegate reports, before due reminders) refreshes the watched records (`TaskRuntime.get_record`, one `ps` per running task) and takes the finished ones, keeping only the ids the conversation still holds (`TaskRuntime.created_ids_in`: a `!rollback` or a restored turn may have dropped the result that started one; an LLM context strategy can't, since its stale and forget stubs are drawn in the request, never written over the saved result). A note that can't be written puts its task back for the next pass. An empty watch costs nothing. A task that finishes during a turn and is still unseen when the turn ends wakes on the next idle pass. One the user stopped with the web's stop-task button is dropped without a note.
56
55
  - **The note** is a context note under its own header (`ContextNote::TASK_HEADER`; `TaskFinishNote`), `note_id: task-<id>`, `source: task <id8>`:
57
56
 
@@ -70,6 +69,6 @@ A task the model leaves running (it ends its turn: "I'll check when it's finishe
70
69
  - **A plain note instead** (the same text without the last two lines, no turn start), written at once, with `queue`, the budget spent (the UIs hear `N finished background tasks noted; the model reads them with your next message` once per human input, the flag shared with the other wakes), wakes paused, a continue offer waiting, input queued, an exit asked for, or a **note-only** task: one started or waited on in a turn the user stopped (reasons `user`, `manual`, `ctrl_c`), since a wake telling the model to carry on would undo that Stop. A prompt's turn takes the finished ones as plain notes first.
71
70
  - **Failure**: a wake turn that fails and kept nothing, or is stopped and restored, turns its note back into the plain one (`WorkerInbound#unmark_wake`, as for a context wake) and pauses wakes until the user writes.
72
71
  - **Idle exit**: with `wake`, a watched task holds the worker's idle exit (`WorkerIdleExit`'s `:background_tasks`) until its wake is taken: while it runs, and once it finished until the next idle pass takes it (`TaskWatch#holding`, in memory, no `ps`), so one that ends between the pass's look and the exit's check still wakes the session. A still-running one holds `/exit` too (`TaskWatch#running`, read once per request by `WorkerIdleExit#request_hold`; the 409 lists the tasks, `{id, command, age_seconds}`, with a `hint` on stopping them, and the attached terminal names them and says to use the web's stop-task button or ask for `task_stop`), as reminders do. When the only holds are ones the user can stop, the 409 carries `holds` too and the terminal asks whether to stop them all (`HoldStop`, which stops these tasks the same way `task_stop` does). A note-only task holds neither (no wake would come for it), nor does one whose note, or the look for it (the conversation's task ids unreadable), failed 3 times (`TaskWatch::MAX_FAILURES`: it is still tried at each look, but can't keep the worker up for good). With `queue` nothing holds: nothing would wake for the end, so keeping the worker up isn't worth it. As the worker leaves (any exit but a stop, a signal or an `/exit --delete`), the tasks that finished since the last look are written as plain notes; a task that ends after the worker left gets no note (the model sees its end at its next look, as before). A forgotten dev server keeps the worker up until it is stopped; a crash of that server wakes the session.
73
- - **The tool descriptions**: a worker's `execute` and `task_create` say "if you end your turn before it finishes, chi tells you when it does" (left out under `off` and in the REPL), fixed when the worker builds its Engine.
72
+ - **The tool descriptions**: a worker's `execute` and `task_create` say "if you end your turn before it finishes, chi tells you when it does" (left out under `off` and in a `--non-interactive` one-shot), fixed when the worker builds its Engine.
74
73
 
75
74
  Work that needs a model, not a shell command, goes to a child chi session instead: `delegate` / `delegate_result` have the same shape (start, then wait) and return only the child's final reply; see [Sessions: Delegating](../sessions.md#delegating).
@@ -30,6 +30,10 @@ model. The TUI is untouched: it shows a fence as source.
30
30
  clone arrives with both. On failure the `<pre>` stays, marked `data-block-failed`, with one muted line: an attribute CSS
31
31
  shows, never markup, and not in the bubble's `textContent`, which the page's dedupe and an annotation read (`copy.js`'s rule). CSS content can't be selected, so a click on the line copies its words.
32
32
  The toggle has an icon and an `aria-label`, no text (`copy.js`'s rule). A jump into the source shows it (`revealInHistory`).
33
+ For a screen reader: the hidden `<pre>` isn't read, so the `<img>`'s alt names the toggle that shows it ("mermaid
34
+ diagram, its source under the Show the source button"). The natural size, a click on the picture, is also a
35
+ `.block-zoom` button (icon, `aria-label`, `aria-pressed`) for the keyboard: before the toggle, shown on keyboard focus
36
+ only, and taking no pointer while hidden, so the mouse's view is as it was.
33
37
  6. Cache: a `Map` on `lang + theme + source`, about 200 entries, holding the `data:` URL; a later `bubble.innerHTML` wipes the
34
38
  figure and the observer redraws in the same pass. No `crypto.subtle`: it's undefined on a plain-http LAN address. A system
35
39
  theme change redraws through the figure (a hidden `<pre>` never intersects).
@@ -98,11 +98,11 @@ the "asking again" and steer-cut rows, ✂ rows and questions (with how they wer
98
98
  `chi guardrails replay` reads. A plain upgrade doesn't reach this: a worker already running seeded before and keeps its own version 1
99
99
  file, and a spawn runs the newest chi installed (`SessionManager.worker_command`), unless that activation raises LoadError: the boot
100
100
  line then falls back to the spawner's or the home's pinned version, which can be older. Otherwise it takes an older process starting
101
- on a session a newer one wrote: a downgrade, a chi home whose newest chi is older (`home_older_than_spawner`), or a REPL or `bin/chi`
101
+ on a session a newer one wrote: a downgrade, a chi home whose newest chi is older (`home_older_than_spawner`), or a `bin/chi`
102
102
  from an older gem or checkout on the same state dir. Nothing on this side stops
103
103
  it: an older chi can't be changed, and having it read the file would mean writing fake `turns` counters into every file.
104
104
  - **A killed turn needs nothing special.** `TurnCheckpoint` keeps a running turn's prompt in the session file, with its id from
105
- the start (`Worker#run_engine_turn` settles it first), so it counts as any opening. A REPL keeps no running turn in the
105
+ the start (`Worker#run_engine_turn` settles it first), so it counts as any opening. A `--non-interactive` one-shot keeps no running turn in the
106
106
  session, so a kill takes the prompt along and there's nothing to count.
107
107
 
108
108
  ## SSEWriter
@@ -401,7 +401,7 @@ for client discovery, with the Bridge's secret (see above).
401
401
  **Worker link.** Every client asks `WorkerLink` (`worker_link.rb`) whether a session's worker can be reached, never
402
402
  reading `bridge.json` or probing a port itself: `WorkerLink.resolve(session_dir, unlink:, owner:)` answers a typed
403
403
  outcome. The rule is the owner lock plus the sidecar's `pid`; there is no port probe (a port can be reused by another
404
- session's Bridge, a free lock is the kernel's proof the worker died). In order: no lock holder, or a chi REPL's: a
404
+ session's Bridge, a free lock is the kernel's proof the worker died). In order: no lock holder, or a one-shot's (kind `"tui"`): a
405
405
  sidecar is `Stale(:lock_free)` (removed with `unlink: true`, only if it is still the file that was read and the lock,
406
406
  read again just before, is still free: a worker that took it meanwhile is resolved as that worker), else `None`;
407
407
  a holder with no kind yet: `Starting`; a leaving worker: `Leaving` (marked before its Bridge stops, on every way out:
@@ -55,7 +55,7 @@ session's first counted turn. A
55
55
  hook's switch (`before_turn`) sends nothing: its turn's first request counts on the new model anyway.
56
56
 
57
57
  A process that loads the session on another model than its last turn ran on (`Engine#session=`:
58
- `SessionMetrics#last_model_ref` differs from `effective_model_ref`; a `--resume`'d REPL with `--model`, a worker woken
58
+ `SessionMetrics#last_model_ref` differs from `effective_model_ref`; a `--resume`'d one-shot with `--model`, a worker woken
59
59
  after a `/model` with no turn since) drops the old window (`forget_model_reports!`) and calls
60
60
  `announce_switched_context!(configured_only: true)`: the configured window only
61
61
  (`EffectiveModel#configured_context_window`, no `/props` GET or model list, since a worker runs it before its Bridge
@@ -1,8 +1,8 @@
1
1
  # Engine: the core agent loop
2
2
 
3
3
  The Engine (`lib/samagotchi/engine.rb`) runs a session's turns. It owns the turn lifecycle, the hooks and
4
- plugins, and the model↔tool cycle, and it reports what happens as events, so every UI (the REPL, a worker
5
- behind the web and the attached TUI, one-shot CLI runs) runs turns the same way without the Engine knowing how
4
+ plugins, and the model↔tool cycle, and it reports what happens as events, so every UI (a worker behind the web
5
+ and the attached TUI, a `--non-interactive` one-shot) runs turns the same way without the Engine knowing how
6
6
  they render.
7
7
 
8
8
  ```
@@ -120,7 +120,7 @@ StandardError in run_turn:
120
120
  # error_kind, retryable, host and summary.
121
121
  → re-raised
122
122
 
123
- Interrupt (Ctrl-C; the REPL only: a worker traps INT, see the signal row below):
123
+ Interrupt (Ctrl-C; a one-shot only: a worker traps INT, see the signal row below):
124
124
  → if turn.ended: re-raised only
125
125
  → turn.controller.cancel!(:ctrl_c)
126
126
  → end_turn(turn, "canceled"): ctrl_c_messages keeps the loop's conversation, else turn.messages
@@ -173,6 +173,12 @@ Stopped before the model was asked (prepare_turn returned false):
173
173
  → LLM::ModelResult(text: "", canceled: true, cancellation_reason: reason)
174
174
  ```
175
175
 
176
+ **Degrading.** Work that has a fallback when it fails (a prompt section left out, an empty list from the Bridge) runs
177
+ in `Degrade.call(area, event, fallback:, rescue:) { … }` (`degrade.rb`): same rescue class and fallback as a bare rescue,
178
+ plus one WARN in the debug log per process for each `[area, event]` (DEBUG after; nothing on stderr). A bare `rescue`
179
+ whose body is only a literal (`nil`, `false`, `[]`, `{}`, `""`, `0`) is flagged in lib by the `Samagotchi/SilentRescue`
180
+ cop; a silence that is right (a client gone, a routine probe, Log's own internals) carries `# degrade-ok: why`.
181
+
176
182
  ## The model↔tool loop
177
183
 
178
184
  The Engine does not talk to models directly. `Engine#backend` picks the loop the effective model's host speaks:
@@ -426,7 +432,7 @@ what a resolve gives, all without a `/model`. Frozen, so any thread may hold one
426
432
  - Never resolved (they hold on a broken resolve): `effective_model_ref`, `bare_model_name`,
427
433
  `guardrail_model_name`, `sync_model_key!`, `served_model(probe: false)` (`HostRegistry#model_ref`/`#bare_name`).
428
434
  - Readers on other threads: the Bridge (`/state`, `/stats`, the SSE reset, a steer's cut wait), the worker's
429
- command thread, the recap thread and plugin threads (`session_model_recap_target`), the REPL.
435
+ command thread, the recap thread and plugin threads (`session_model_recap_target`).
430
436
  - `ModelChoice` (`ModelBinding#choice`) holds the name and the typed name as one value, so a reader on another
431
437
  thread never pairs one model's name with another's typed name.
432
438
  - `EffectiveModel.of_session` builds a saved session's (no worker): `Web::App`, `LLMContextStrategy.for_session`.
@@ -517,7 +523,7 @@ drain hands the message to the model at a boundary, or until the turn ends (`Ste
517
523
  ### The turn's sink (`on_event`) and the observers
518
524
 
519
525
  `Engine#emit_event` (`EventFanout#emit_event`, `event_fanout.rb`, which also builds the loops' stream handler and
520
- emits the working set) sends each turn event to the turn's sink (`on_event`, the REPL's renderer) and then to the
526
+ emits the working set) sends each turn event to the turn's sink (`on_event`, a one-shot's renderer) and then to the
521
527
  persistent observers (`Engine#subscribe`), which get a copy with a monotonic `event_seq`. The observers are the
522
528
  metrics, the debug log, the thinking tails, and in a worker the Bridge, which streams to the web and the attached
523
529
  TUI. Errors in the sink or an observer are caught; they never break the turn.
@@ -777,7 +783,7 @@ The verdict is one of:
777
783
  - **allow**: the call runs.
778
784
  - **deny**: that call's output is replaced with denial text; the rest of the batch still runs.
779
785
  - **ask**: `Gate#settle_ask` first looks for a stored approval that covers the call. Otherwise
780
- `Engine#request_approval` asks the user through the question flow (REPL, attached TUI, web). A
786
+ `Engine#request_approval` asks the user through the question flow (attached TUI, web). A
781
787
  `--non-interactive` run has no one to ask, so the call is denied. An approval wider than this once is stored.
782
788
 
783
789
  docs/guardrails.md covers the rules and the approval scopes.
@@ -870,7 +876,27 @@ Paths are under `lib/samagotchi/`.
870
876
 
871
877
  | File | Role |
872
878
  |---|---|
873
- | `engine.rb` | Engine: turn lifecycle, event emission, plugins, model switching |
879
+ | `engine.rb` | Engine: the wiring (`initialize`, the `build_*` methods, the kernel), the turn skeleton (`run_turn`, `begin_turn`, `prepare_turn`'s ordered steps, `generate`, `end_turn`) and `shutdown`; the rest is in its parts, below |
880
+ | `engine/steering.rb` | `Engine::SteeringPart`: steering the running turn (or the next), the drains, the cut, the activity clock, the cancel |
881
+ | `engine/side_requests.rb` | `Engine::SideRequestsPart`: ctx.ask_model's and Check this's side requests, their side usage, System 1's recording |
882
+ | `engine/recap.rb` | `Engine::RecapPart`: the idle recap job and `/recap`, the idle scheduler's start and stop |
883
+ | `engine/models.rb` | `Engine::ModelsPart`: the model's names, identity and notes, `/model`'s switch, the profile, the loop it runs on |
884
+ | `engine/model_settings.rb` | `Engine::ModelSettingsPart`: the session's model settings (SessionModelSettings) and the snapshots (EngineSnapshots), forwarded |
885
+ | `engine/events.rb` | `Engine::EventsPart`: observers, `#announce`, the event lock, held announcements, load events, the emit helpers |
886
+ | `engine/cards.rb` | `Engine::CardsPart`: `#show_card`, anytime commands and their threads |
887
+ | `engine/objectives.rb` | `Engine::ObjectivesPart`: the objectives' view, proposals and audit |
888
+ | `engine/messages.rb` | `Engine::MessagesPart`: the session messages API (out-of-turn edits, checkpoints, the live conversation) |
889
+ | `engine/plugins.rb` | `Engine::PluginsPart`: loading plugins, init tasks and staged tools, `Plugin::Host` |
890
+ | `engine/hooks.rb` | `Engine::HooksPart`: turn-scoped hooks, the hook runtime (HookRoutes, forwarded), reminders |
891
+ | `engine/guardrails.rb` | `Engine::GuardrailsPart`: the guardrails' readers and the first turn's load warnings |
892
+ | `engine/participants.rb` | `Engine::ParticipantsPart`: the turn's author, a pair turn's stop, the participants line |
893
+ | `engine/session.rb` | `Engine::SessionPart`: `#session=`, the state dir, the relay's view, the system prompt and its memory names |
894
+ | `engine/verify.rb` | `Engine::VerifyPart`: Verify and Revise anchors at a turn's end, the verdict |
895
+ | `engine/questions.rb` | `Engine::QuestionsPart`: ask-user-question (QuestionDesk, forwarded) |
896
+ | `engine/toolkit.rb` | `Engine::ToolkitPart`: the toolkit, `ctx.load_tools`, System 1's toolkit suggestion |
897
+ | `engine/context_judge.rb` | `Engine::ContextJudgePart`: the attached-context candidate judge and its cards |
898
+ | `engine/turn_helpers.rb` | `Engine::TurnHelpersPart`: a turn's settings, images, model ref, used memories, the turn-end warm-up |
899
+ | `engine/turn_end.rb` | `Engine::TurnEndPart`: the normal ending and after-turn hooks, release, what an ending keeps, last_turn, the answer's display |
874
900
  | `event_fanout.rb` | `EventFanout`: how an event reaches the turn's sink and the observers (`emit_event`), the loops' stream handler, the working set's emit and dedupe; the Engine forwards |
875
901
  | `engine_snapshots.rb` | `EngineSnapshots`: the UIs' read model, `session_state_snapshot` (cheap, never raises) and `stats_snapshot` (may probe `/props`); the Engine forwards |
876
902
  | `model_binding.rb` | `ModelBinding`: the model choice and the prompt profile resolved for it, under one mutex, and the overlay keys; the Engine forwards |
@@ -71,8 +71,8 @@ the payload, so an edit and a continue carry none: a new link starts with no las
71
71
  counts. A leading `[done]`, `[dropped]` or `[active]` is stripped, so a text can't pass for a status. Any whitespace
72
72
  collapses, Unicode line separators too, so each item stays one line of the frame.
73
73
  - It never writes session.json and runs on any thread: it only adds a file to `notes/`.
74
- - It is refused in a session a chi REPL owns (`--no-shared`), with the reason `chi note` gives
75
- (`NoteDelivery::REPL_REFUSAL`). A show still answers there.
74
+ - It is refused while a `--non-interactive` one-shot has the session, with the reason `chi note` gives
75
+ (`NoteDelivery::ONE_SHOT_REFUSAL`, in `OwnerLock::TUI_BUSY`'s words).
76
76
  - A second edit stacks on an unabsorbed first, since the current list reads the pending file.
77
77
  - `WorkerInbound#absorb_notes` adds only the newest list in a batch and deletes the others, so edits made during a
78
78
  turn cost one entry. It runs between turns: an idle edit lands at the worker's next pass (the fallback tick, or just
@@ -115,7 +115,8 @@ Bridge's `TurnAccumulator#current_messages`) leaves tool calls out. A move shows
115
115
  ## Proposals
116
116
 
117
117
  `action: propose` (`change` add, reword, done or drop; `id`; `text`; `note`, why) checks the change against the list
118
- (`Objectives::Proposal#refusal`) and hands it to `Engine#propose_objective`, which keeps it until the turn is over,
118
+ (`Objectives::Proposal#refusal`: its line through `Objectives::Edit`, the edit the card's Apply runs, so the tool refuses
119
+ what Apply would) and hands it to `Engine#propose_objective`, which keeps it until the turn is over,
119
120
  as `#context_choice`'s cards are kept. `run_turn`'s ensure shows them only for a turn that completed: a canceled or
120
121
  failed one drops them (`Engine#discard_objective_proposals`), so its Apply card never outlives a turn whose call a
121
122
  rollback has taken out of the history, and nothing carries over to the next turn. A card is between turns (no
@@ -226,7 +227,7 @@ notes said, what tools returned, and the agent's visible answers labelled `[agen
226
227
  newest 24,000 kept). Without those answers a claim about what the agent said or asked could only come back `unclear`. So the request says they settle
227
228
  what was said or asked and are never proof of work, which needs a tool's output or a person's word. That rule is words to a model, not a mechanism: a
228
229
  probe on Qwen3.8 Flash Next held it 3 of 3 (only the agent's "all pass": `unclear`), and a verdict is information only either way.
229
- The record never holds the agent's thinking (`own_words`: a chat entry's `thinking` and `anthropic_content` aren't read; a native
230
+ The record never holds the agent's thinking (`AnswerText.visible`, the rule the continue note's last answer and decision-card's `/short` share: a chat entry's `thinking` and `anthropic_content` aren't read; a native
230
231
  entry's content is the raw response, so its inline thoughts, a thought cut off unclosed, and its call markup come out), the system prompt, the list's
231
232
  notes, a continue's carry note, an earlier session's answer that would read as outside news
232
233
  (`Objectives.outside_note?`, the rule the audit shares), or the objectives tool's calls, since a move isn't evidence for itself.
@@ -237,7 +238,7 @@ stays inside; the request says that what's inside is data and its instructions a
237
238
  the frame to hold; a model may still be swayed by text inside it, which is why a verdict is information only.
238
239
 
239
240
  **Its host.** The model is the session's, or `objectives.check_model`, whose host may be another one. The whole user text goes through
240
- `Web::ParticipantView::Redact.text`, chi's participant redaction: known key shapes and `token=`-style values become `[redacted]`, and
241
+ `Samagotchi::Redact.text`, chi's participant redaction: known key shapes and `token=`-style values become `[redacted]`, and
241
242
  the home folder `~`. That is by pattern, so a secret of another shape still goes.
242
243
 
243
244
  **State.** `objective_checks.jsonl` beside the session: a `running` line, then one with `verdict` (confirmed, refuted, unclear,
@@ -5,8 +5,8 @@ chi web with a link of their own. This page is the identity layer: grants, the g
5
5
  reaches, and what is written where; and their **turns**: review turns queued, read-only, limited and capped, pair
6
6
  turns that act under the owner's guardrails; and what they **see**: their page and its reads, all through one
7
7
  filter. Participants are experimental. An agent participant waits on its own turns' replies through the API. The
8
- code and docs say "participants", "review" and "pair", never "shared": `session.shared` already means "attached by
9
- default".
8
+ code and docs say "participants", "review" and "pair", never "shared": that word means several UIs
9
+ on one worker.
10
10
 
11
11
  Participants reach chi web on the **LAN** (`web.host: lan`), or **behind an auth proxy** with `web.public_url`
12
12
  (experimental; docs/configuration.md, "Behind an auth proxy"): there every request needs a token, this machine's too,
@@ -18,7 +18,7 @@ works on this machine only) is the participant, never the owner.
18
18
 
19
19
  ## Grants
20
20
 
21
- A grant (`Web::Grants`, `lib/samagotchi/web/grants.rb`) binds **one random token** to **one session**, **one
21
+ A grant (`Grants`, `lib/samagotchi/grants.rb`) binds **one random token** to **one session**, **one
22
22
  participant** and **one role**, until it expires or is revoked.
23
23
 
24
24
  - **Token.** `chp_` + 43 URL-safe characters (32 random bytes, as the owner's `Web::Token`). The prefix lets the gate
@@ -57,7 +57,7 @@ participant** and **one role**, until it expires or is revoked.
57
57
  an invite can't keep only its own grant and lose every other digest. The owner's invite, cap, revoke and list are
58
58
  refused with one message naming the file (`Grants::Corrupt`: `chi share` exits 1, chi web answers 500 with it);
59
59
  an archive or delete still goes ahead. Fix the file, or move it aside to end every link.
60
- - **Exposure** (`exposure`, `Web::ParticipantExposure`; the review-composer plan's D3): what the participant learns
60
+ - **Exposure** (`exposure`, `ParticipantExposure`; the review-composer plan's D3): what the participant learns
61
61
  of the review's usage, their review data's `viewer.usage` ([below](#what-a-participant-sees)). One level today,
62
62
  `own`, every grant's: their own spend and cap, their own queued turns and queue limit, their link's expiry, and the
63
63
  session's model; never another participant's or the owner's usage, nor the session's. `ParticipantExposure::LEVELS`
@@ -469,7 +469,7 @@ fields: a field added to an event later reaches nobody until it is named there.
469
469
  its final assistant reply (`AnswerTail`: the turn's last assistant message with text) contains the quote. Matching is case-sensitive, collapses whitespace, and strips common
470
470
  Markdown presentation (links, headings, list/blockquote prefixes, emphasis and code delimiters) from both sides;
471
471
  this matches the rendered text the page selects without treating different casing as the same claim. The quote may
472
- also be in the answer as the pages show it (`Engine#verify_quote_in?`, `ParticipantView::Redact.text`: the session's
472
+ also be in the answer as the pages show it (`Engine#verify_quote_in?`, `Redact.text`: the session's
473
473
  folder relative, home `~`, secret shapes `[redacted]`): a participant's page has only that text, and the owner's
474
474
  page quotes it the same way (`owner_claim.js` `opts()`, `normalizeClaim`; `spec/fixtures/redact_cases.json` keeps the two
475
475
  sides alike), so a claim naming a path is taken on both. A fresh child
@@ -555,7 +555,7 @@ fields: a field added to an event later reaches nobody until it is named there.
555
555
  paste, drop or pick with one line (the image button is hidden) until participant images land. The grip's floor and
556
556
  the fold are the owner page's keys (`chi_composer_floor`, `chi_composer_collapsed`), per browser. The owner's
557
557
  participant view mounts none: its dock stays hidden.
558
- - **Redaction** (`ParticipantView::Redact`, on every string that goes out): secret shapes (`sk-…`, `ghp_…`,
558
+ - **Redaction** (`Samagotchi::Redact`, `lib/samagotchi/redact.rb`, on every string that goes out): secret shapes (`sk-…`, `ghp_…`,
559
559
  `github_pat_…`, `AKIA…`, `xox…-`, `chp_…`, `AIza…`, `rk_live_…`, JWTs, Slack webhooks, private key blocks, an
560
560
  `Authorization:` header's value, any `…token` / `…api_key` / `…secret` / `…password` value, `=` or `:`, quoted or
561
561
  not, and a URL's password), the owner's web token, the session's folder (shown as relative paths) and the home
@@ -749,7 +749,7 @@ participant TUI in v1.
749
749
  participant; the status row counts who is in.
750
750
  - **Commands.** `/share`, `/participants` (`TerminalUI::ParticipantCommands`) run `ShareCommand` for the attached
751
751
  session through the link's `SessionAccess` (`AttachLink#session_access`: `Local`, or the remote's), so their words
752
- and refusals are `chi share`'s, led by the command as typed. The REPL answers them with a note.
752
+ and refusals are `chi share`'s, led by the command as typed.
753
753
 
754
754
  ## Invariants
755
755
 
@@ -120,7 +120,7 @@ right-hand column happens.
120
120
  groups changes the prompt key and asks `/toolkit` to confirm the cache cost once the session has history.
121
121
  - A worker's `execute` and `task_create` descriptions carry the finish wake's sentence ("if you end your turn before
122
122
  it finishes, chi tells you when it does"; `Engine.new(task_finish_notes:)`, set once as the worker builds its
123
- Engine); the `--no-shared` REPL's, and a worker's under `session.task_wakes: off`, don't. A session that moves
123
+ Engine); a `--non-interactive` one-shot's, and a worker's under `session.task_wakes: off`, don't. A session that moves
124
124
  between the two, or whose worker restarts after `task_wakes` changed, misses the cache once.
125
125
  - On remote Claude, `LLM::PromptCache.mark` sends the system message as two text parts. The stable part carries
126
126
  breakpoint #1, so a **new session** reads it from the cache. The last non-system message carries breakpoint
@@ -69,9 +69,9 @@ command, or a flag of a supported command is missing from both lists, or when th
69
69
  exist. **A new session command or flag fails that spec until you choose one.**
70
70
 
71
71
  `Contract::RUN` does the same for bin/chi's run flags (`RunOptions.switches`): `SUPPORTED` (`--attach`, `--resume`,
72
- `-p`, `--shared`, `--model`, `--thinking`, `--llm-context*`, `--toolkit`, `--tool`, `--no-tool`, `--set`,
72
+ `-p`, `--model`, `--thinking`, `--llm-context*`, `--toolkit`, `--tool`, `--no-tool`, `--set`,
73
73
  `--parent-answers`, `--no-interrupt`, `--no-default-input`, `--dir`, `--help`, `--version`) or `LOCAL_ONLY`
74
- (`--no-shared`, `--non-interactive` and `--verbose`: the in-process REPL, P4-D4; `--memory`, `--mute`,
74
+ (`--non-interactive`: one turn in this process, P4-D4; `--verbose`: this machine's debug log; `--memory`, `--mute`,
75
75
  `--append-system-prompt`: this machine's memories and files, P4-D2/D3; `chi web`'s flags; `--profile`). A config
76
76
  registry flag (`--recap-base-url` …) is this machine's config: `LOCAL_ONLY` without being listed
77
77
  (`Contract.run_status`). The contract spec fails on a run flag in neither.
@@ -152,7 +152,7 @@ the session commands*. Keep that table in step with `SessionAccessRoutes`.
152
152
  - **A remote attach.** Before the loop runs (resolve, create, wake, the feature check) every failure is
153
153
  `AttachLauncher::Error` or the command's own line, exit 1 (`BadRequest`: exit 2): the wake's 409 `owned_by_tui`
154
154
  is `OwnedByTUI` (its message is the one `chi --attach` prints here too: `AttachLauncher.connect` now uses it), its
155
- 409 `scratch` and 504 `worker_timeout` are `Refused` with `LaunchMode::SCRATCH_LEFTOVER` / `BRIDGE_LATE`'s words
155
+ 409 `scratch` and 504 `worker_timeout` are `Refused` with `Session::SCRATCH_REFUSED` / `BRIDGE_LATE`'s words
156
156
  (`Remote#attach_wake`). Once the loop runs, `WebBridgeClient` turns every `RemoteFailed` into `Unreachable`
157
157
  (an `IOError`), so the loop's rescues catch it and `AttachedLoop#worker_down` prints its message as is ("could not
158
158
  send the prompt (remote studio (http://…): the token was refused …)"); the stream's end is worded by its reason
@@ -190,7 +190,7 @@ with a "Machine-local" comment; the rest branch on `@access.remote?` (`SendComma
190
190
  | `context`'s default session (`SAMAGOTCHI_PARENT_SESSION`, `ContextCommand#inside_session`) | client | no default: ids are required (`added_by` still reads it) |
191
191
  | A context URL's provider (`ContextProviders.resolve`) | client's bundles | server's bundles (`Remote#context_add_url`) |
192
192
  | UI-only slash commands (`SessionCommands.ui_only_entry`), `--image` ingest, note text (`SessionInbox.checked_text`), push text (`ContextSources.parse_output`) | client | client too (the server checks note and push text again) |
193
- | A remote attach's `--attach`/`--resume` id (`Session.resolve_id`), the leftover scratch check, the "open in a chi REPL" refusal | client (bin/chi, `AttachLauncher.connect`) | server: `Remote#resolve`, then `POST …/wake`'s 409s |
193
+ | A remote attach's `--attach`/`--resume` id (`Session.resolve_id`), the leftover scratch check, the one-turn run's refusal (`OwnerLock::TUI_BUSY`) | client (bin/chi, `AttachLauncher.connect`) | server: `Remote#resolve`, then `POST …/wake`'s 409s |
194
194
  | A new attached session's `--toolkit`, `--set`, `--thinking`, `--llm-context*`, `--parent-answers`, `--model` | client (bin/chi) | server: `POST /api/sessions` with `idle: true` and `SetupWords`; its warnings printed as `send --new` prints them |
195
195
  | The status row's default model, the newest chi for the version line, what a parent may pick (exit 3) | client | server: the wake's `default_model`, `installed`, `parent_strategy` (the last read again from `GET …/worker` at report time) |
196
196
  | `@path` images (ingest, convert, downscale) | client, into the session's `images/` | client, into a scratch dir, then uploaded (`POST …/images`) |
@@ -213,7 +213,7 @@ with a "Machine-local" comment; the rest branch on `@access.remote?` (`SendComma
213
213
  (`App#principal(req)`), so a handler reads the principal and never trusts the body for it. Each `ROUTES` row names
214
214
  who may use it (its permission column, `:owner` or `:participant`; `Principal#permits?`; any other answers 403
215
215
  `not_permitted`). The owner passes every route (`local_viewer?` only hides `editor_url`). A participant, a
216
- `Web::Grants` token for one session, reaches only the `:participant` rows, and another session's id is a 404
216
+ `Grants` token for one session, reaches only the `:participant` rows, and another session's id is a 404
217
217
  ([participants.md](participants.md)). A turn's author comes from the principal
218
218
  (`Principal#author`, nil for the owner) and rides with the turn (`SessionManager.deliver_turn(author:)` →
219
219
  `BridgeClient#post_turn` → `Bridge#handle_post_turn` → the input file, the fallback's too → the turn's `origin`);
@@ -297,7 +297,7 @@ bin/chi ── RunOptions.parse ── a remote, run flags ──> RemoteAttachC
297
297
  **chi web's routes** (tabled in [architecture.md](../architecture.md)): `POST …/wake` (`Local#wake`: the live worker
298
298
  as is, else `SessionManager.resume_session` and `WorkerLink.await`, at most `WAKE_MAX_WAIT` 10 s, the attach's
299
299
  `BRIDGE_WAIT` here, after waiting for a worker still leaving to let go of its lock, `OwnerLock.await_release`: its
300
- recap on leave's timeout plus 5 s; a leftover scratch session is refused in `LaunchMode::SCRATCH_LEFTOVER`'s words, a late Bridge
300
+ recap on leave's timeout plus 5 s; a leftover scratch session is refused in `Session::SCRATCH_REFUSED`'s words, a late Bridge
301
301
  in `LaunchMode::BRIDGE_LATE`'s; a worker on a newer sidecar protocol is refused, 409 `worker_newer`, never resumed over) and `GET …/worker` (`Local#worker`: live, the stop marker, the owner, `leaving` and `leave_seconds` when its worker
302
302
  is leaving, `WorkerState#leaving?`, `stuck` for a worker that never published its Bridge, `skew` for a newer protocol; it never starts one; it also names that machine's `default_model`), both answering the worker as `{epoch, started_at, version,
303
303
  features}`; `POST …/exit` (the worker's exit, `client_id` passed on only when it is a `tui:` one), `POST …/recap`,
@@ -436,7 +436,7 @@ the closing line reads `stopped` as `unreachable`, the words it always had.
436
436
  writes the stream's GET (host, port, path, extra headers) and reads its headers, for a stream through chi web too.
437
437
  - `RunOptions.parse(argv)` is bin/chi's one run-flag parser (`RunOptions.switches` lists its flags), so a remote
438
438
  branch can take the parsed options before bin/chi's local checks; `LaunchMode.attached_conflict` and
439
- `LaunchMode::SCRATCH_LEFTOVER` / `BRIDGE_LATE` are the words both would print.
439
+ `Session::SCRATCH_REFUSED` / `BRIDGE_LATE` are the words both would print.
440
440
 
441
441
  An https chi web (a pod behind a proxy) needs TLS and chunked decoding in `StreamRequest`.
442
442
  - **Shared sessions (guests, roles).** The only access point is `App#token_gate`: one token, the owner's. Roles would
@@ -75,7 +75,7 @@ dropped that way, and an unknown id is ignored.
75
75
  That is how a subscription iOS revoked silently (Apple keeps answering 201, so `:gone` never comes) leaves the store:
76
76
  the user turns the bell off and on, the page has no live subscription to delete, and the new one replaces it. Every write is a read-change-write under
77
77
  `store.lock` (chi web's request threads and the sender thread both write). A `subscriptions.json` that doesn't read
78
- lists no device and is never written over. `Store#check_permissions!` uses `Web::PrivateFiles.check!`, the check the
78
+ lists no device and is never written over. `Store#check_permissions!` uses `PrivateFiles.check!`, the check the
79
79
  grants use, worded with what the folder holds; `Server.launch` runs it (`Push.preflight!`, with the contact and the
80
80
  key) before any tunnel goes up, and a refusal is one `Error:` line, exit 1.
81
81
 
data/docs/memory.md CHANGED
@@ -33,8 +33,8 @@ dir has a folder of the same basename with another hash, chi says so, naming bot
33
33
  `mv` that fixes it (a name match can't prove it's the same project, so nothing is renamed for you):
34
34
 
35
35
  - at session start, a `memory>` load warning (a `:guardrail_warning` with label `memory`; the attached
36
- TUI, the REPL, `--non-interactive` and the web show it; a UI that attaches later gets it from the
37
- worker's cards), once per worker or REPL process, so a respawn or resume says it again;
36
+ TUI, `--non-interactive` and the web show it; a UI that attaches later gets it from the
37
+ worker's cards), once per worker or `--non-interactive` process, so a respawn or resume says it again;
38
38
  - under a `memory_read` miss that looked in project scope (`scope: "project"` or none), and under the
39
39
  project index read, on a `Hint:` line after `Error: memory not found: …`. A system-scope read never
40
40
  gets it.