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/testing.md CHANGED
@@ -30,6 +30,11 @@ stage's hand-off dwell in stage.spec.js); a pause that is the behaviour outside
30
30
  route) carries `// eslint-disable-line no-restricted-syntax -- why`. And in the seven
31
31
  modules that hold the owner page's session state, one more: no write into `current`, `store.current` or `state()`
32
32
  in place (`samagotchi/session-writes`; the store is the write path, see internals/web-frontend.md, The session store).
33
+ In `spec/web/public/`, one rule only (`samagotchi/dom-null-asserts`): no `assert.equal(domNode, null)`. When it fails,
34
+ node's assert formats the happy-dom node, whose graph (its document, its window) takes minutes and gigabytes, so
35
+ `npm test` hangs instead of failing (`--test-timeout` doesn't end it). Write `assert.ok(x === null)` (or `=== undefined`),
36
+ which names the expression and fails at once. The rule sees a DOM lookup by its shape (`querySelector`, `closest`,
37
+ `parentElement`…); a helper that returns a node (`h.input()`) isn't seen, so the same form goes there by hand.
33
38
 
34
39
  CI's e2e job keeps the tests, chi and the fake model on the runner but runs Chromium in Playwright's image
35
40
  (`mcr.microsoft.com/playwright:v<version>-noble`, `playwright run-server` on the host network); the tests connect
@@ -39,7 +44,9 @@ moves the image too (`spec/ci_playwright_image_spec.rb` keeps a pinned tag out o
39
44
  that way locally: `npx playwright run-server --port 3333`, then `PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:3333/ npm run e2e`.
40
45
 
41
46
  No spec reads your own setup. The suite points `XDG_CONFIG_HOME` and `XDG_STATE_HOME` at temp folders,
42
- clears your `SAMAGOTCHI_*` environment, and keeps every spec off the network (WebMock).
47
+ clears your `SAMAGOTCHI_*` environment, and keeps every spec off the network (WebMock). `TMPDIR` is a folder of the
48
+ run's own (`SPEC_TMPDIR`, removed at the end), so a spec's bare `Dir.mktmpdir` or `Dir.tmpdir`, and what chi writes or
49
+ sweeps beside it, never touches your real `$TMPDIR`. A spec can't assume that path's length: it is your `$TMPDIR`'s plus one folder.
43
50
  It also clears what tells git which repo to act on (`GIT_DIR`, `GIT_WORK_TREE`, the rest of
44
51
  `git rev-parse --local-env-vars`: `GitEnv`), first thing in `spec_helper.rb` and as the bundle test kit's
45
52
  adapters load, so a run from `git rebase --exec`, `bisect run` or a hook can't reach the real repo through a spec's git.
@@ -74,11 +81,11 @@ back (`t.mock` restores itself). A test that sets off a module's timer (a flash,
74
81
  node running until it fires (a 4 s one made the run 6 s for 2 s of work). One file:
75
82
  `node --test spec/web/public/NAME.test.js`.
76
83
 
77
- Where a composer line goes is decided twice, in Ruby (`ComposerRoute`, both terminals) and in the web
84
+ Where a composer line goes is decided twice, in Ruby (`ComposerRoute`, the attached terminal) and in the web
78
85
  (`composer.js#routeComposerLine`). `spec/fixtures/composer_routes.json` holds the cases both answer: rspec
79
86
  (`spec/composer_route_spec.rb`) and `npm test` (`spec/web/public/composer.test.js`) run every row. A change to
80
- either router adds its row there; a row where a UI differs on purpose (or a found divergence) carries a `web` or
81
- `repl` override and a `note`. The Ruby side runs each row with every core command (a new one fails its
87
+ either router adds its row there; a row where a UI differs on purpose (or a found divergence) carries a `web`
88
+ override and a `note`. The Ruby side runs each row with every core command (a new one fails its
82
89
  `CORE_ROUTES` check until listed there); a row's names that core lacks stand for plugin commands.
83
90
 
84
91
  ## Levels
@@ -174,6 +181,9 @@ whose value is dropped, an example's last line included, `expect(wait_until {
174
181
  `Timeout.timeout`, or a statement `thread.join(N)`, whose N is unscaled and whose nil is dropped: use `join!`) live in `rubocop/` and run in `rake lint`, so CI fails on them. RSpec counts `wait_for`,
175
182
  `eventually`, `join!` and `pop!` as an example's expectation (`RSpec/NoExpectationExample`), so a wait can be an
176
183
  example's last line. What the cops can't see, `script/affected-specs` hints at (`timing:`, below).
184
+ One lib cop sits beside them: `Samagotchi/SilentRescue` (a `rescue` of StandardError whose body is only a literal,
185
+ `nil`, `[]`…: use `Degrade`, or tag a right silence `# degrade-ok: why`; docs/internals/engine.md, Degrading). The
186
+ files that had such rescues when it came are excluded in `.rubocop.yml`: that list only shrinks.
177
187
 
178
188
  ### Flakes
179
189
 
@@ -308,8 +318,8 @@ A builder runs these while working, not the full suite: the specs the change tou
308
318
  files it changed (`lib/samagotchi/foo/bar.rb` → `spec/foo/bar_spec.rb`, `bar_*_spec.rb`…;
309
319
  `lib/samagotchi/foo/bar/baz.rb` → `spec/foo/bar_baz_spec.rb`), the specs that read
310
320
  docs or source as text (its `RULES`: add one when a full run catches what the names missed) and the golden of a changed
311
- file's layer (any `terminal_ui/*.rb` → `spec/terminal_ui_golden_spec.rb`; `turn_flow.rb` and `turn_note.rb`, whose
312
- words the REPL prints, pick it by a rule). Changes to `tool_declarations.rb` and `system_prompt.rb` also pick the
321
+ file's layer (any `foo/*.rb` → `spec/foo_golden_spec.rb` when there is one; `turn_flow.rb` and `turn_note.rb`, whose
322
+ words the attached TUI prints, pick its `event_renderer` and `attached_loop` specs by a rule). Changes to `tool_declarations.rb` and `system_prompt.rb` also pick the
313
323
  effective-model, request, prompt-snapshot and full system-prompt goldens because they hash the request's prompt and
