samagotchi 0.152.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 (229) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -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/bundle-authoring.md +1 -1
  8. data/docs/bundles.md +93 -12
  9. data/docs/cli.md +83 -99
  10. data/docs/configuration.md +46 -40
  11. data/docs/context.md +3 -3
  12. data/docs/desktop.md +1 -1
  13. data/docs/guardrails.md +15 -15
  14. data/docs/hooks.md +13 -12
  15. data/docs/internals/anthropic.md +3 -3
  16. data/docs/internals/attached-context.md +5 -2
  17. data/docs/internals/background-tasks.md +3 -4
  18. data/docs/internals/block-renderers.md +4 -0
  19. data/docs/internals/bridge.md +3 -3
  20. data/docs/internals/context-telemetry.md +1 -1
  21. data/docs/internals/engine.md +42 -12
  22. data/docs/internals/objectives.md +6 -5
  23. data/docs/internals/participants.md +8 -8
  24. data/docs/internals/prompt-caching.md +8 -4
  25. data/docs/internals/remote-sessions.md +7 -7
  26. data/docs/internals/web-frontend.md +20 -17
  27. data/docs/internals/web-push.md +1 -1
  28. data/docs/memory.md +2 -2
  29. data/docs/plugins.md +52 -43
  30. data/docs/releasing.md +1 -1
  31. data/docs/sessions.md +25 -25
  32. data/docs/sub-agent.md +2 -2
  33. data/docs/system-one.md +1 -1
  34. data/docs/testing.md +18 -8
  35. data/lib/samagotchi/answer_text.rb +34 -0
  36. data/lib/samagotchi/bridge.rb +43 -32
  37. data/lib/samagotchi/bridge_client.rb +10 -9
  38. data/lib/samagotchi/broadcast/recipients.rb +2 -2
  39. data/lib/samagotchi/broadcast/triage.rb +1 -1
  40. data/lib/samagotchi/broadcast_command.rb +5 -5
  41. data/lib/samagotchi/bundle_command/try/command.rb +1 -1
  42. data/lib/samagotchi/bundle_command/try/run.rb +1 -1
  43. data/lib/samagotchi/bundle_command.rb +2 -2
  44. data/lib/samagotchi/bundles/decision-card/manifest.yml +4 -4
  45. data/lib/samagotchi/bundles/decision-card/plugin.rb +1 -9
  46. data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
  47. data/lib/samagotchi/bundles/system/config_modification_protocol.md +3 -4
  48. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  49. data/lib/samagotchi/bundles/system/self_map.md +1 -1
  50. data/lib/samagotchi/bundles/web-search/manifest.yml +13 -0
  51. data/lib/samagotchi/bundles/web-search/plugin.rb +174 -0
  52. data/lib/samagotchi/child_move.rb +3 -3
  53. data/lib/samagotchi/commands/registry.rb +6 -6
  54. data/lib/samagotchi/composer_route.rb +4 -5
  55. data/lib/samagotchi/config.rb +14 -12
  56. data/lib/samagotchi/context_candidate_choice.rb +1 -1
  57. data/lib/samagotchi/context_candidates.rb +4 -3
  58. data/lib/samagotchi/continue_offer.rb +1 -1
  59. data/lib/samagotchi/debug_log.rb +1 -1
  60. data/lib/samagotchi/degrade.rb +75 -0
  61. data/lib/samagotchi/desktop/macos/ChiRunner.swift +2 -2
  62. data/lib/samagotchi/engine/cards.rb +87 -0
  63. data/lib/samagotchi/engine/context_judge.rb +138 -0
  64. data/lib/samagotchi/engine/events.rb +131 -0
  65. data/lib/samagotchi/engine/guardrails.rb +87 -0
  66. data/lib/samagotchi/engine/hooks.rb +119 -0
  67. data/lib/samagotchi/engine/messages.rb +124 -0
  68. data/lib/samagotchi/engine/model_settings.rb +127 -0
  69. data/lib/samagotchi/engine/models.rb +261 -0
  70. data/lib/samagotchi/engine/objectives.rb +116 -0
  71. data/lib/samagotchi/engine/participants.rb +86 -0
  72. data/lib/samagotchi/engine/plugins.rb +208 -0
  73. data/lib/samagotchi/engine/questions.rb +98 -0
  74. data/lib/samagotchi/engine/recap.rb +130 -0
  75. data/lib/samagotchi/engine/session.rb +131 -0
  76. data/lib/samagotchi/engine/side_requests.rb +135 -0
  77. data/lib/samagotchi/engine/steering.rb +134 -0
  78. data/lib/samagotchi/engine/toolkit.rb +269 -0
  79. data/lib/samagotchi/engine/turn_end.rb +180 -0
  80. data/lib/samagotchi/engine/turn_helpers.rb +160 -0
  81. data/lib/samagotchi/engine/verify.rb +153 -0
  82. data/lib/samagotchi/engine.rb +63 -2642
  83. data/lib/samagotchi/grants.rb +575 -0
  84. data/lib/samagotchi/guardrail_wiring.rb +2 -2
  85. data/lib/samagotchi/guardrails/context.rb +2 -3
  86. data/lib/samagotchi/guardrails/parent_approvals.rb +2 -2
  87. data/lib/samagotchi/guardrails/parent_policy.rb +2 -2
  88. data/lib/samagotchi/guardrails/review_boundary.rb +4 -3
  89. data/lib/samagotchi/hook_routes.rb +3 -3
  90. data/lib/samagotchi/idle_recap.rb +4 -5
  91. data/lib/samagotchi/idle_reminders.rb +3 -4
  92. data/lib/samagotchi/iteration_limit.rb +2 -2
  93. data/lib/samagotchi/kernel_loop.rb +10 -11
  94. data/lib/samagotchi/launch_mode.rb +13 -32
  95. data/lib/samagotchi/level_setting_command.rb +3 -2
  96. data/lib/samagotchi/llm_context_command.rb +3 -2
  97. data/lib/samagotchi/llm_context_stale.rb +2 -2
  98. data/lib/samagotchi/log.rb +7 -7
  99. data/lib/samagotchi/log_line.rb +1 -1
  100. data/lib/samagotchi/log_path.rb +1 -1
  101. data/lib/samagotchi/message_parts.rb +3 -2
  102. data/lib/samagotchi/note_command.rb +1 -1
  103. data/lib/samagotchi/note_delivery.rb +18 -8
  104. data/lib/samagotchi/objectives.rb +24 -3
  105. data/lib/samagotchi/objectives_check.rb +8 -27
  106. data/lib/samagotchi/objectives_command.rb +4 -69
  107. data/lib/samagotchi/objectives_edit.rb +87 -0
  108. data/lib/samagotchi/output_formatter.rb +7 -1
  109. data/lib/samagotchi/owner_lock.rb +7 -1
  110. data/lib/samagotchi/parent_answers_command.rb +5 -4
  111. data/lib/samagotchi/participant_exposure.rb +42 -0
  112. data/lib/samagotchi/participant_mode.rb +1 -1
  113. data/lib/samagotchi/participant_name.rb +1 -1
  114. data/lib/samagotchi/participants.rb +17 -17
  115. data/lib/samagotchi/participants_watch.rb +1 -1
  116. data/lib/samagotchi/pending_input_queue.rb +2 -2
  117. data/lib/samagotchi/plugin/api.rb +3 -2
  118. data/lib/samagotchi/plugin/context.rb +21 -7
  119. data/lib/samagotchi/plugin/peer.rb +2 -1
  120. data/lib/samagotchi/plugin/sessions.rb +6 -4
  121. data/lib/samagotchi/plugin_tasks.rb +1 -2
  122. data/lib/samagotchi/private_files.rb +29 -0
  123. data/lib/samagotchi/project_env.rb +2 -2
  124. data/lib/samagotchi/question_desk.rb +8 -7
  125. data/lib/samagotchi/recap_command.rb +2 -1
  126. data/lib/samagotchi/recap_setup.rb +1 -1
  127. data/lib/samagotchi/recap_store.rb +1 -1
  128. data/lib/samagotchi/redact.rb +86 -0
  129. data/lib/samagotchi/reminder_queue.rb +2 -2
  130. data/lib/samagotchi/remote_attach_command.rb +1 -1
  131. data/lib/samagotchi/reply_wait/poll.rb +2 -2
  132. data/lib/samagotchi/review_start.rb +1 -1
  133. data/lib/samagotchi/run_options.rb +6 -6
  134. data/lib/samagotchi/self_report.rb +42 -39
  135. data/lib/samagotchi/send_command/busy.rb +1 -1
  136. data/lib/samagotchi/send_command.rb +4 -1
  137. data/lib/samagotchi/session.rb +11 -2
  138. data/lib/samagotchi/session_access/contract.rb +3 -3
  139. data/lib/samagotchi/session_access/local.rb +9 -9
  140. data/lib/samagotchi/session_access/remote.rb +17 -7
  141. data/lib/samagotchi/session_access.rb +4 -4
  142. data/lib/samagotchi/session_archive_command.rb +2 -2
  143. data/lib/samagotchi/session_chain.rb +8 -6
  144. data/lib/samagotchi/session_commands.rb +45 -48
  145. data/lib/samagotchi/session_continue.rb +1 -1
  146. data/lib/samagotchi/session_delete_command.rb +4 -4
  147. data/lib/samagotchi/session_manager.rb +59 -36
  148. data/lib/samagotchi/session_metrics.rb +2 -2
  149. data/lib/samagotchi/session_model_settings.rb +3 -2
  150. data/lib/samagotchi/session_participants.rb +1 -1
  151. data/lib/samagotchi/session_retention.rb +3 -3
  152. data/lib/samagotchi/sessions_command.rb +4 -4
  153. data/lib/samagotchi/share_command.rb +10 -10
  154. data/lib/samagotchi/steer.rb +1 -1
  155. data/lib/samagotchi/system_prompt.rb +102 -109
  156. data/lib/samagotchi/task_watch.rb +1 -1
  157. data/lib/samagotchi/terminal_ui/attach_launcher.rb +72 -10
  158. data/lib/samagotchi/terminal_ui/attached_loop/composer.rb +12 -9
  159. data/lib/samagotchi/terminal_ui/attached_loop/exit_commands.rb +16 -0
  160. data/lib/samagotchi/terminal_ui/attached_loop/questions.rb +2 -3
  161. data/lib/samagotchi/terminal_ui/attached_loop.rb +17 -8
  162. data/lib/samagotchi/terminal_ui/attached_view.rb +2 -3
  163. data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -5
  164. data/lib/samagotchi/terminal_ui/formatting.rb +7 -7
  165. data/lib/samagotchi/terminal_ui/input_support.rb +1 -7
  166. data/lib/samagotchi/terminal_ui/line_reader.rb +2 -2
  167. data/lib/samagotchi/terminal_ui/live_region.rb +2 -2
  168. data/lib/samagotchi/terminal_ui/log_tail.rb +135 -0
  169. data/lib/samagotchi/terminal_ui/one_shot_sink.rb +2 -2
  170. data/lib/samagotchi/terminal_ui/plain_surface.rb +1 -1
  171. data/lib/samagotchi/terminal_ui/question_prompt.rb +4 -5
  172. data/lib/samagotchi/terminal_ui/reline_seam.rb +3 -3
  173. data/lib/samagotchi/terminal_ui/status_row.rb +7 -8
  174. data/lib/samagotchi/terminal_ui.rb +142 -1387
  175. data/lib/samagotchi/title_command.rb +2 -2
  176. data/lib/samagotchi/tool_runner.rb +14 -16
  177. data/lib/samagotchi/toolkit.rb +8 -5
  178. data/lib/samagotchi/toolkit_binding.rb +30 -5
  179. data/lib/samagotchi/toolkit_command.rb +2 -1
  180. data/lib/samagotchi/toolkit_report_command.rb +9 -5
  181. data/lib/samagotchi/toolkit_suggest.rb +12 -6
  182. data/lib/samagotchi/tools/builtin_calls.rb +14 -10
  183. data/lib/samagotchi/tools/deferred_tools.rb +2 -2
  184. data/lib/samagotchi/tools/delegate.rb +3 -1
  185. data/lib/samagotchi/tools/delegate_cursor.rb +3 -3
  186. data/lib/samagotchi/tools/delegate_wait.rb +1 -1
  187. data/lib/samagotchi/tools/objectives_tool.rb +2 -2
  188. data/lib/samagotchi/tools/registry.rb +44 -20
  189. data/lib/samagotchi/tools/send_note.rb +2 -1
  190. data/lib/samagotchi/tools/tool_index.rb +27 -21
  191. data/lib/samagotchi/turn_endings.rb +2 -2
  192. data/lib/samagotchi/turn_flow.rb +4 -5
  193. data/lib/samagotchi/update_hint.rb +2 -2
  194. data/lib/samagotchi/version.rb +1 -1
  195. data/lib/samagotchi/web/app.rb +39 -50
  196. data/lib/samagotchi/web/participant_access.rb +2 -2
  197. data/lib/samagotchi/web/participant_review.rb +5 -5
  198. data/lib/samagotchi/web/participant_stream_filter.rb +8 -8
  199. data/lib/samagotchi/web/participant_view.rb +2 -82
  200. data/lib/samagotchi/web/participants_admin.rb +1 -1
  201. data/lib/samagotchi/web/public/app.js +61 -357
  202. data/lib/samagotchi/web/public/block_host.js +35 -8
  203. data/lib/samagotchi/web/public/block_renderers.css +9 -5
  204. data/lib/samagotchi/web/public/chains.js +2 -2
  205. data/lib/samagotchi/web/public/claim_link.js +4 -4
  206. data/lib/samagotchi/web/public/composer_mode.js +110 -0
  207. data/lib/samagotchi/web/public/format.js +3 -3
  208. data/lib/samagotchi/web/public/owner_claim.js +81 -0
  209. data/lib/samagotchi/web/public/owner_verify.js +112 -0
  210. data/lib/samagotchi/web/public/session_fields.js +3 -3
  211. data/lib/samagotchi/web/public/strip_grip.js +3 -3
  212. data/lib/samagotchi/web/public/strip_hidden.js +65 -0
  213. data/lib/samagotchi/web/public/version_notices.js +132 -0
  214. data/lib/samagotchi/web/push/store.rb +1 -1
  215. data/lib/samagotchi/web/session_access_routes.rb +5 -4
  216. data/lib/samagotchi/web/session_control.rb +12 -8
  217. data/lib/samagotchi/web/session_create.rb +1 -1
  218. data/lib/samagotchi/web/session_hub.rb +2 -2
  219. data/lib/samagotchi/web/session_show.rb +2 -2
  220. data/lib/samagotchi/web/session_summary.rb +1 -1
  221. data/lib/samagotchi/worker.rb +106 -9
  222. data/lib/samagotchi/worker_link.rb +3 -3
  223. data/lib/samagotchi/working_set_command.rb +3 -2
  224. metadata +36 -6
  225. data/lib/samagotchi/terminal_ui/between_turns.rb +0 -138
  226. data/lib/samagotchi/terminal_ui/repl_input.rb +0 -152
  227. data/lib/samagotchi/web/grants.rb +0 -575
  228. data/lib/samagotchi/web/participant_exposure.rb +0 -44
  229. data/lib/samagotchi/web/private_files.rb +0 -31
