samagotchi 0.153.0 → 0.154.1

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 (226) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -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 +82 -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 +21 -8
  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 +4 -2
  23. data/docs/internals/remote-sessions.md +7 -7
  24. data/docs/internals/web-frontend.md +2 -2
  25. data/docs/internals/web-push.md +1 -1
  26. data/docs/memory.md +2 -2
  27. data/docs/plugins.md +23 -28
  28. data/docs/sessions.md +24 -24
  29. data/docs/sub-agent.md +2 -2
  30. data/docs/system-one.md +1 -1
  31. data/docs/testing.md +31 -6
  32. data/lib/samagotchi/answer_text.rb +33 -0
  33. data/lib/samagotchi/bridge/card_places.rb +56 -7
  34. data/lib/samagotchi/bridge/card_store.rb +24 -9
  35. data/lib/samagotchi/bridge.rb +52 -32
  36. data/lib/samagotchi/bridge_client.rb +19 -14
  37. data/lib/samagotchi/broadcast/recipients.rb +2 -2
  38. data/lib/samagotchi/broadcast/triage.rb +1 -1
  39. data/lib/samagotchi/broadcast_command.rb +5 -5
  40. data/lib/samagotchi/bundle_command/try/command.rb +1 -1
  41. data/lib/samagotchi/bundle_command/try/run.rb +1 -1
  42. data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
  43. data/lib/samagotchi/bundle_command.rb +2 -2
  44. data/lib/samagotchi/bundles/btw/manifest.yml +2 -2
  45. data/lib/samagotchi/bundles/btw/plugin.rb +1 -1
  46. data/lib/samagotchi/bundles/coordinator/manifest.yml +2 -2
  47. data/lib/samagotchi/bundles/coordinator/plugin.rb +3 -2
  48. data/lib/samagotchi/bundles/decision-card/manifest.yml +4 -4
  49. data/lib/samagotchi/bundles/decision-card/plugin.rb +1 -9
  50. data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
  51. data/lib/samagotchi/bundles/github-pr/manifest.yml +2 -2
  52. data/lib/samagotchi/bundles/github-pr/plugin.rb +3 -3
  53. data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
  54. data/lib/samagotchi/bundles/skills/plugin.rb +3 -2
  55. data/lib/samagotchi/bundles/system/config_modification_protocol.md +3 -4
  56. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  57. data/lib/samagotchi/bundles/system/self_map.md +1 -1
  58. data/lib/samagotchi/child_move.rb +3 -3
  59. data/lib/samagotchi/commands/registry.rb +6 -6
  60. data/lib/samagotchi/composer_route.rb +4 -5
  61. data/lib/samagotchi/config.rb +14 -12
  62. data/lib/samagotchi/context_candidate_choice.rb +1 -1
  63. data/lib/samagotchi/context_candidates.rb +4 -3
  64. data/lib/samagotchi/continue_offer.rb +1 -1
  65. data/lib/samagotchi/debug_log.rb +1 -1
  66. data/lib/samagotchi/degrade.rb +75 -0
  67. data/lib/samagotchi/desktop/macos/ChiRunner.swift +3 -3
  68. data/lib/samagotchi/engine/cards.rb +87 -0
  69. data/lib/samagotchi/engine/context_judge.rb +138 -0
  70. data/lib/samagotchi/engine/events.rb +131 -0
  71. data/lib/samagotchi/engine/guardrails.rb +87 -0
  72. data/lib/samagotchi/engine/hooks.rb +119 -0
  73. data/lib/samagotchi/engine/messages.rb +124 -0
  74. data/lib/samagotchi/engine/model_settings.rb +127 -0
  75. data/lib/samagotchi/engine/models.rb +261 -0
  76. data/lib/samagotchi/engine/objectives.rb +116 -0
  77. data/lib/samagotchi/engine/participants.rb +86 -0
  78. data/lib/samagotchi/engine/plugins.rb +208 -0
  79. data/lib/samagotchi/engine/questions.rb +98 -0
  80. data/lib/samagotchi/engine/recap.rb +130 -0
  81. data/lib/samagotchi/engine/session.rb +131 -0
  82. data/lib/samagotchi/engine/side_requests.rb +135 -0
  83. data/lib/samagotchi/engine/steering.rb +134 -0
  84. data/lib/samagotchi/engine/toolkit.rb +269 -0
  85. data/lib/samagotchi/engine/turn_end.rb +180 -0
  86. data/lib/samagotchi/engine/turn_helpers.rb +160 -0
  87. data/lib/samagotchi/engine/verify.rb +153 -0
  88. data/lib/samagotchi/engine.rb +62 -2646
  89. data/lib/samagotchi/engine_snapshots.rb +9 -8
  90. data/lib/samagotchi/grants.rb +575 -0
  91. data/lib/samagotchi/guardrail_wiring.rb +2 -2
  92. data/lib/samagotchi/guardrails/context.rb +2 -3
  93. data/lib/samagotchi/guardrails/parent_approvals.rb +2 -2
  94. data/lib/samagotchi/guardrails/parent_policy.rb +2 -2
  95. data/lib/samagotchi/hook_routes.rb +3 -3
  96. data/lib/samagotchi/idle_recap.rb +7 -6
  97. data/lib/samagotchi/idle_reminders.rb +3 -4
  98. data/lib/samagotchi/iteration_limit.rb +2 -2
  99. data/lib/samagotchi/kernel_loop.rb +13 -13
  100. data/lib/samagotchi/launch_mode.rb +13 -32
  101. data/lib/samagotchi/level_setting_command.rb +3 -2
  102. data/lib/samagotchi/llm/chat_loop.rb +3 -1
  103. data/lib/samagotchi/llm_context_command.rb +3 -2
  104. data/lib/samagotchi/log.rb +7 -7
  105. data/lib/samagotchi/log_line.rb +1 -1
  106. data/lib/samagotchi/log_path.rb +1 -1
  107. data/lib/samagotchi/note_command.rb +1 -1
  108. data/lib/samagotchi/note_delivery.rb +18 -8
  109. data/lib/samagotchi/objectives.rb +24 -3
  110. data/lib/samagotchi/objectives_check.rb +8 -27
  111. data/lib/samagotchi/objectives_command.rb +4 -69
  112. data/lib/samagotchi/objectives_edit.rb +87 -0
  113. data/lib/samagotchi/output_formatter.rb +7 -1
  114. data/lib/samagotchi/owner_lock.rb +7 -1
  115. data/lib/samagotchi/parent_answers_command.rb +5 -4
  116. data/lib/samagotchi/participant_exposure.rb +42 -0
  117. data/lib/samagotchi/participant_mode.rb +1 -1
  118. data/lib/samagotchi/participant_name.rb +1 -1
  119. data/lib/samagotchi/participants.rb +17 -17
  120. data/lib/samagotchi/participants_watch.rb +1 -1
  121. data/lib/samagotchi/plugin/api.rb +3 -2
  122. data/lib/samagotchi/plugin/context.rb +21 -7
  123. data/lib/samagotchi/plugin/peer.rb +2 -1
  124. data/lib/samagotchi/plugin/sessions.rb +6 -4
  125. data/lib/samagotchi/plugin_tasks.rb +1 -2
  126. data/lib/samagotchi/private_files.rb +29 -0
  127. data/lib/samagotchi/project_env.rb +2 -2
  128. data/lib/samagotchi/question_desk.rb +8 -7
  129. data/lib/samagotchi/recap_command.rb +2 -1
  130. data/lib/samagotchi/recap_setup.rb +1 -1
  131. data/lib/samagotchi/recap_store.rb +1 -1
  132. data/lib/samagotchi/redact.rb +86 -0
  133. data/lib/samagotchi/reminder_queue.rb +2 -2
  134. data/lib/samagotchi/remote_attach_command.rb +1 -1
  135. data/lib/samagotchi/reply_wait/poll.rb +2 -2
  136. data/lib/samagotchi/review_start.rb +1 -1
  137. data/lib/samagotchi/run_options.rb +6 -6
  138. data/lib/samagotchi/self_report.rb +42 -39
  139. data/lib/samagotchi/send_command/busy.rb +1 -1
  140. data/lib/samagotchi/send_command.rb +4 -1
  141. data/lib/samagotchi/session.rb +11 -2
  142. data/lib/samagotchi/session_access/contract.rb +3 -3
  143. data/lib/samagotchi/session_access/local.rb +9 -9
  144. data/lib/samagotchi/session_access/remote.rb +17 -7
  145. data/lib/samagotchi/session_access.rb +4 -4
  146. data/lib/samagotchi/session_archive_command.rb +2 -2
  147. data/lib/samagotchi/session_chain.rb +8 -6
  148. data/lib/samagotchi/session_commands.rb +45 -48
  149. data/lib/samagotchi/session_continue.rb +1 -1
  150. data/lib/samagotchi/session_delete_command.rb +4 -4
  151. data/lib/samagotchi/session_manager.rb +67 -49
  152. data/lib/samagotchi/session_metrics.rb +2 -2
  153. data/lib/samagotchi/session_model_settings.rb +3 -2
  154. data/lib/samagotchi/session_participants.rb +1 -1
  155. data/lib/samagotchi/session_retention.rb +27 -7
  156. data/lib/samagotchi/sessions_command.rb +4 -4
  157. data/lib/samagotchi/share_command.rb +10 -10
  158. data/lib/samagotchi/steer.rb +13 -2
  159. data/lib/samagotchi/stream_event.rb +22 -0
  160. data/lib/samagotchi/system_prompt.rb +154 -145
  161. data/lib/samagotchi/task_watch.rb +1 -1
  162. data/lib/samagotchi/terminal_ui/attach_launcher.rb +72 -10
  163. data/lib/samagotchi/terminal_ui/attached_loop/composer.rb +12 -9
  164. data/lib/samagotchi/terminal_ui/attached_loop/exit_commands.rb +16 -0
  165. data/lib/samagotchi/terminal_ui/attached_loop/questions.rb +2 -3
  166. data/lib/samagotchi/terminal_ui/attached_loop.rb +17 -8
  167. data/lib/samagotchi/terminal_ui/attached_view.rb +1 -11
  168. data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -5
  169. data/lib/samagotchi/terminal_ui/formatting.rb +7 -7
  170. data/lib/samagotchi/terminal_ui/input_support.rb +17 -22
  171. data/lib/samagotchi/terminal_ui/join_replay.rb +16 -12
  172. data/lib/samagotchi/terminal_ui/line_reader.rb +2 -2
  173. data/lib/samagotchi/terminal_ui/live_region.rb +2 -2
  174. data/lib/samagotchi/terminal_ui/log_tail.rb +135 -0
  175. data/lib/samagotchi/terminal_ui/one_shot_sink.rb +2 -2
  176. data/lib/samagotchi/terminal_ui/plain_surface.rb +1 -1
  177. data/lib/samagotchi/terminal_ui/question_prompt.rb +4 -5
  178. data/lib/samagotchi/terminal_ui/reline_seam.rb +2 -12
  179. data/lib/samagotchi/terminal_ui/status_row.rb +6 -14
  180. data/lib/samagotchi/terminal_ui.rb +142 -1387
  181. data/lib/samagotchi/title_command.rb +2 -2
  182. data/lib/samagotchi/tool_runner.rb +15 -16
  183. data/lib/samagotchi/toolkit_command.rb +2 -1
  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/send_note.rb +2 -1
  189. data/lib/samagotchi/turn_endings.rb +2 -2
  190. data/lib/samagotchi/turn_flow.rb +11 -7
  191. data/lib/samagotchi/update_hint.rb +2 -2
  192. data/lib/samagotchi/version.rb +1 -1
  193. data/lib/samagotchi/web/app.rb +42 -51
  194. data/lib/samagotchi/web/message_display.rb +7 -0
  195. data/lib/samagotchi/web/participant_access.rb +2 -2
  196. data/lib/samagotchi/web/participant_review.rb +5 -5
  197. data/lib/samagotchi/web/participant_stream_filter.rb +8 -8
  198. data/lib/samagotchi/web/participant_view.rb +2 -82
  199. data/lib/samagotchi/web/participants_admin.rb +1 -1
  200. data/lib/samagotchi/web/public/block_host.js +35 -8
  201. data/lib/samagotchi/web/public/block_renderers.css +9 -5
  202. data/lib/samagotchi/web/public/card_stamps.js +27 -0
  203. data/lib/samagotchi/web/public/cards_place.js +5 -3
  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 +1 -1
  207. data/lib/samagotchi/web/public/format.js +4 -3
  208. data/lib/samagotchi/web/public/turn_html.js +8 -4
  209. data/lib/samagotchi/web/push/store.rb +1 -1
  210. data/lib/samagotchi/web/session_access_routes.rb +13 -4
  211. data/lib/samagotchi/web/session_control.rb +12 -8
  212. data/lib/samagotchi/web/session_create.rb +1 -1
  213. data/lib/samagotchi/web/session_hub.rb +2 -2
  214. data/lib/samagotchi/web/session_show.rb +6 -2
  215. data/lib/samagotchi/web/session_summary.rb +1 -1
  216. data/lib/samagotchi/worker.rb +103 -9
  217. data/lib/samagotchi/worker_link.rb +3 -3
  218. data/lib/samagotchi/working_set_command.rb +3 -2
  219. data/lib/samagotchi.rb +0 -1
  220. metadata +31 -7
  221. data/lib/samagotchi/pending_input_queue.rb +0 -55
  222. data/lib/samagotchi/terminal_ui/between_turns.rb +0 -138
  223. data/lib/samagotchi/terminal_ui/repl_input.rb +0 -152
  224. data/lib/samagotchi/web/grants.rb +0 -575
  225. data/lib/samagotchi/web/participant_exposure.rb +0 -44
  226. 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.