314
324
  tools. Then come the other name matches (a spec named after several changed files first, e.g. `engine_toolkit_spec.rb` for `engine.rb` and
315
325
  `toolkit.rb`; then one beside the file, so `plugin/context.rb` picks `spec/samagotchi/plugin/*context*` before
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "model_profile"
4
+ require_relative "output_formatter"
5
+ require_relative "tool_call_parser"
6
+
7
+ module Samagotchi
8
+ # What the user saw of a model entry's text: the one rule for the continue
9
+ # note's last answer (SessionChain.last_answer), a check's record of the
10
+ # agent's own words (ObjectivesCheck) and a bundle's ctx.last_answer. A chat
11
+ # entry's content is that already (its reasoning is the separate :thinking,
12
+ # its calls :tool_calls); a native entry's is the raw response, with its
13
+ # thoughts and call markup inline: both hosts' thoughts come out, a thought
14
+ # cut off before it closed too, then the calls and the wire tokens.
15
+ module AnswerText
16
+ QWEN_THINK = %r{<think>.*?</think>}m
17
+ # The template opened the block, so the text starts inside it.
18
+ TEMPLATE_OPENED_THINK = %r{\A.*?</think>}m
19
+
20
+ module_function
21
+
22
+ # @param text [String, nil]
23
+ # @return [String] "" when nothing was visible
24
+ def visible(text)
25
+ text = text.to_s.scrub
26
+ template_opened = !text.include?("<think>")
27
+ gemma = ToolCallParser::Gemma.new(ModelProfile.gemma4)
28
+ text = gemma.strip_thought(text).gsub(QWEN_THINK, "")
29
+ text = text.sub(TEMPLATE_OPENED_THINK, "") if template_opened
30
+ text = ToolCallParser.strip_calls(gemma.strip_tool_calls(text), "<tool_call>", "</tool_call>")
31
+ OutputFormatter.strip_markup(text) # drops a thought cut off before it closed
32
+ end
33
+ end
34
+ end
@@ -9,6 +9,7 @@ require "openssl"
9
9
  require "time"
10
10
  require_relative "atomic_file"
11
11
  require_relative "bridge_secret"
12
+ require_relative "degrade"
12
13
  require_relative "delivery"
13
14
  require_relative "session_inbox"
14
15
  require_relative "turn_anchors"
@@ -156,6 +157,9 @@ module Samagotchi
156
157
  # Open streams by the client_id a stream named (`?client_id=`); web
157
158
  # tabs, through the `chi web` proxy, name none.
158
159
  @streams_by_client = Hash.new(0)
160
+ # Streams served since the start, open or closed (a scratch worker
161
+ # leaves once its terminal's has gone: Worker#scratch_unattended?).
162
+ @streams_served = 0
159
163
  @last_client_activity_at = monotonic_now
160
164
  end
161
165
 
@@ -178,6 +182,11 @@ module Samagotchi
178
182
  @mutex.synchronize { @open_streams }
179
183
  end
180
184
 
185
+ # @return [Integer] SSE streams served since the Bridge started
186
+ def streams_served
187
+ @mutex.synchronize { @streams_served }
188
+ end
189
+
181
190
  # Streams held by anyone but +client_id+: a client's own stream (or two,
182
191
  # while it reconnects and the old one isn't noticed dead yet) doesn't
183
192
  # count. A closed stream stops counting once its writer reads the
@@ -236,7 +245,7 @@ module Samagotchi
236
245
  @mutex.synchronize { @stopped = true }
237
246
  begin
238
247
  @server&.close
239
- rescue StandardError
248
+ rescue StandardError # degrade-ok: stopping: a server socket already closed is fine
240
249
  nil
241
250
  end
242
251
  wake_acceptor
@@ -254,7 +263,7 @@ module Samagotchi
254
263
  @mutex.synchronize { @stopped = true }
255
264
  begin
256
265
  @server&.close
257
- rescue StandardError
266
+ rescue StandardError # degrade-ok: stopping: a server socket already closed is fine
258
267
  nil
259
268
  end
260
269
  wake_acceptor
@@ -268,13 +277,13 @@ module Samagotchi
268
277
  # the web, which then queues the prompt again from the input file.
269
278
  await_answers(REQUEST_GRACE_SECONDS)
270
279
  remove_sidecar
271
- @connection_threads.each { |t| t.kill rescue nil }
280
+ @connection_threads.each { |t| t.kill rescue nil } # degrade-ok: stopping: a thread already gone is fine
272
281
  @connection_threads.clear
273
282
  # Its relay watchers (RelayWatcher) end with it.
274
283
  watchers = @mutex.synchronize { (@relay_watchers || []).tap { @relay_watchers = [] } }
275
284
  watchers.each { |thread| thread.kill.join(RELAY_WATCHER_JOIN_SECONDS) }
276
285
  @accept_thread&.join(2)
277
- [@wake_reader, @wake_writer].each { |io| io&.close rescue nil }
286
+ [@wake_reader, @wake_writer].each { |io| io&.close rescue nil } # degrade-ok: stopping: a closed pipe is fine
278
287
  nil
279
288
  end
280
289
 
@@ -399,9 +408,10 @@ module Samagotchi
399
408
  # O(1) and non-blocking.
400
409
  def capture_observer
401
410
  @capture_proc ||= proc do |event|
402
- @ring.push(seq: event[:event_seq], data: event)
403
- rescue StandardError
404
- nil
411
+ # Per event: the first failure warns, the rest log at debug.
412
+ Degrade.call(:bridge, "event_capture_failed") do
413
+ @ring.push(seq: event[:event_seq], data: event)
414
+ end
405
415
  end
406
416
  end
407
417
 
@@ -527,7 +537,7 @@ module Samagotchi
527
537
  Thread.current[:bridge_answering] = false
528
538
  begin
529
539
  io.close
530
- rescue StandardError
540
+ rescue StandardError # degrade-ok: the client may be gone already; closing is all that's left
531
541
  nil
532
542
  end
533
543
  end
@@ -555,6 +565,7 @@ module Samagotchi
555
565
  )