@@ -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
  ```
@@ -57,7 +57,8 @@ Engine#run_turn(session, prompt, on_event:, max_iterations:, cancel_controller:,
57
57
  │ # local System 1 host they wait for the turn's end: started before warm_up_next_turn, whose warm-up
58
58
  │ # waits for them, or from run_turn's ensure; docs/context.md, The judge);
59
59
  │ # then the session's toolkit (apply_toolkit!: Registry#toolkit, ToolIndex#refresh! for find_tools'
60
- │ # chi line, a prompt rebuild when it changed). A Stop during the probes or the waits returns false
60
+ │ # chi line, a prompt rebuild when it changed; a warn notice per plugin tool that arrived mid-session
61
+ │ # with nothing to reach it, Registry#stranded_names). A Stop during the probes or the waits returns false
61
62
  │ # (the ask's line says canceled; an ensure marks one the turn never settled failed).
62
63
  │ # fires the :session_start hook on the Engine's first turn, then the :before_turn hook
63
64
  │ # (a frozen copy of the history, the prompt, and resumed)
@@ -119,7 +120,7 @@ StandardError in run_turn:
119
120
  # error_kind, retryable, host and summary.
120
121
  → re-raised
121
122
 
122
- 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):
123
124
  → if turn.ended: re-raised only
124
125
  → turn.controller.cancel!(:ctrl_c)
125
126
  → end_turn(turn, "canceled"): ctrl_c_messages keeps the loop's conversation, else turn.messages
@@ -172,6 +173,12 @@ Stopped before the model was asked (prepare_turn returned false):
172
173
  → LLM::ModelResult(text: "", canceled: true, cancellation_reason: reason)
173
174
  ```