@@ -53,6 +60,21 @@ paths (replace them with placeholders), no PATH tools (the suite pins `rg` as fo
53
60
  that sets PATH to test the lookup is tagged `:real_rg`), no Ruby-version-dependent `inspect` (a Hash's or an
54
61
  exception's text changes between Ruby versions: write the string yourself).
55
62
 
63
+ The whole-turn goldens (`spec/engine_turn_golden_spec.rb`, `spec/fixtures/turn_goldens/`) run a real
64
+ `Engine#run_turn` (its own KernelLoop, ChatLoop, ToolRunner, Gate and GuardrailWiring) over `FakeProviderServer` on
65
+ the native (llama.cpp `/completion`, Qwen tool calls), `api: openai` and `api: anthropic` wires: a turn of two tool
66
+ iterations (a muted `memory_read` and a `read`, then a `write` a config rule asks about, allowed through the question
67
+ flow), a `before_turn` hook switching the model (the request names the new model with the old one's sampling), and a
68
+ review participant's turn whose `write` the review boundary refuses. Each golden pins every request (the chi-set
69
+ headers, the body in its key order, the native prompt as text), the turn's events (keys sorted; the context-status
70
+ estimates and clock values masked), each `before_tool_call` payload, the approval asked and the session's messages.
71
+ System 1, the toolkit suggestion, the context judge, the recap and the warm-up are off by config, so the order is the
72
+ turn's own. The arch P3 refactors (TurnContext, turn stages, ToolSurface) keep them byte-identical: a phase that
73
+ changes one stops and asks. An intended change elsewhere: `UPDATE_PROMPTS=1 bundle exec rspec
74
+ spec/engine_turn_golden_spec.rb`, then read `git diff spec/fixtures/turn_goldens/`; a failure writes what the run got
75
+ to `tmp/turn_goldens/<golden>.actual` and names the `diff` to run. Changes to the turn path (engine, kernel loop, chat loop,
76
+ tool runner, gate, guardrail wiring, system prompt, tool declarations, registry) pick it in `script/affected-specs`.
77
+
56
78
  The web specs run in plain node. One that loads lit-html, itself or through a module it imports (`turn_block.js`,
57
79
  `turn_html.js`, `card.js`, `copy.js`, `images.js`, `scope.js`, `command_view.js`, `diff_view.js`, `read_view.js`,
58
80
  `question_cards.js`, `stream_handlers.js`, `stage_view.js`, `bubbles.js`, `cards_place.js`, the chips, `info_bar.js`, `info_bar_model.js`, `lists_view.js`, `context_chips.js`, `families_view.js`,
@@ -74,11 +96,11 @@ back (`t.mock` restores itself). A test that sets off a module's timer (a flash,
74
96
  node running until it fires (a 4 s one made the run 6 s for 2 s of work). One file:
75
97
  `node --test spec/web/public/NAME.test.js`.
76
98
 
77
- Where a composer line goes is decided twice, in Ruby (`ComposerRoute`, both terminals) and in the web
99
+ Where a composer line goes is decided twice, in Ruby (`ComposerRoute`, the attached terminal) and in the web
78
100
  (`composer.js#routeComposerLine`). `spec/fixtures/composer_routes.json` holds the cases both answer: rspec
79
101
  (`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
102
+ either router adds its row there; a row where a UI differs on purpose (or a found divergence) carries a `web`
103
+ override and a `note`. The Ruby side runs each row with every core command (a new one fails its
82
104
  `CORE_ROUTES` check until listed there); a row's names that core lacks stand for plugin commands.
83
105
 
84
106
  ## Levels
@@ -174,6 +196,9 @@ whose value is dropped, an example's last line included, `expect(wait_until {
174
196
  `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
197
  `eventually`, `join!` and `pop!` as an example's expectation (`RSpec/NoExpectationExample`), so a wait can be an
176
198
  example's last line. What the cops can't see, `script/affected-specs` hints at (`timing:`, below).
199
+ One lib cop sits beside them: `Samagotchi/SilentRescue` (a `rescue` of StandardError whose body is only a literal,
200
+ `nil`, `[]`…: use `Degrade`, or tag a right silence `# degrade-ok: why`; docs/internals/engine.md, Degrading). The
201
+ files that had such rescues when it came are excluded in `.rubocop.yml`: that list only shrinks.
177
202
 
178
203
  ### Flakes
179
204
 
@@ -308,8 +333,8 @@ A builder runs these while working, not the full suite: the specs the change tou
308
333
  files it changed (`lib/samagotchi/foo/bar.rb` → `spec/foo/bar_spec.rb`, `bar_*_spec.rb`…;
309
334
  `lib/samagotchi/foo/bar/baz.rb` → `spec/foo/bar_baz_spec.rb`), the specs that read
310
335
  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
336
+ file's layer (any `foo/*.rb` → `spec/foo_golden_spec.rb` when there is one; `turn_flow.rb` and `turn_note.rb`, whose
337
+ 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
338
  effective-model, request, prompt-snapshot and full system-prompt goldens because they hash the request's prompt and
314
339
  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
340
  `toolkit.rb`; then one beside the file, so `plugin/context.rb` picks `spec/samagotchi/plugin/*context*` before
@@ -0,0 +1,33 @@
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
+ # The template opened the block, so the text starts inside it.
17
+ TEMPLATE_OPENED_THINK = %r{\A.*?</think>}m
18
+
19
+ module_function
20
+
21
+ # @param text [String, nil]
22
+ # @return [String] "" when nothing was visible
23
+ def visible(text)
24
+ text = text.to_s.scrub
25
+ template_opened = !text.include?("<think>")
26
+ gemma = ToolCallParser::Gemma.new(ModelProfile.gemma4)
27
+ text = gemma.strip_thought(text).gsub(OutputFormatter::QWEN_THINK_RE, "")
28
+ text = text.sub(TEMPLATE_OPENED_THINK, "") if template_opened
29
+ text = ToolCallParser.strip_calls(gemma.strip_tool_calls(text), "<tool_call>", "</tool_call>")
30
+ OutputFormatter.strip_markup(text) # drops a thought cut off before it closed
31
+ end
32
+ end
33
+ end
@@ -9,16 +9,22 @@ module Samagotchi
9
9
  # the recap share (Steer.turn_opening?); the store's turn log only orders
10
10
  # the ids. So a turn with no opening (a reminder, a Continue, a failed or
11
11
  # rolled-back one, a prompt stopped before the model) counts nothing, and
12
- # an opening with no id (/continue no's reason, Qwen's recovery line) counts
13
- # as it does on the page. Nothing orders such an opening against an entry, so
14
- # an entry says how many it came after (+past_idless+, #idless_after). Pure:
15
- # no Engine, no file.
12
+ # an opening with no turn id (/continue no's reason) counts as it does on
13
+ # the page. Nothing orders such an opening against an entry, so
14
+ # an entry says how many it came after (+past_idless+, #idless_after), and
15
+ # which one that is when it has an id (+after_opening+, #after_opening).
16
+ #
17
+ # The same place as a stamp (#opening_of): the id of the opening the entry
18
+ # comes after (a prompt's turn_id, a line's opening_id), which the page
19
+ # finds by its bubble, not by counting. Pure: no Engine, no file.
16
20
  class CardPlaces
17
21
  # @param turn_log [Array<String>] the turn ids, oldest first
18
22
  # @param messages [Array<Hash>] the messages the page draws
19
23
  def initialize(turn_log, messages)
20
24
  positions = turn_log.each_with_index.to_h
21
- @ids = Array(messages).select { |message| Steer.turn_opening?(message) }.map { |message| message[:turn_id] || message["turn_id"] }
25
+ openings = Array(messages).select { |message| Steer.turn_opening?(message) }
26
+ @ids = openings.map { |message| message[:turn_id] || message["turn_id"] }
27
+ @stamps = openings.map { |message| stamp_of(message) }
22
28
  @openings = @ids.size
23
29
  @opened = @ids.compact.to_set
24
30
  @places = @ids.map { |id| positions[id] }
@@ -45,13 +51,44 @@ module Samagotchi
45
51
  entry[:base_since].to_i + since
46
52
  end
47
53
 
54
+ # The opening +entry+ comes after, as #turns_since counts: its id (a
55
+ # prompt's turn_id, a line's opening_id), the page's stamp. nil when it
56
+ # comes before every opening, when that opening has no id (a line an
57
+ # older chi wrote), or for an entry from an older file (+base_since+):
58
+ # the page counts those (turns_since).
59
+ # @param entry [Hash] a store's entry: :turn_id, :past_idless, :after_opening, :base_since
60
+ # @return [String, nil]
61
+ def opening_of(entry)
62
+ return nil if entry[:base_since]
63
+
64
+ at_or_before = counted_from(entry[:turn_id])
65
+ return nil unless at_or_before
66
+
67
+ run = at_or_before..(at_or_before + idless_run(at_or_before))
68
+ # The line it saved it came after, while that line is in the run (as
69
+ # the count past it, the two save together); else the count's.
70
+ after = entry[:after_opening] && @stamps.index(entry[:after_opening])
71
+ @stamps[after && run.cover?(after) ? after : [at_or_before + entry[:past_idless].to_i, run.end].min]
72
+ end
73
+
74
+ # The id of the last opening with no turn id right after the one
75
+ # +turn_id+ counts from (#idless_after's run), nil when there is none or
76
+ # it has no id either.
77
+ # @return [String, nil]
78
+ def after_opening(turn_id)
79
+ at_or_before = counted_from(turn_id)
80
+ return nil unless at_or_before
81
+
82
+ run = idless_run(at_or_before)
83
+ run.positive? ? @stamps[at_or_before + run] : nil
84
+ end
85
+
48
86
  # The openings with no id right after the one +turn_id+ counts from: what
49
87
  # an entry shown now has come after. A rollback can only shorten the run, so
50
88
  # a count read back against it never eats a later turn.
51
89
  # @return [Integer]
52
90
  def idless_after(turn_id)
53
- position = @positions.fetch(turn_id, -1)
54
- at_or_before = position >= 0 ? @last_at_or_before[position] : nil
91
+ at_or_before = counted_from(turn_id)
55
92
  at_or_before ? idless_run(at_or_before) : 0
56
93
  end
57
94
 
@@ -70,6 +107,18 @@ module Samagotchi
70
107
 
71
108
  private
72
109
 
110
+ # The last opening whose turn is at or before +turn_id+'s in the log
111
+ # (an index into the openings), nil for none.
112
+ def counted_from(turn_id)
113
+ position = @positions.fetch(turn_id, -1)
114
+ position >= 0 ? @last_at_or_before[position] : nil
115
+ end
116
+
117
+ def stamp_of(message)
118
+ id = message[:turn_id] || message["turn_id"] || message[:opening_id] || message["opening_id"]
119
+ id.is_a?(String) && !id.empty? ? id : nil
120
+ end
121
+
73
122
  def idless_run(opening)
74
123
  @ids.drop(opening + 1).take_while(&:nil?).size
75
124
  end
@@ -26,7 +26,9 @@ module Samagotchi
26
26
  # holds the id of the turn it belongs to or follows (+turn_id+: the running
27
27
  # turn's, else the last one's, nil before any) and lists with +turns_since+,
28
28
  # the turns drawn after it, counted from the messages the page draws
29
- # (CardPlaces, with the turn log the store keeps), and +current+ for a card
29
+ # (CardPlaces, with the turn log the store keeps), +opening_id+, the same
30
+ # place as a stamp (the id of the opening it comes after, which the page
31
+ # finds by its bubble; none where only the count can say), and +current+ for a card
30
32
  # of the turn running now. A card that isn't the turn's but comes while one
31
33
  # runs (an anytime command's, such as /btw) goes after that turn: its prompt
32
34
  # is already in the history. Listed against messages, an in-turn entry of a
@@ -70,7 +72,7 @@ module Samagotchi
70
72
  SAVED_ON = (%i[turn_started card hook_notice empty_answer_retry steer_cut llm_context_edited question_requested question_answered
71
73
  question_cancelled question_relay] + Events::TURN_END).freeze
72
74
  # What only the store reads: never listed.
73
- INTERNAL_FIELDS = %i[turn_id continued base_since past_idless during after_failed command].freeze
75
+ INTERNAL_FIELDS = %i[turn_id continued base_since past_idless after_opening during after_failed command].freeze
74
76
 
75
77
  # What a session's saved file lists, as a store started from it lists
76
78
  # (no turn running): the web's cards for a session no worker runs.
@@ -118,7 +120,8 @@ module Samagotchi
118
120
  # them (a body re-read, a trace): no turns_since:, nothing to tell a turn
119
121
  # that left nothing by.
120
122
  # @return [Array<Hash>] oldest first: the card's (or notice's) fields
121
- # plus current:, turns_since: with +messages+, earlier: true for one an
123
+ # plus current:, turns_since: and opening_id: (CardPlaces#opening_of,
124
+ # when it has one) with +messages+, earlier: true for one an
122
125
  # earlier worker saved, during: true for a card that came while the
123
126
  # running turn runs but isn't that turn's (it goes after the running
124
127
  # turn's prompt), and after_failed: true for one shown after the last
@@ -141,6 +144,8 @@ module Samagotchi
141
144
 
142
145
  out = shown.except(*INTERNAL_FIELDS)
143
146
  out[:turns_since] = places.turns_since(entry) if places
147
+ stamp = places&.opening_of(entry)
148
+ out[:opening_id] = stamp if stamp
144
149
  out[:current] = current
145
150
  out[:during] = true if entry[:during]
146
151
  out[:after_failed] = true if entry[:after_failed] && entry[:turn_id] == @failed_turn
@@ -255,7 +260,7 @@ module Samagotchi
255
260
  # A hand-edited or foreign file's entry goes, rather than break every later
256
261
  # listing of the session.
257
262
  def sound_place?(entry)
258
- return false unless entry[:turn_id].nil? || entry[:turn_id].is_a?(String)
263
+ return false unless [entry[:turn_id], entry[:after_opening]].all? { |id| id.nil? || id.is_a?(String) }
259
264
 
260
265
  entry.values_at(:base_since, :past_idless).compact.all? { |count| count.is_a?(Integer) && !count.negative? }
261
266
  end
@@ -315,8 +320,8 @@ module Samagotchi
315
320
  shown = @entries.select { |entry| entry[:turn_id] == @running_turn && !entry[:in_turn] }
316
321
  return if shown.empty? || !@messages
317
322
 
318
- run = CardPlaces.new(@turn_log, @messages.call).idless_after(@running_turn)
319
- shown.each { |entry| entry[:past_idless] = run if run.positive? }
323
+ places = CardPlaces.new(@turn_log, @messages.call)
324
+ shown.each { |entry| note_idless(entry, places) }
320
325
  end
321
326
 
322
327
  # The entry's anchor: the running turn, else the last one.
@@ -331,7 +336,7 @@ module Samagotchi
331
336
  # button's command replaced, which nobody typed).
332
337
  old = @entries[index]
333
338
  @entries[index] = card.except(:command).merge(updated: true, **old.slice(:in_turn, :turn_id, :during, :continued, :base_since,
334
- :past_idless, :after_failed, :command))
339
+ :past_idless, :after_opening, :after_failed, :command))
335
340
  else
336
341
  entry = card.merge(in_turn: event[:in_turn] ? true : false, turn_id: anchor)
337
342
  entry[:during] = true if @running_turn && !event[:in_turn]
@@ -393,8 +398,18 @@ module Samagotchi
393
398
  def note_past_idless(entry)
394
399
  return unless @messages
395
400
 
396
- run = CardPlaces.new(@turn_log, @messages.call).idless_after(entry[:turn_id])
397
- entry[:past_idless] = run if run.positive?
401
+ note_idless(entry, CardPlaces.new(@turn_log, @messages.call))
402
+ end
403
+
404
+ # How many openings with no turn id +entry+ came after, and the last
405
+ # one's own id when it has one (+after_opening+: its stamp, #list).
406
+ def note_idless(entry, places)
407
+ run = places.idless_after(entry[:turn_id])
408
+ return unless run.positive?
409
+
410
+ entry[:past_idless] = run
411
+ after = places.after_opening(entry[:turn_id])
412
+ entry[:after_opening] = after if after
398
413
  end
399
414
 
400
415
  # Which cap +entry+ counts against: :notice, :llm_context or :card.
@@ -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
@@ -187,6 +196,15 @@ module Samagotchi
187
196
  @mutex.synchronize { @open_streams - (client_id ? @streams_by_client[client_id] : 0) }
188
197
  end
189
198
 
199
+ # Streams held by the clients whose id starts with +prefix+
200
+ # (ClientId::TUI_PREFIX: the attached terminals, which a scratch worker
201
+ # counts, Worker#scratch_unattended?). A stream that named no client
202
+ # (a web tab's, through the chi web proxy) is none of them.
203
+ # @return [Integer]
204
+ def open_streams_of(prefix)
205
+ @mutex.synchronize { @streams_by_client.sum { |client_id, count| client_id.start_with?(prefix) ? count : 0 } }
206
+ end
207
+
190
208
  # Monotonic time of the last request, stream open or stream close. The
191
209
  # worker's idle exit counts from it. A connect that sends no request (the
192
210
  # sidecar liveness probe) doesn't count.
@@ -236,7 +254,7 @@ module Samagotchi
236
254
  @mutex.synchronize { @stopped = true }
237
255
  begin
238
256
  @server&.close
239
- rescue StandardError
257
+ rescue StandardError # degrade-ok: stopping: a server socket already closed is fine
240
258
  nil
241
259
  end
242
260
  wake_acceptor
@@ -254,7 +272,7 @@ module Samagotchi
254
272
  @mutex.synchronize { @stopped = true }
255
273
  begin
256
274
  @server&.close
257
- rescue StandardError
275
+ rescue StandardError # degrade-ok: stopping: a server socket already closed is fine
258
276
  nil
259
277
  end
260
278
  wake_acceptor
@@ -268,13 +286,13 @@ module Samagotchi
268
286
  # the web, which then queues the prompt again from the input file.
269
287
  await_answers(REQUEST_GRACE_SECONDS)
270
288
  remove_sidecar
271
- @connection_threads.each { |t| t.kill rescue nil }
289
+ @connection_threads.each { |t| t.kill rescue nil } # degrade-ok: stopping: a thread already gone is fine
272
290
  @connection_threads.clear
273
291
  # Its relay watchers (RelayWatcher) end with it.
274
292
  watchers = @mutex.synchronize { (@relay_watchers || []).tap { @relay_watchers = [] } }
275
293
  watchers.each { |thread| thread.kill.join(RELAY_WATCHER_JOIN_SECONDS) }
276
294
  @accept_thread&.join(2)
277
- [@wake_reader, @wake_writer].each { |io| io&.close rescue nil }
295
+ [@wake_reader, @wake_writer].each { |io| io&.close rescue nil } # degrade-ok: stopping: a closed pipe is fine
278
296
  nil
279
297
  end
280
298
 
@@ -399,9 +417,10 @@ module Samagotchi
399
417
  # O(1) and non-blocking.
400
418
  def capture_observer
401
419
  @capture_proc ||= proc do |event|
402
- @ring.push(seq: event[:event_seq], data: event)
403
- rescue StandardError
404
- nil
420
+ # Per event: the first failure warns, the rest log at debug.
421
+ Degrade.call(:bridge, "event_capture_failed") do
422
+ @ring.push(seq: event[:event_seq], data: event)
423
+ end
405
424
  end
406
425
  end
407
426
 
@@ -527,7 +546,7 @@ module Samagotchi
527
546
  Thread.current[:bridge_answering] = false
528
547
  begin
529
548
  io.close
530
- rescue StandardError
549
+ rescue StandardError # degrade-ok: the client may be gone already; closing is all that's left
531
550
  nil
532
551
  end
533
552
  end
@@ -555,6 +574,7 @@ module Samagotchi
555
574
  )
556
575
  @mutex.synchronize do
557
576
  @open_streams += 1
577
+ @streams_served += 1
558
578
  @streams_by_client[client_id] += 1 if client_id
559
579
  end
560
580
  begin
@@ -860,7 +880,7 @@ module Samagotchi
860
880
  end
861
881
  end
862
882
 
863
- # Dismiss the pending question (an empty answer, as in the REPL): the
883
+ # Dismiss the pending question (an empty answer at a terminal): the
864
884
  # tool returns without an answer and every UI gets :question_cancelled.
865
885
  # Only the question the client saw, and only while nobody has answered
866
886
  # it: Engine#cancel_question checks both under the question lock. A past
@@ -1425,44 +1445,44 @@ module Samagotchi
1425
1445
  # else fall back to the explicit `?from_seq=` query param used by clients
1426
1446
  # that remember their own cursor.
1427
1447
  def reconnect_cursor(headers, query)
1428
- cursor = headers["last-event-id"]
1429
- return cursor if cursor && !cursor.empty?
1448
+ Degrade.call(:bridge, "sse_cursor_unparsable") do
1449
+ cursor = headers["last-event-id"]
1450
+ return cursor if cursor && !cursor.empty?
1430
1451
 
1431
- return nil unless query
1452
+ return nil unless query
1432
1453
 
1433
- URI.decode_www_form(query).to_h["from_seq"]
1434
- rescue StandardError
1435
- nil
1454
+ URI.decode_www_form(query).to_h["from_seq"]
1455
+ end
1436
1456
  end
1437
1457
 
1438
1458
  # `?snapshot=1`: join with a snapshot frame instead of a replay.
1439
1459
  def snapshot_requested?(query)
1440
- return false unless query
1460
+ Degrade.call(:bridge, "snapshot_query_unparsable", fallback: false) do
1461
+ return false unless query
1441
1462
 
1442
- URI.decode_www_form(query).to_h["snapshot"].to_s == "1"
1443
- rescue StandardError
1444
- false
1463
+ URI.decode_www_form(query).to_h["snapshot"].to_s == "1"
1464
+ end
1445
1465
  end
1446
1466
 
1447
1467
  # `?raw=1`: events as the engine emitted them (a chunk's provider
1448
1468
  # payload), not as clients get them (ClientEvent). For debugging on the
1449
1469
  # worker's port: chi web never passes it on.
1450
1470
  def raw_requested?(query)
1451
- return false unless query
1471
+ Degrade.call(:bridge, "raw_query_unparsable", fallback: false) do
1472
+ return false unless query
1452
1473
 
1453
- URI.decode_www_form(query).to_h["raw"].to_s == "1"
1454
- rescue StandardError
1455
- false
1474
+ URI.decode_www_form(query).to_h["raw"].to_s == "1"
1475
+ end
1456
1476
  end
1457
1477
 
1458
1478
  # `?client_id=`: whose stream it is (see #open_streams_except).
1459
1479
  def stream_client_id(query)
1460
- return nil unless query
1480
+ Degrade.call(:bridge, "client_id_query_unparsable") do
1481
+ return nil unless query
1461
1482
 
1462
- id = URI.decode_www_form(query).to_h["client_id"].to_s
1463
- id.empty? ? nil : id
1464
- rescue StandardError
1465
- nil
1483
+ id = URI.decode_www_form(query).to_h["client_id"].to_s
1484
+ id.empty? ? nil : id
1485
+ end
1466
1486
  end
1467
1487
 
1468
1488
  def close_after_request?(headers)
@@ -1534,11 +1554,11 @@ module Samagotchi
1534
1554
  # same session may have written its own since (a file without an epoch
1535
1555
  # is only ever an older worker's).
1536
1556
  def remove_sidecar
1537
- return unless @server
1557
+ Degrade.call(:bridge, "sidecar_remove_failed") do
1558
+ return unless @server
1538
1559
 
1539
- File.unlink(WorkerSidecar.path(session_dir)) if WorkerSidecar.read(session_dir)&.epoch == @epoch.to_s
1540
- rescue StandardError
1541
- nil
1560
+ File.unlink(WorkerSidecar.path(session_dir)) if WorkerSidecar.read(session_dir)&.epoch == @epoch.to_s
1561
+ end
1542
1562
  end
1543
1563
 
1544
1564
  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"
@@ -308,12 +309,16 @@ module Samagotchi
308
309
  # older worker's 404 for a route it lacks from a refused connect, a
309
310
  # timeout or a 500.
310
311
  # @return [Array(Integer, Object)] the status and the parsed JSON body
311
- # (nil when it isn't JSON); [nil, nil] when no reply came
312
+ # (nil when it isn't JSON); [nil, nil] when no reply came (a refused
313
+ # connect, a timeout, a cut or garbled reply), and when anything else
314
+ # failed, which is logged
312
315
  def get(path)
313
- response = get_response(path)
314
- [response.status, response.json]
315
- rescue StandardError
316
- [nil, nil]
316
+ Degrade.call(:bridge, "get_failed", fallback: [nil, nil], path: path) do
317
+ response = get_response(path)
318
+ [response.status, response.json]
319
+ rescue SystemCallError, IOError
320
+ [nil, nil]
321
+ end
317
322
  end
318
323
 
319
324
  # GET /session/:id/<path>, preserving the raw response and raising on I/O.
@@ -329,17 +334,17 @@ module Samagotchi
329
334
  body = response.split("\r\n\r\n", 2)[1] || ""
330
335
  Response.new(status: status.to_i, body: body)
331
336
  ensure
332
- sock&.close rescue nil
337
+ sock&.close rescue nil # degrade-ok: done with the socket; one already closed is fine
333
338
  end
334
339
 
335
340
  # Monotonic SSE cursor of the live Engine, from GET /session/:id/state.
336
341
  # @return [Integer, nil]
337
342
  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
343
+ Degrade.call(:bridge, "event_seq_unreadable") do
344
+ state = get_json("state")
345
+ seq = state && state.dig("session_state_snapshot", "event_seq")
346
+ seq.nil? ? nil : seq.to_i
347
+ end
343
348
  end
344
349
 
345
350
  # Open GET /session/:id/stream and classify the response before the
@@ -358,7 +363,7 @@ module Samagotchi
358
363
  if connection
359
364
  sock, status, headers = connection
360
365
  unless !block_given? || yield(sock)
361
- sock.close rescue nil
366
+ sock.close rescue nil # degrade-ok: the stream is given up; a socket already closed is fine
362
367
  return StreamOpen::Unreachable.new(attempts: count)
363
368
  end
364
369
  return StreamOpen.classify(sock, status, headers)
@@ -389,7 +394,7 @@ module Samagotchi
389
394
  break
390
395
  end
391
396
  ensure
392
- sock&.close rescue nil
397
+ sock&.close rescue nil # degrade-ok: done with the socket; one already closed is fine
393
398
  end
394
399
  end
395
400
 
@@ -435,7 +440,7 @@ module Samagotchi
435
440
  body = read_body ? reply.split("\r\n\r\n", 2)[1] : nil
436
441
  Response.new(status: reply[STATUS_LINE, 1].to_i, body: body)
437
442
  ensure
438
- sock&.close rescue nil
443
+ sock&.close rescue nil # degrade-ok: done with the socket; one already closed is fine
439
444
  end
440
445
 
441
446
  # 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).