556
566
  @mutex.synchronize do
557
567
  @open_streams += 1
568
+ @streams_served += 1
558
569
  @streams_by_client[client_id] += 1 if client_id
559
570
  end
560
571
  begin
@@ -860,7 +871,7 @@ module Samagotchi
860
871
  end
861
872
  end
862
873
 
863
- # Dismiss the pending question (an empty answer, as in the REPL): the
874
+ # Dismiss the pending question (an empty answer at a terminal): the
864
875
  # tool returns without an answer and every UI gets :question_cancelled.
865
876
  # Only the question the client saw, and only while nobody has answered
866
877
  # it: Engine#cancel_question checks both under the question lock. A past
@@ -1425,44 +1436,44 @@ module Samagotchi
1425
1436
  # else fall back to the explicit `?from_seq=` query param used by clients
1426
1437
  # that remember their own cursor.
1427
1438
  def reconnect_cursor(headers, query)
1428
- cursor = headers["last-event-id"]
1429
- return cursor if cursor && !cursor.empty?
1439
+ Degrade.call(:bridge, "sse_cursor_unparsable") do
1440
+ cursor = headers["last-event-id"]
1441
+ return cursor if cursor && !cursor.empty?
1430
1442
 
1431
- return nil unless query
1443
+ return nil unless query
1432
1444
 
1433
- URI.decode_www_form(query).to_h["from_seq"]
1434
- rescue StandardError
1435
- nil
1445
+ URI.decode_www_form(query).to_h["from_seq"]
1446
+ end
1436
1447
  end
1437
1448
 
1438
1449
  # `?snapshot=1`: join with a snapshot frame instead of a replay.
1439
1450
  def snapshot_requested?(query)
1440
- return false unless query
1451
+ Degrade.call(:bridge, "snapshot_query_unparsable", fallback: false) do
1452
+ return false unless query
1441
1453
 
1442
- URI.decode_www_form(query).to_h["snapshot"].to_s == "1"
1443
- rescue StandardError
1444
- false
1454
+ URI.decode_www_form(query).to_h["snapshot"].to_s == "1"
1455
+ end
1445
1456
  end
1446
1457
 
1447
1458
  # `?raw=1`: events as the engine emitted them (a chunk's provider
1448
1459
  # payload), not as clients get them (ClientEvent). For debugging on the
1449
1460
  # worker's port: chi web never passes it on.
1450
1461
  def raw_requested?(query)
1451
- return false unless query
1462
+ Degrade.call(:bridge, "raw_query_unparsable", fallback: false) do
1463
+ return false unless query
1452
1464
 