174
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
+
175
182
  ## The model↔tool loop
176
183
 
177
184
  The Engine does not talk to models directly. `Engine#backend` picks the loop the effective model's host speaks:
@@ -298,12 +305,13 @@ One iteration with tool calls:
298
305
  on_stream_event:, max_tool_output_chars:)
299
306
  → under TurnSettings#loads_tools, a direct call of a source tool's acts_as (mcp_fs_read) is wrapped
300
307
  into the call_tool call it stands for (Registry::Index#wrap, ToolIndex#wrap)
301
- → first, the call's entry unwraps it (Registry::Entry#unwrap: a call_tool of chi/<built-in> becomes
302
- that built-in's call with via: "call_tool", ToolIndex#unwrap); the rest sees the real call
308
+ → first, the call's entry unwraps it (Registry::Entry#unwrap: a call_tool of chi/<name>, a built-in or
309
+ a plugin tool, becomes that tool's call with via: "call_tool", ToolIndex#unwrap); the rest sees the
310
+ real call
303
311
  → a tool the session's toolkit leaves out (Registry#left_out; with via:, one Registry#indexed doesn't
304
312
  reach) skips the gate: no hooks, no card; KernelLoop#dispatch answers it (the call_tool route for a
305
313
  reachable tool, the not-loaded error for a removed one). Under TurnSettings#loads_tools a reachable
306
- built-in called by its own name is no such call: it runs through the gate, and KernelLoop#dispatch
314
+ built-in or plugin tool called by its own name is no such call: it runs through the gate, and KernelLoop#dispatch
307
315
  takes it (#loaded_entry)
308
316
  → Guardrails::Gate#evaluate (the before_tool_call hooks, then the core checks)
309
317
  → :tool_call_started
@@ -424,7 +432,7 @@ what a resolve gives, all without a `/model`. Frozen, so any thread may hold one
424
432
  - Never resolved (they hold on a broken resolve): `effective_model_ref`, `bare_model_name`,
425
433
  `guardrail_model_name`, `sync_model_key!`, `served_model(probe: false)` (`HostRegistry#model_ref`/`#bare_name`).
426
434
  - Readers on other threads: the Bridge (`/state`, `/stats`, the SSE reset, a steer's cut wait), the worker's
427
- 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`).
428
436
  - `ModelChoice` (`ModelBinding#choice`) holds the name and the typed name as one value, so a reader on another
429
437
  thread never pairs one model's name with another's typed name.
430
438
  - `EffectiveModel.of_session` builds a saved session's (no worker): `Web::App`, `LLMContextStrategy.for_session`.
@@ -515,7 +523,7 @@ drain hands the message to the model at a boundary, or until the turn ends (`Ste
515
523
  ### The turn's sink (`on_event`) and the observers
516
524
 
517
525
  `Engine#emit_event` (`EventFanout#emit_event`, `event_fanout.rb`, which also builds the loops' stream handler and
518
- 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
519
527
  persistent observers (`Engine#subscribe`), which get a copy with a monotonic `event_seq`. The observers are the
520
528
  metrics, the debug log, the thinking tails, and in a worker the Bridge, which streams to the web and the attached
521
529
  TUI. Errors in the sink or an observer are caught; they never break the turn.
@@ -644,7 +652,9 @@ Engine initialization (in this order, among other things):
644
652
  the bundles' plugins load (load_plugins → ExtensionLoad#load_plugins; each plugin's ctx is a
645
653
  Plugin::Context over plugin_host, whose ctx.sessions it builds itself: the test kit passes its own, sessions:),
646
654
  then (plugins or not) @tool_index.declare(@tools): find_tools + call_tool (source "index", not core),
647
- declared per toolkit by Registry#declared? (a source registered at load, or a reachable built-in) and
655
+ declared per toolkit by Registry#declared? (a source registered at load, or a tool registered then that
656
+ the toolkit leaves reachable: Registry::Index#loaded; Toolkit::Resolved#declares? treats built-ins and
657
+ plugin tools alike: full declares both, another kit its list and adds) and
648
658
  sets call_tool's unwrap (Tools::ToolIndex: the chi group from Registry#reachable_names, the sources'
649
659
  groups, search, call checks, the unwrap; a source runs the call with its own plugin Context)