1453
- URI.decode_www_form(query).to_h["raw"].to_s == "1"
1454
- rescue StandardError
1455
- false
1465
+ URI.decode_www_form(query).to_h["raw"].to_s == "1"
1466
+ end
1456
1467
  end
1457
1468
 
1458
1469
  # `?client_id=`: whose stream it is (see #open_streams_except).
1459
1470
  def stream_client_id(query)
1460
- return nil unless query
1471
+ Degrade.call(:bridge, "client_id_query_unparsable") do
1472
+ return nil unless query
1461
1473
 
1462
- id = URI.decode_www_form(query).to_h["client_id"].to_s
1463
- id.empty? ? nil : id
1464
- rescue StandardError
1465
- nil
1474
+ id = URI.decode_www_form(query).to_h["client_id"].to_s
1475
+ id.empty? ? nil : id
1476
+ end
1466
1477
  end
1467
1478
 
1468
1479
  def close_after_request?(headers)
@@ -1534,11 +1545,11 @@ module Samagotchi
1534
1545
  # same session may have written its own since (a file without an epoch
1535
1546
  # is only ever an older worker's).
1536
1547
  def remove_sidecar
1537
- return unless @server
1548
+ Degrade.call(:bridge, "sidecar_remove_failed") do
1549
+ return unless @server
1538
1550
 
1539
- File.unlink(WorkerSidecar.path(session_dir)) if WorkerSidecar.read(session_dir)&.epoch == @epoch.to_s
1540
- rescue StandardError
1541
- nil
1551
+ File.unlink(WorkerSidecar.path(session_dir)) if WorkerSidecar.read(session_dir)&.epoch == @epoch.to_s
1552
+ end
1542
1553
  end
1543
1554
 
1544
1555
  def session_dir
@@ -4,6 +4,7 @@ require "digest"
4
4
  require "json"
5
5
  require "socket"
6
6
 
7
+ require_relative "degrade"
7
8
  require_relative "worker_sidecar"
8
9
  require_relative "bridge_secret"
9
10
  require_relative "delivery"
@@ -329,17 +330,17 @@ module Samagotchi
329
330
  body = response.split("\r\n\r\n", 2)[1] || ""
330
331
  Response.new(status: status.to_i, body: body)
331
332
  ensure
332
- sock&.close rescue nil
333
+ sock&.close rescue nil # degrade-ok: done with the socket; one already closed is fine
333
334
  end
334
335
 
335
336
  # Monotonic SSE cursor of the live Engine, from GET /session/:id/state.
336
337
  # @return [Integer, nil]
337
338
  def event_seq
338
- state = get_json("state")
339
- seq = state && state.dig("session_state_snapshot", "event_seq")
340
- seq.nil? ? nil : seq.to_i
341
- rescue StandardError
342
- nil
339
+ Degrade.call(:bridge, "event_seq_unreadable") do
340
+ state = get_json("state")
341
+ seq = state && state.dig("session_state_snapshot", "event_seq")
342
+ seq.nil? ? nil : seq.to_i
343
+ end
343
344
  end
344
345
 
345
346
  # Open GET /session/:id/stream and classify the response before the
@@ -358,7 +359,7 @@ module Samagotchi
358
359
  if connection
359
360
  sock, status, headers = connection
360
361
  unless !block_given? || yield(sock)
361
- sock.close rescue nil
362
+ sock.close rescue nil # degrade-ok: the stream is given up; a socket already closed is fine
362
363
  return StreamOpen::Unreachable.new(attempts: count)
363
364
  end
364
365
  return StreamOpen.classify(sock, status, headers)
@@ -389,7 +390,7 @@ module Samagotchi
389
390
  break
390
391
  end
391
392
  ensure
392
- sock&.close rescue nil
393
+ sock&.close rescue nil # degrade-ok: done with the socket; one already closed is fine
393
394
  end
394
395
  end
395
396
 
@@ -435,7 +436,7 @@ module Samagotchi
435
436
  body = read_body ? reply.split("\r\n\r\n", 2)[1] : nil
436
437
  Response.new(status: reply[STATUS_LINE, 1].to_i, body: body)
437
438
  ensure
438
- sock&.close rescue nil
439
+ sock&.close rescue nil # degrade-ok: done with the socket; one already closed is fine
439
440
  end
440
441
 
441
442
  # The reply up to the Bridge's close (or its first line only), within
@@ -12,7 +12,7 @@ module Samagotchi
12
12
  # A session a broadcast may concern.
13
13
  # @!attribute project [String, nil] its git project's root
14
14
  # @!attribute live [Boolean] a worker runs it now
15
- # @!attribute owner [String, nil] "worker", "tui" (a chi REPL, which
15
+ # @!attribute owner [String, nil] "worker", "tui" (a one-turn chi run, which
16
16
  # takes no notes) or nil
17
17
  Recipient = Data.define(:id, :short_id, :project, :cwd, :desc, :live, :owner) do
18
18
  def repl? = owner == "tui"