650
660
  @reminder_store = ReminderStore.new (the kernel gets it), @reminder_queue, @reminders
@@ -773,7 +783,7 @@ The verdict is one of:
773
783
  - **allow**: the call runs.
774
784
  - **deny**: that call's output is replaced with denial text; the rest of the batch still runs.
775
785
  - **ask**: `Gate#settle_ask` first looks for a stored approval that covers the call. Otherwise
776
- `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
777
787
  `--non-interactive` run has no one to ask, so the call is denied. An approval wider than this once is stored.
778
788
 
779
789
  docs/guardrails.md covers the rules and the approval scopes.
@@ -866,7 +876,27 @@ Paths are under `lib/samagotchi/`.
866
876
 
867
877
  | File | Role |
868
878
  |---|---|
869
- | `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 |
870
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 |
871
901
  | `engine_snapshots.rb` | `EngineSnapshots`: the UIs' read model, `session_state_snapshot` (cheap, never raises) and `stats_snapshot` (may probe `/props`); the Engine forwards |
872
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,9 +469,9 @@ 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
- page quotes it the same way (`claimOpts()`, `normalizeClaim`; `spec/fixtures/redact_cases.json` keeps the two
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
476
476
  checks the same anchor against its parent's history. A completed Verify reply whose final non-empty line has
477
477
  a valid `Verdict: confirmed|refuted|unclear — <reason>` appends a verdict to the session. Both pages place its card
@@ -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
 
@@ -108,15 +108,19 @@ right-hand column happens.
108
108
  Claude 5.x cost the preserved thinking each time).
109
109
  - find_tools' description is a function of the toolkit and the source groups **at load**: `ToolIndex#declare`
110
110
  snapshots source group names and description lines (name, summary, callable count). `ToolIndex#refresh!` (from
111
- `ToolkitBinding#apply!`) filters that snapshot by the new toolkit and rebuilds the `chi` line from the built-ins
112
- it reaches; it never consults live group names. The same group filter drives searches, deferred MCP tools and
113
- whether `find_tools`/`call_tool` are declared (`Registry#index_needed?`). An MCP server that finishes booting
111
+ `ToolkitBinding#apply!` at a toolkit change, and from `Engine#apply_staged_tools!` when a plugin's tool set
112
+ changed) filters that snapshot by the toolkit and rebuilds the `chi` line from the built-ins and plugin tools it
113
+ reaches; it never consults live group names. The same group filter drives searches and deferred MCP tools.
114
+ Whether `find_tools`/`call_tool` are declared (`Registry#index_needed?`) reads the groups and the entries
115
+ registered at load (`Registry::Index#loaded`), never later ones: a plugin tool `chi.replace_tools` adds doesn't
116
+ bring them in, and a loaded plugin tool it later removes still counts, so `find_tools` stays declared, perhaps
117
+ with an empty `chi` group. That is deliberate: the declared tools stay the same for the worker's life. An MCP server that finishes booting
114
118
  between load and the first turn changes search answers only, so two sessions on the same toolkit with the same
115
119
  MCP cache send the same bytes. Kits that omit `groups:` keep their previous request bytes; switching only
116
120
  groups changes the prompt key and asks `/toolkit` to confirm the cache cost once the session has history.
117
121
  - A worker's `execute` and `task_create` descriptions carry the finish wake's sentence ("if you end your turn before
118
122
  it finishes, chi tells you when it does"; `Engine.new(task_finish_notes:)`, set once as the worker builds its
119
- 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
120
124
  between the two, or whose worker restarts after `task_wakes` changed, misses the cache once.
121
125
  - On remote Claude, `LLM::PromptCache.mark` sends the system message as two text parts. The stable part carries
122
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
@@ -10,8 +10,9 @@ only library. Pure modules (`*_model.js`, `session_state.js`, `sessions_list.js`
10
10
  and have `node --test` specs under `spec/web/public/` (happy-dom for the DOM ones); the pages as a whole are covered
11
11
  by `npm run e2e` (`spec/e2e/`). `app.js` has no unit spec.
12
12
 
13
- **`app.js` has a size cap**: `npm run lint` fails past `max-lines` in `eslint.config.js` (counted as `wc -l` counts,
14
- a little over its size today). A new feature goes into a module of its own, wired from `app.js` in a few lines; a
13
+ **`app.js` has a size cap**: `npm run lint` fails past `max-lines` in `eslint.config.js` (counted as `wc -l` counts:
14
+ 2000, ratcheted down from 2250 when the versions, the owner's claim links and Verify, the composer's mode and the
15
+ strip's hide went into modules of their own, which left room for a few features' wiring). A new feature goes into a module of its own, wired from `app.js` in a few lines; a
15
16
  refactor that shrinks `app.js` ratchets the cap down with it. Never raise it: extract instead.
16
17
 
17
18
  **New exports go in new modules.** The static JS is served at unversioned URLs. Today every load revalidates
@@ -23,7 +24,7 @@ page. A factory dep a new version stops passing is the same hazard the other way
23
24
  out of `send_turn.js`, `session_actions.js` and `lists_view.js`); a page loaded before keeps all its old modules
24
25
  until a reload, so the risk is a mixed cache, not an open tab. `answer_fold.js`, `details_motion.js`,
25
26
  `turn_range.js`, `history_reveal.js`, `latest.js`, `session_payload.js`, `method_api.js`, `session_store.js`,
26
- `session_fields.js`, `feature_gates.js`, `revise_stack.js`, `revise_switch.js`, `shell_command.js`, `stale_params.js`, `popover_modal.js`, `more_sheet.js`, `hash_intent.js` (what the hash asks for: session, all, start or keep), `worker_features.js`, `block_open.js`, `text_roots.js`, `block_source.js`, `block_frame.js`, `block_host.js` and the `objectives_*.js` modules are new modules for that reason. The last three `block_*` draw a bundle's fenced blocks: [block-renderers.md](block-renderers.md).
27
+ `session_fields.js`, `feature_gates.js`, `revise_stack.js`, `revise_switch.js`, `shell_command.js`, `stale_params.js`, `popover_modal.js`, `more_sheet.js`, `hash_intent.js` (what the hash asks for: session, all, start or keep), `worker_features.js`, `version_notices.js`, `owner_claim.js`, `owner_verify.js`, `composer_mode.js`, `strip_hidden.js`, `block_open.js`, `text_roots.js`, `block_source.js`, `block_frame.js`, `block_host.js` and the `objectives_*.js` modules are new modules for that reason. The last three `block_*` draw a bundle's fenced blocks: [block-renderers.md](block-renderers.md).
27
28
 
28
29
  ## Two pages, two models
29
30
 
@@ -37,7 +38,7 @@ claim links, `composer_mount.js`, the verdict card and Verify fresh, `verify_tar
37
38
  `auth_lost.js`, `token_fragment.js`, `format.js`) but none of the data layer, the stream handling or the message
38
39
  rendering. It has its own `api` (`{status, ok, body}`, never throws, its own ended / auth-lost handling) and its own
39
40
  `EventSource`. Its parallel copies of the owner's Verify and claim helpers (`verifyFreshState`, `verifyAnchorFor`,
40
- `showPendingClaim`, `CLAIM_ROOTS`) differ on purpose in places (the source kind `reply` vs `answer`, the server's
41
+ `showPendingClaim`, `CLAIM_ROOTS`; the owner's are `owner_verify.js` and `owner_claim.js`) differ on purpose in places (the source kind `reply` vs `answer`, the server's
41
42
  `fresh_running` vs `verify_fresh`, no home/cwd in its claim options). Don't converge them as a side effect of another
42
43
  change.
43
44
 
@@ -46,8 +47,8 @@ change.
46
47
  | Holder | What | Who writes | How a change reaches the screen |
47
48
  |---|---|---|---|
48
49
  | **The session store** (`session_store.js`; app.js `store`) | `store.selected` (the open session's id), `store.current` and `store.sessions` (below) | through the store only (see The session store); other modules get getters (`selected: () => store.selected`, `state: () => store.current`) or the store itself (`stream_handlers.js`, `send_turn.js`, `session_actions.js`) | the store runs the redraws that read what changed (below); nobody calls them by hand |
49
- | **app.js module scope**: about 20 top-level `let`s (`streamControl`, `eventsControl`, `pageCapabilities`/`pageEditorUrl`, `pendingVerifyAnchor`, `pendingClaim`, `resyncsInFlight`/`resyncAgain`, `resyncAttempts`, `scopeError`…) | the page's mode and its streams | app.js only; other modules get getters and hooks | the writer touches the redraw that reads it (`store.touch("infoBar")`: the stream, versions, `restartingIds`), or calls a redraw only it feeds |
50
- | **`store.current`** (`session_state.js`: `emptySessionState` / `sessionStateFrom`; per field: `session_fields.js FIELDS`) | the open session's fields, about 40 (info bar, chips, verdicts, timing, worker, stream cursor) | `store.update` / `setStatus` from app.js, `stream_handlers.js`, `send_turn.js` (`firstPreview`, `owner`), `session_actions.js` (`status`); `forgetSession`'s `replaceCurrent`. Two writers in place, on purpose (D8 in `FIELDS`): live_timing.js's `activeTurn`, verdict_fresh.js's running list | the store runs the field's redraws (`FIELDS[key].redraws`) |
50
+ | **app.js module scope**: about 20 top-level `let`s (`streamControl`, `eventsControl`, `pageCapabilities`/`pageEditorUrl`, `resyncsInFlight`/`resyncAgain`, `resyncAttempts`, `scopeError`…) | the page's mode and its streams | app.js only; other modules get getters and hooks | the writer touches the redraw that reads it (`store.touch("infoBar")`: the stream; `version_notices.js` does it for the versions and its `restarting` set), or calls a redraw only it feeds |
51
+ | **`store.current`** (`session_state.js`: `emptySessionState` / `sessionStateFrom`; per field: `session_fields.js FIELDS`) | the open session's fields, about 40 (info bar, chips, verdicts, timing, worker, stream cursor) | `store.update` / `setStatus` from app.js, `stream_handlers.js`, `send_turn.js` (`firstPreview`, `owner`), `session_actions.js` (`status`), `owner_verify.js` (`verdicts`, `verifyFresh`); `forgetSession`'s `replaceCurrent`. Two writers in place, on purpose (D8 in `FIELDS`): live_timing.js's `activeTurn`, verdict_fresh.js's running list | the store runs the field's redraws (`FIELDS[key].redraws`) |
51
52
  | **`store.sessions`** (the hub's projection) | every card | `store.update({sessions})` from app.js: `openSessionEvents` (`applySessionEvent`), `refresh` (the list fallback), `addCreatedSession`, `applyView` (re-sorted), `replaceSelectedCard` (`setLiveStatus` through `setStatus`, `readTurnEnd`, `setUsedMemories`), the stream's close (`withoutWorker`); `session_actions.js` (`setSessions`) | the store: `lists` and `infoBar`. **Always a new array**; an entry is replaced, never changed in place. `followSelectedSession` copies the open one's owner and worker (`fieldsFromCard`) and, when not streaming, its status (`setStatus(…, "hub")`) into `store.current` |
52
53
  | **Factory closures**: the `create*` factories (popovers, panels, views) | each component's own state: open-for, busy, error, its own fetched list | the factory | an `onChange` hook back into app.js, or a getter the factory reads at draw time |
53
54
  | **The history DOM** | the conversation as drawn: which turn a bubble belongs to (`data-turn-id`, `data-answer-turn`), what opens a turn (`TURN_PROMPT`, `bubbles.js`: a prompt or a wake's note, the same turns `Steer.turn_opening?` counts for a card's or the recap's `turns_since`), kept command bubbles, which texts were shown, unnamed badges, folded answers and long prompts (`.folded`), Revise stamps (`data-revises` on a prompt; a Verify turn's `data-verify`) and the stacks' folded versions (`.stack-hidden`, `.stack-stub.open`); the switcher's version on view is `revise_stack.js`'s own (a copy `.stack-view` before the top answer, the top `.stack-swapped`; never stored) | `bubbles.js`, `cards_place.js`, `question_cards.js`, `turn_block.js`/`stage_view.js`, `answer_fold.js`, `revise_stack.js`, `stream_handlers.js` (a live `turn_completed`'s `revises` and `verify` onto its prompt) | read back as state: `turn_range.js turnElements` (one walker for a turn's elements), `keptCommandBubbles`, `seedSeenFromDom`, `nameUnnamedBadges`/`nameVerdictCards`, `answer_fold.js` and `revise_stack.js` (their observers decorate every path that fills `#history`; the stacks are `stacks()` of the stamps, read again on each change, never a store field), `history_reveal.js revealInHistory` (the one way a jump unfolds what hides its target) |
@@ -73,8 +74,9 @@ through the store**, never `current.X = …`:
73
74
  old one writes there alone.
74
75
  - `store.batch(fn)`, `store.touch(name)`, `store.on(name, fn)`: the redraws (below).
75
76
 
76
- The ESLint block `samagotchi/session-writes` (eslint.config.js) enforces it in the six modules that hold the state
77
- (app.js, `stream_handlers.js`, `send_turn.js`, `session_actions.js`, `start_page.js`, `working_set_view.js`): an
77
+ The ESLint block `samagotchi/session-writes` (eslint.config.js) enforces it in the seven modules that hold the state
78
+ (app.js, `stream_handlers.js`, `send_turn.js`, `session_actions.js`, `start_page.js`, `working_set_view.js`,
79
+ `owner_verify.js`): an
78
80
  assignment, `++`, `delete` or `Object.assign` into `current`, `store.current` or `state()` is an error
79
81
  (`spec/web/public/lint_session_writes.test.js` runs it on a sample). An alias (`const st = state(); st.x = …`) isn't
80
82
  caught: don't write through one.
@@ -105,16 +107,16 @@ stale).
105
107
  **The redraws are registered, not called.** app.js registers each name once (`store.on`, at the bottom, once every
106
108
  piece a redraw calls exists): `lists` → `lists.render()`, `paintActive` → `lists.paintActive()`, `settingsPanel` →