@@ -20,7 +20,7 @@ module Samagotchi
20
20
 
21
21
  # Who a broadcast goes to: the user's own sessions, not delegate
22
22
  # children (their parent relays what concerns them) and not scratch
23
- # ones, that a worker or a chi REPL runs now or that ended a turn in
23
+ # ones, that a worker or a one-turn chi run runs now or that ended a turn in
24
24
  # the last +active_hours+. Forks count: they are the user's sessions.
25
25
  # Archived sessions don't; test runs only when chi runs as one.
26
26
  module Recipients
@@ -23,7 +23,7 @@ module Samagotchi
23
23
  # @!attribute by [String] who decided: "tags" (a shared tag),
24
24
  # "model", "fallback" (delivered unchecked: deadline, error, an
25
25
  # answer that is no yes or no), "scope" (the note's first line
26
- # names another project), "all" (--all) or "repl" (a chi REPL owns
26
+ # names another project), "all" (--all) or "repl" (a one-turn chi run owns
27
27
  # it: no notes)
28
28
  # @!attribute match [Match, nil] the shared tag (by "tags")
29
29
  Verdict = Data.define(:relevant, :p, :reason, :by, :match) do
@@ -46,7 +46,7 @@ module Samagotchi
46
46
  note from "broadcast": background the model sees on its next turn,
47
47
  not a prompt. It starts no turn.
48
48
  Recipients: your own sessions (not delegate children, not scratch
49
- ones) that a worker or a chi REPL runs now, or that ended a turn in
49
+ ones) that a worker or a one-turn chi run runs now, or that ended a turn in
50
50
  the last broadcast.active_hours (8). One gets the note when it shares
51
51
  a tag with it: a ticket id (PAY-123) in its branch or your prompts
52
52
  there, a pull request (PR #42, its URL) attached to it, a link in
@@ -190,13 +190,13 @@ module Samagotchi
190
190
  corrections: []), decisions, cards)
191
191
  end
192
192
 
193
- # Every recipient's verdict: a chi REPL takes no notes, --all takes the
193
+ # Every recipient's verdict: a one-turn chi run takes no notes, --all takes the
194
194
  # rest, else Broadcast::Triage (tags, the scope line, the model).
195
195
  # @return [Array<Decision>] in +recipients+' order
196
196
  def decide(text, recipients, cards, note_tags, all:)
197
197
  fixed = recipients.to_h do |r|
198
198
  verdict = if r.repl?
199
- Broadcast::Triage::Verdict.new(relevant: false, p: nil, reason: "open in a chi REPL", by: "repl")
199
+ Broadcast::Triage::Verdict.new(relevant: false, p: nil, reason: OwnerLock::TUI_BUSY, by: "repl")
200
200
  elsif all
201
201
  Broadcast::Triage::Verdict.new(relevant: true, p: nil, reason: "--all", by: "all")
202
202
  end
@@ -254,7 +254,7 @@ module Samagotchi
254
254
 
255
255
  result = NoteDelivery.deliver(d.recipient.id, text: body(broadcast.text, d.verdict), source: SOURCE,
256
256
  state_dir: @state_dir)
257
- next [d, "skipped", "open in a chi REPL"] unless result.delivered?
257
+ next [d, "skipped", OwnerLock::TUI_BUSY] unless result.delivered?
258
258
 
259
259
  [d, "delivered", "#{d.verdict.reason}#{"; waits for its next start" if result.status == :waits}"]
260
260
  rescue SessionInbox::NoteRejected, SystemCallError => e
@@ -369,7 +369,7 @@ module Samagotchi
369
369
 
370
370
  result = NoteDelivery.deliver(id, text: passed_on_body(record), source: SOURCE, state_dir: @state_dir)
371
371
  unless result.delivered?
372
- @stdout.puts(line_for(id, "skipped", "open in a chi REPL"))
372
+ @stdout.puts(line_for(id, "skipped", OwnerLock::TUI_BUSY))
373
373
  return Broadcast::TriageLog::Delivery.new(session: id, result: "refused")
374
374
  end
375
375
 
@@ -42,7 +42,7 @@ module Samagotchi
42
42
  f.switch "--json"
43
43
  f.switch "--keep"
44
44
  f.value "--timeout"
45
- f.refuse "--attach", "--shared", "chi bundle try can't use --attach or --shared"
45
+ f.refuse "--attach", "chi bundle try can't use --attach"
46
46
  end
47
47
  DEFAULT_TIMEOUT = 300
48
48
  # The sandbox's cost-cap bundle setting (--set cost-cap.cap=N).
@@ -30,7 +30,7 @@ module Samagotchi
30
30
 
31
31
  def run(prompt:, options:, timeout:)
32
32
  command = [RbConfig.ruby, "-I", Sandbox::LIB_DIR, Sandbox::CHI,
33
- "--no-shared", "--non-interactive", "-p", prompt, *child_options(options)]
33
+ "--non-interactive", "-p", prompt, *child_options(options)]
34
34
  trap_state, old_traps = trap_forwarding
35
35
  pid = nil
36
36
  streams = []
@@ -367,8 +367,8 @@ module Samagotchi
367
367
  prompt = build_conflict_prompt(bundle_name, installer.conflicts, expanded_source)
368
368
  before = installer.conflicts.to_h { |key, info| [key, Samagotchi::MemoryBundle::Merger.digest(info[:current])] }
369
369
  @stdout.puts "Launching an interactive session to merge them… (/exit when done)"
370
- require "samagotchi/terminal_ui"
371
- Samagotchi::TerminalUI.new(prompt: prompt).run
370
+ require "samagotchi/terminal_ui/attach_launcher"
371
+ Samagotchi::TerminalUI::AttachLauncher.run(prompt: prompt)
372
372
  settle_conflicts(installer.conflicts, before, bundle_name)
373
373
  ensure
374
374
  # An owned source (git/zip/tar) was kept for the incoming files above;
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: decision-card
3
- version: 0.2.0
3
+ version: 0.2.1
4
4
  scope: system
5
5
  description: "In a session with objectives, a long answer ending in a question gets a card: a short summary and the question, answered in the composer"
6
6
  trust_level: reviewed
7
7
  plugin:
8
8
  file: plugin.rb
9
- sha256: sha256:e628d84ab9521f7e8e5f3bc69da97fe00e574082412ea24e9f3f1c6234ad31a9
10
- # ctx.objectives and ctx.turn_running? are new after 0.148.0
11
- requires_chi: ">= 0.149.0"
9
+ sha256: sha256:b85fd2162e41d1ea059ff6ca89ce6bb543e4f283d18337e9d4ac6936477d3770
10
+ # ctx.last_answer is new after 0.153.0 (ctx.objectives, ctx.turn_running? after 0.148.0)
11
+ requires_chi: ">= 0.154.0"
12
12
  # config.yml bundles: decision-card: (another key warns; an older chi ignores this)
13
13
  settings: [min_chars, tail_chars, model, timeout, max_tokens]
@@ -135,7 +135,7 @@ class Plugin
135
135
  return SHORT_BUSY if ctx.turn_running?
136
136
 
137
137
  messages = ctx.messages
138
- answer = last_answer(messages) or return "no answer to shorten yet"
138
+ answer = ctx.last_answer or return "no answer to shorten yet"
139
139
 
140
140
  id = ctx.card(title: SHORT_TITLE, body: "…")
141
141
  @mutex.synchronize { @shorts[id] = ctx }
@@ -158,14 +158,6 @@ class Plugin
158
158
  ctx.card(id: id, title: SHORT_TITLE, body: body)
159
159
  end
160
160
 
161
- # @return [String, nil] the newest non-empty answer's text
162
- def last_answer(messages)
163
- found = Array(messages).reverse_each.find do |message|
164
- %w[model assistant].include?(field(message, :role).to_s) && !field(message, :content).to_s.strip.empty?
165
- end
166
- found && field(found, :content).to_s
167
- end
168
-
169
161
  def turn_ended(event, ctx)
170
162
  return unless event[:status].to_s == "completed"
171
163
  # A participant's question is for the participant, on the owner's model; a
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  name: dev
3
- version: 0.7.6
3
+ version: 0.7.7
4
4
  scope: system
5
5
  description: "Developer tools"
6
6
  trust_level: reviewed
7
- requires_chi: ">= 0.150.0"
7
+ requires_chi: ">= 0.154.0"
8
8
  includes:
9
9
  - known-names
10
10
  - mcp