107
109
  `settingsControl.refresh()`, `workingSetStrategy` → `workingSet.strategyChanged()`, `featureGates` (below), `composer` →
108
- `updateComposerMode()`, `infoBar` → `updateInfoBar()`. A writer writes and stops there: no `updateInfoBar()` after
110
+ `composerMode.update()` (`composer_mode.js`), `infoBar` → `updateInfoBar()`. A writer writes and stops there: no `updateInfoBar()` after
109
111
  a store write. A value written unchanged redraws nothing (a chunk with the same ctx %).
110
112
 
111
113
  An input the store doesn't hold calls `store.touch(name)` where it changes: `infoBar` for the clock (liveTurn's
112
- `onTick`), the versions (`noticeVersions`), `restartingIds`, the stream (`startStream`, `workerGone`), the working
114
+ `onTick`), the versions and the sessions restarting (`version_notices.js` `notice`, `restart`), the stream (`startStream`, `workerGone`), the working
113
115
  set's and the participants' `onChange`, the continue and review in-flight sets
114
116
  (`session_actions.js`); `composer` and `infoBar` for `liveTurn.running` (`setTurnRunning`, with `settingsPanel`:
115
117
  the open ⚙ panel's running-turn note) and at the end of `renderSessionView` (a whole view drawn). Left as they are,
116
- on purpose: the `narrowScreen` listener calls `updateComposerMode()` (the bar doesn't read it),
117
- `applyStripHidden`/`applyView` call `updateSessionTitle()`, and `closeStream`/`stream.restart` set
118
+ on purpose: `composer_mode.js`'s phone-width listener calls its `update()` (the bar doesn't read it),
119
+ `strip_hidden.js`'s apply and `applyView` call `updateSessionTitle()`, and `closeStream`/`stream.restart` set
118
120
  `streamControl = null` without a redraw (a resync or a workerGone follows).
119
121
 
120
122
  A function that writes more than once batches its synchronous part, so each redraw runs once: `select` (before its
@@ -178,7 +180,7 @@ The redraws, by their store names:
178
180
  - `infoBar` (`updateInfoBar()`): the session bar's buttons and info line (`info_bar_model.js sessionBar` →
179
181
  `info_bar.js`), the annotate bar, the fold controls, `updateSessionTitle()` and the fresh link's picker. Cheap: it
180
182
  runs on most writes and every clock tick.
181
- - `composer` (`updateComposerMode()`): the composer's placeholder, buttons and delivery line (selected, running,
183
+ - `composer` (`composer_mode.js` `update()`): the composer's placeholder, buttons and delivery line (selected, running,
182
184
  owner).