@@ -18,7 +18,7 @@ Single registry `Samagotchi::Config` (`Config::ENTRIES` in `lib/samagotchi/confi
18
18
 
19
19
  Sections forbid `_`/`-` (`SECTION_RE` `/\A[a-z0-9]+\z/`), since an env or CLI name is derived from them; a config-only section (`system_one`: `expose: [:config]`, no derived name) may carry `_`. Leaves keep `snake_case` in YAML (`base_url`) and become kebab in CLI (`base-url`) via registry derivation — no generic string split, registry lookup avoids flat vs nested collision.
20
20
 
21
- **Universal entries** (`expose: [:env,:config,:cli]`): `default.model`, `server.host/port/open_timeout/read_timeout`, `server.first_token_timeout` (env and config only), `recap.model/base_url/host_ref/inactivity/timeout/min_user_turns/sentences`, `session.retention_days/max_count/keep_status/sweep_interval_hours/idle_exit_minutes`, `session.shared/keep_empty/max_children` (env and config only), `log.file/disable`, `status.line`, `context.status/window_tokens/chars_per_token/status_thresholds`, `memory.index_warn_tokens/entry_warn_tokens/read_max_chars`, `default.max_tokens`, `max_tool_output_chars`, `retry.max/base_delay/max_delay`, `read.*`, `execute.*`, `web.port/host`, `no_interrupt`, `no_default_input` etc. (`Config::ENTRIES`; `expose` says which of env/config/cli each takes). Precedence is `CLI > ENV > file > default`.
21
+ **Universal entries** (`expose: [:env,:config,:cli]`): `default.model`, `server.host/port/open_timeout/read_timeout`, `server.first_token_timeout` (env and config only), `recap.model/base_url/host_ref/inactivity/timeout/min_user_turns/sentences`, `session.retention_days/max_count/keep_status/sweep_interval_hours/idle_exit_minutes`, `session.keep_empty/max_children` (env and config only), `log.file/disable`, `status.line`, `context.status/window_tokens/chars_per_token/status_thresholds`, `memory.index_warn_tokens/entry_warn_tokens/read_max_chars`, `default.max_tokens`, `max_tool_output_chars`, `retry.max/base_delay/max_delay`, `read.*`, `execute.*`, `web.port/host`, `no_interrupt`, `no_default_input` etc. (`Config::ENTRIES`; `expose` says which of env/config/cli each takes). Precedence is `CLI > ENV > file > default`.
22
22
 
23
23
  Example `config.yml` (new nested form, preferred):
24
24
 
@@ -40,10 +40,9 @@ recap:
40
40
  session:
41
41
  retention_days: 14
42
42
  max_count: 500
43
- keep_status: "" # the default: no status protects a session from pruning; a live worker or REPL always does
43
+ keep_status: "" # the default: no status protects a session from pruning; a live worker or a one-turn run always does
44
44
  sweep_interval_hours: 24
45
45
  idle_exit_minutes: 30 # a background worker nobody uses exits; 0 = never
46
- shared: true # the default: plain `chi` runs its session in a background worker and attaches (as `chi --shared`); false keeps the in-process REPL (env SAMAGOTCHI_SESSION_SHARED; no CLI flag, `--no-shared` opts out per run)
47
46
  keep_empty: false # the default: a session nothing happened in (no prompt, default model, no note/image) is deleted when left; true keeps it (env SAMAGOTCHI_SESSION_KEEP_EMPTY)
48
47
  max_children: 4 # running child sessions one session may have delegated at a time (the delegate tool; env SAMAGOTCHI_SESSION_MAX_CHILDREN)
49
48
  log:
@@ -77,7 +76,7 @@ guardrails:
77
76
  disable: [guardrails:git-rebase]
78
77
  ```
79
78
 
80
- - `bundles:` map of installed bundle name → its settings (one Hash, string keys, handed to the bundle's hook/plugin `initialize(settings)`). Read when a session's Engine starts: after a change restart the session's worker (`chi sessions stop <id>`) or the REPL. Unknown keys are ignored by the bundle, not validated. `chi bundle list` names the bundles; each bundle's keys are in `docs/bundles.md` / `docs/guardrails.md`.
79
+ - `bundles:` map of installed bundle name → its settings (one Hash, string keys, handed to the bundle's hook/plugin `initialize(settings)`). Read when a session's Engine starts: after a change restart the session's worker (`chi sessions stop <id>`). Unknown keys are ignored by the bundle, not validated. `chi bundle list` names the bundles; each bundle's keys are in `docs/bundles.md` / `docs/guardrails.md`.
81
80
 
82
81
  ```yaml
83
82
  bundles:
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: samagotchi-system
3
- version: 0.153.0
3
+ version: 0.154.0
4
4
  scope: system
5
5
  description: Default system memories — identity, self map, config modification protocol, memory guide and the delegated-session rules
6
6
  files:
7
7
  identity.md: sha256:29c2f7df9f6c5acbc5ec17000a4490741e55e426af15ca0d81c8706edd9bd027
8
- self_map.md: sha256:e695f78f858c29ccfef2ed6f9d6dd86d0ec067ed704a5a1956c01e53617948a1
9
- config_modification_protocol.md: sha256:8896d1d364ce835040f69b504518d43dbdf959b93fdd9b10f87a8de61cae84a8
8
+ self_map.md: sha256:709bd52710bf6c50334a89a10742ea1554c5985cc2d7c4e7571e526cc4479526
9
+ config_modification_protocol.md: sha256:f3f6545c93d59b624a2bf1766e26059520ffc7b236470dd9f1b51ab77312376d
10
10
  memory_guide.md: sha256:8b7727b939b9c04486ca59859561023dcc319a73e0f5b47b3fcf5741edaa69e2
11
11
  delegated.md: sha256:a0d6ddcd4a8be06526d964898611775fd251cfd9d145a9d16afaccaa503597bd
@@ -67,7 +67,7 @@
67
67
  `docs/bundle-authoring.md` in my source dir first.
68
68
 
69
69
  ## Where things live (relative to the source dir)
70
- - `docs/`: `cli.md` (flags, subcommands, REPL commands), `configuration.md` (config.yml keys,
70
+ - `docs/`: `cli.md` (flags, subcommands, the terminal's commands), `configuration.md` (config.yml keys,
71
71
  hosts, the server api), `hooks.md` (hook events table), `plugins.md` (plugin API), `bundles.md` (each shipped bundle: btw, mcp, cost-cap…),
72
72
  `guardrails.md`, `memory.md`, `sessions.md`. `README.md` is only the quickstart.
73
73
  - `lib/samagotchi/tools/<tool>.rb`: each tool's real limits and defaults (e.g. `execute.rb`,
@@ -47,7 +47,7 @@ module Samagotchi
47
47
  # What a continue does with the previous link's children: +move+ the
48
48
  # direct children whose subtree has an open node (they go to the new
49
49
  # link, their subtrees with them), +refuse+ the open ones it can't move
50
- # (SessionContinue::OpenChild, why: an older chi worker or a chi REPL).
50
+ # (SessionContinue::OpenChild, why: an older chi worker or a one-turn chi run).
51
51
  Plan = Data.define(:move, :refuse)
52
52
  # A move under way: from the session, the children moved, and those
53
53
  # already sent their note (so a move finished again notes no one twice).
@@ -87,14 +87,14 @@ module Samagotchi
87
87
  Plan.new(move: move, refuse: refuse)
88
88
  end
89
89
 
90
- # Why a child's live owner keeps it from moving: a chi REPL holds its
90
+ # Why a child's live owner keeps it from moving: a one-turn chi run holds its
91
91
  # own Session and never reads the override; a worker on an older chi
92
92
  # neither. A worker with no sidecar yet is starting, on the newest chi
93
93
  # installed (SessionManager.worker_command): it moves.
94
94
  # @return [String, nil]
95
95
  def unmovable(child_id, state_dir:)
96
96
  link = WorkerLink.resolve(Session.session_dir(child_id, state_dir: state_dir), unlink: false)
97
- return "open in a chi REPL; close it there" if link.owner&.tui?
97
+ return "#{OwnerLock::TUI_BUSY}; let it finish" if link.owner&.tui?
98
98
  return nil if !link.live? || link.features.include?(FEATURE)
99
99
 
100
100
  "older chi worker#{" #{link.version}" if link.version}; restart it"
@@ -29,9 +29,9 @@ module Samagotchi
29
29
  # @!attribute mid_turn [Symbol, #call] what a line of it does while a
30
30
  # turn runs (MID_TURN): a symbol, or a lambda line (stripped) → one
31
31
  # @!attribute local [Boolean] the UI runs it; completion and help only
32
- # @!attribute uis [Array<Symbol>, nil] a local entry's UIs (:repl,
33
- # :attached, :web) for completion and /help; nil means every UI
34
- # (for a local entry, every terminal)
32
+ # @!attribute uis [Array<Symbol>, nil] a local entry's UIs (:attached,
33
+ # :web) for completion and /help; nil means every UI (for a local
34
+ # entry, the terminal)
35
35
  # @!attribute match [#call] line (stripped) → whether it is this command
36
36
  # @!attribute handler [Proc, nil] runs it; SessionCommands instance_execs
37
37
  # a built-in's with the line, and calls a bundle's with the text
@@ -165,8 +165,8 @@ module Samagotchi
165
165
  end
166
166
 
167
167
  # @return [Entry, nil] the first entry the UI runs itself (local) that
168
- # matches +line+, whatever its uis (a UI answers the others' too:
169
- # the REPL's /detach note)
168
+ # matches +line+, whatever its uis (a UI answers the others' too,
169
+ # with a note)
170
170
  def lookup_local(line)
171
171
  text = line.to_s.strip
172
172
  @entries.find { |entry| entry.local && entry.match?(text) }
@@ -190,7 +190,7 @@ module Samagotchi
190
190
  end
191
191
  end
192
192
 
193
- # @param ui [Symbol] :repl or :attached
193
+ # @param ui [Symbol] :attached
194
194
  # @return [Array<String>] the /names Tab offers in +ui+, sorted
195
195
  def completions(ui)
196
196
  @entries.select { |entry| entry.slash? && entry.in_ui?(ui) }.map(&:name).uniq.sort
@@ -3,11 +3,10 @@
3
3
  require_relative "delivery"
4
4
 
5
5
  module Samagotchi
6
- # Where a line typed at a terminal's prompt goes, for both terminals (the
7
- # REPL's TerminalUI#steer_line / #run_input_line, the attached
8
- # AttachedLoop#submit). It only classifies; each UI keeps what it does
9
- # with the answer (the REPL's mid-turn command policy and image lines, the
10
- # attached TUI's continue offer and question). The web's composer.js
6
+ # Where a line typed at a terminal's prompt goes (AttachedLoop#submit; a
7
+ # -p prompt's /cut or /queue: TerminalUI#run). It only classifies; each
8
+ # UI keeps what it does with the answer (the attached TUI's continue
9
+ # offer and question). The web's composer.js
11
10
  # routeComposerLine decides the same; spec/fixtures/composer_routes.json
12
11
  # holds the cases both answer (docs/testing.md).
13
12
  #