183
185
  - `settingsPanel` (`settingsControl.refresh()`): the ⚙ chip's **open panel** (`settings_chip.js`; the chip itself is
184
186
  drawn by the bar), on `thinking`, `reasoningHistory` and `llmContext`; `workingSetStrategy`: the working set's open
@@ -190,7 +192,8 @@ The redraws, by their store names:
190
192
  `forgetSession()` is the one reset: `select` and `clearSelection` both call it, so anything per-session that isn't
191
193
  in `store.current` (capabilities, the markdown warning and tip, the init row, the Verify claim, the armed Revise,
192
194
  the running turn's DOM) is cleared there, and the feature gates are primed for the empty session. A new per-session holder
193
- (an armed composer anchor like the Verify claim or the Revise) goes into it. The armed Revise
195
+ (an armed composer anchor like the Verify claim or the Revise) goes into it. The armed Verify claim is
196
+ `owner_verify.js`'s holder (`anchor()`, `set(a)`, `clear(a?)`: `clear(a)` lets go only of `a`). The armed Revise
194
197
  (`pendingReviseAnchor`) is set only through `setReviseAnchor`, which redraws its chip and the Revise buttons (a
195
198
  redraw only it feeds, not a store field).
196
199
 
@@ -222,7 +225,7 @@ each keeps one set of handlers, confirmations and in-flight rules. A chip the sh
222
225
  matters at exactly 600 px, where the CSS is a phone's but the shell (`SHEET_MAX`, `<`) still draws a popover, not a sheet.
223
226
  A new chip on the bar needs its place in that CSS's hidden list and a row in `moreRows`, and its attention state in the row, or
224
227
  it is lost on a phone. `app.js`'s `updateInfoBar` feeds the sheet (`update`) on every redraw; its rows and actions draw into
225
- fixed slots, so a row that joins keeps the focus where it was. The `narrowScreen` listener closes the sheet when the window grows.
228
+ fixed slots, so a row that joins keeps the focus where it was. `composer_mode.js`'s phone-width listener closes the sheet when the window grows.
226
229
 
227
230
 
228
231
  ## The stage's height
@@ -321,11 +324,11 @@ reach another device or the server.
321
324
  | `chi_model`, `chi_model_recent` | `model_pick.js`, `model_picker.js`, `start_page.js` | the start page's model and its recent list |
322
325
  | `chi_composer_collapsed` | `composer_fold.js` | the composer folded (a card's text field folds it for the time it has the focus on a narrow or touch screen, `composer_grip.js watchCardField`, on top of this and of the visit's state: neither is written) |
323
326
  | `chi_composer_floor` | `composer_grip.js`, `composer_mount.js` | the composer's dragged height |
324
- | `chi_strip_hidden` (`chi_zen`: the old name, read once) | app.js | the session strip hidden |
327
+ | `chi_strip_hidden` (`chi_zen`: the old name, read once) | `strip_hidden.js` | the session strip hidden |
325
328
  | `chi_stage_collapsed` | `stage_view.js` | the stage collapsed |
326
329
  | `chi_working_set_collapsed` | `working_set_rail.js` | the working set rail collapsed |
327
330
  | `chi_markdown_tip_dismissed` | `markdown_tip.js` | the markdown tip dismissed |
328
- | `chi_update_available_shown` | app.js | update notices already shown |
331
+ | `chi_update_available_shown` | `version_notices.js` | update notices already shown |
329
332
  | `chi_notify`, `chi_push_device` | `notifications.js` (an injected storage) | the bell's state, this device's push id |
330
333
  | `chi_answer_fold:<session>` | `answer_fold.js`, `revise_stack.js` | the answers and long prompts (`<turn id>:prompt`) folded by hand, and the Revise stubs unfolded by hand (`stub:<turn id>`) (swept with the session list) |
331
334
  | `chi_review_history:<grant>`, `chi_review_history_session:<session>` | `review_history.js` (participant page) | a participant's composer history |
@@ -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.