samagotchi 0.155.0 → 0.157.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 (234) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +57 -1
  3. data/bin/chi +2 -1
  4. data/docs/architecture.md +22 -13
  5. data/docs/bundles.md +3 -1
  6. data/docs/cli.md +9 -8
  7. data/docs/configuration.md +36 -4
  8. data/docs/guardrails.md +9 -5
  9. data/docs/hooks.md +11 -5
  10. data/docs/internals/anthropic.md +1 -1
  11. data/docs/internals/attached-context.md +3 -2
  12. data/docs/internals/background-tasks.md +1 -1
  13. data/docs/internals/bridge.md +83 -6
  14. data/docs/internals/bundles.md +28 -18
  15. data/docs/internals/context-telemetry.md +4 -3
  16. data/docs/internals/engine.md +641 -937
  17. data/docs/internals/llm-context-forget.md +8 -3
  18. data/docs/internals/objectives.md +2 -2
  19. data/docs/internals/participants.md +3 -2
  20. data/docs/internals/prompt-caching.md +4 -1
  21. data/docs/internals/remote-sessions.md +8 -3
  22. data/docs/internals/web-frontend.md +3 -2
  23. data/docs/internals/working-set.md +2 -2
  24. data/docs/memory.md +13 -2
  25. data/docs/plugins.md +35 -2
  26. data/docs/sessions.md +11 -11
  27. data/docs/sub-agent.md +17 -2
  28. data/docs/system-one.md +3 -2
  29. data/docs/testing.md +53 -2
  30. data/lib/samagotchi/answer_command.rb +2 -1
  31. data/lib/samagotchi/archive_store.rb +4 -3
  32. data/lib/samagotchi/bootstrap/config_writer.rb +1 -1
  33. data/lib/samagotchi/bootstrap_command.rb +3 -2
  34. data/lib/samagotchi/bridge/card_places.rb +10 -4
  35. data/lib/samagotchi/bridge/card_store.rb +3 -0
  36. data/lib/samagotchi/bridge/turn_accumulator.rb +109 -9
  37. data/lib/samagotchi/bridge/turn_drawings.rb +265 -0
  38. data/lib/samagotchi/bridge.rb +31 -6
  39. data/lib/samagotchi/broadcast/recipients.rb +8 -8
  40. data/lib/samagotchi/broadcast/triage_model.rb +1 -1
  41. data/lib/samagotchi/broadcast_command.rb +3 -3
  42. data/lib/samagotchi/bundle_command/check.rb +18 -0
  43. data/lib/samagotchi/bundle_command/try/sandbox.rb +2 -2
  44. data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
  45. data/lib/samagotchi/bundle_command.rb +5 -2
  46. data/lib/samagotchi/bundles/known-names/manifest.yml +4 -7
  47. data/lib/samagotchi/bundles/known-names/{hooks/known_names.rb → plugin.rb} +17 -0
  48. data/lib/samagotchi/bundles/source-links/manifest.yml +4 -7
  49. data/lib/samagotchi/bundles/source-links/{hooks/source_links.rb → plugin.rb} +23 -4
  50. data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
  51. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  52. data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
  53. data/lib/samagotchi/child_move.rb +13 -57
  54. data/lib/samagotchi/child_reports.rb +2 -2
  55. data/lib/samagotchi/children_status.rb +15 -21
  56. data/lib/samagotchi/cli/parent_wait.rb +9 -23
  57. data/lib/samagotchi/config.rb +20 -112
  58. data/lib/samagotchi/context_command.rb +1 -2
  59. data/lib/samagotchi/context_sources.rb +5 -4
  60. data/lib/samagotchi/context_window.rb +9 -12
  61. data/lib/samagotchi/desktop/macos.rb +1 -1
  62. data/lib/samagotchi/effective_model.rb +57 -22
  63. data/lib/samagotchi/engine/context_judge.rb +19 -11
  64. data/lib/samagotchi/engine/messages.rb +1 -1
  65. data/lib/samagotchi/engine/models.rb +5 -2
  66. data/lib/samagotchi/engine/objectives.rb +29 -11
  67. data/lib/samagotchi/engine/plugins.rb +1 -1
  68. data/lib/samagotchi/engine/recap.rb +8 -4
  69. data/lib/samagotchi/engine/side_requests.rb +21 -11
  70. data/lib/samagotchi/engine/toolkit.rb +10 -9
  71. data/lib/samagotchi/engine/turn_end.rb +1 -1
  72. data/lib/samagotchi/engine/turn_helpers.rb +7 -7
  73. data/lib/samagotchi/engine/verify.rb +1 -1
  74. data/lib/samagotchi/engine.rb +43 -12
  75. data/lib/samagotchi/events.rb +3 -2
  76. data/lib/samagotchi/extension_load.rb +6 -3
  77. data/lib/samagotchi/guardrails/targets.rb +24 -9
  78. data/lib/samagotchi/guardrails_command.rb +1 -1
  79. data/lib/samagotchi/host_address.rb +52 -0
  80. data/lib/samagotchi/host_config.rb +72 -1
  81. data/lib/samagotchi/host_model.rb +2 -6
  82. data/lib/samagotchi/host_registry.rb +58 -46
  83. data/lib/samagotchi/kernel_loop.rb +53 -124
  84. data/lib/samagotchi/llm/anthropic_wire.rb +2 -2
  85. data/lib/samagotchi/llm/chat_loop.rb +40 -21
  86. data/lib/samagotchi/llm/native_backend.rb +1 -1
  87. data/lib/samagotchi/llm_context_apply.rb +54 -1
  88. data/lib/samagotchi/llm_context_notice.rb +1 -1
  89. data/lib/samagotchi/llm_context_strategy.rb +29 -67
  90. data/lib/samagotchi/llm_context_view.rb +9 -0
  91. data/lib/samagotchi/memory_bundle/asset_installer.rb +71 -76
  92. data/lib/samagotchi/memory_bundle/builder.rb +2 -1
  93. data/lib/samagotchi/memory_bundle/file_actions.rb +193 -0
  94. data/lib/samagotchi/memory_bundle/file_digest.rb +13 -0
  95. data/lib/samagotchi/memory_bundle/file_state.rb +152 -0
  96. data/lib/samagotchi/memory_bundle/installed_copy.rb +2 -1
  97. data/lib/samagotchi/memory_bundle/installed_manifest.rb +8 -1
  98. data/lib/samagotchi/memory_bundle/installer.rb +185 -169
  99. data/lib/samagotchi/memory_bundle/manifest.rb +14 -4
  100. data/lib/samagotchi/memory_bundle/merger.rb +69 -65
  101. data/lib/samagotchi/memory_bundle/plugin_ref.rb +6 -5
  102. data/lib/samagotchi/memory_bundle/profile.rb +5 -5
  103. data/lib/samagotchi/memory_bundle/provenance.rb +15 -1
  104. data/lib/samagotchi/memory_bundle/remerge.rb +25 -12
  105. data/lib/samagotchi/memory_bundle/shipped_update.rb +51 -6
  106. data/lib/samagotchi/memory_bundle/status.rb +14 -20
  107. data/lib/samagotchi/memory_bundle/system_bundle.rb +24 -6
  108. data/lib/samagotchi/memory_bundle/trash.rb +23 -8
  109. data/lib/samagotchi/memory_bundle/uninstaller.rb +16 -11
  110. data/lib/samagotchi/memory_usage.rb +1 -1
  111. data/lib/samagotchi/model_lookup.rb +75 -0
  112. data/lib/samagotchi/model_price.rb +4 -10
  113. data/lib/samagotchi/model_profile.rb +7 -6
  114. data/lib/samagotchi/model_settings.rb +37 -2
  115. data/lib/samagotchi/models_command.rb +1 -1
  116. data/lib/samagotchi/note_command.rb +2 -3
  117. data/lib/samagotchi/objectives_audit.rb +4 -3
  118. data/lib/samagotchi/participant_mode.rb +2 -1
  119. data/lib/samagotchi/participants.rb +2 -2
  120. data/lib/samagotchi/plugin/api.rb +41 -6
  121. data/lib/samagotchi/plugin/context.rb +1 -1
  122. data/lib/samagotchi/plugin/loader.rb +33 -10
  123. data/lib/samagotchi/plugin/sessions.rb +3 -6
  124. data/lib/samagotchi/plugin/tool_spec.rb +5 -3
  125. data/lib/samagotchi/project_env.rb +1 -1
  126. data/lib/samagotchi/reasoning_history.rb +14 -20
  127. data/lib/samagotchi/recap_place.rb +27 -0
  128. data/lib/samagotchi/reminder_carry.rb +1 -1
  129. data/lib/samagotchi/reply_wait/liveness.rb +38 -0
  130. data/lib/samagotchi/reply_wait/poll.rb +44 -15
  131. data/lib/samagotchi/reply_wait/target.rb +113 -0
  132. data/lib/samagotchi/reply_wait.rb +22 -2
  133. data/lib/samagotchi/reprefill_meter.rb +45 -0
  134. data/lib/samagotchi/review_command.rb +1 -1
  135. data/lib/samagotchi/sampling_settings.rb +9 -16
  136. data/lib/samagotchi/self_report.rb +36 -37
  137. data/lib/samagotchi/send_command.rb +17 -23
  138. data/lib/samagotchi/session.rb +370 -445
  139. data/lib/samagotchi/session_access/local.rb +12 -11
  140. data/lib/samagotchi/session_access/remote.rb +22 -6
  141. data/lib/samagotchi/session_archive_command.rb +2 -2
  142. data/lib/samagotchi/session_chain.rb +7 -6
  143. data/lib/samagotchi/session_continue.rb +6 -5
  144. data/lib/samagotchi/session_delete_command.rb +1 -1
  145. data/lib/samagotchi/session_field.rb +133 -0
  146. data/lib/samagotchi/session_inbox.rb +49 -1
  147. data/lib/samagotchi/session_manager/delivery.rb +254 -0
  148. data/lib/samagotchi/session_manager/emptiness.rb +139 -0
  149. data/lib/samagotchi/session_manager/lifecycle.rb +274 -0
  150. data/lib/samagotchi/session_manager/listing.rb +118 -0
  151. data/lib/samagotchi/session_manager/removal.rb +190 -0
  152. data/lib/samagotchi/session_manager/spawn.rb +242 -0
  153. data/lib/samagotchi/session_manager/worker_command.rb +157 -0
  154. data/lib/samagotchi/session_manager.rb +28 -1541
  155. data/lib/samagotchi/session_metrics.rb +8 -5
  156. data/lib/samagotchi/session_paths.rb +57 -0
  157. data/lib/samagotchi/session_retention.rb +2 -2
  158. data/lib/samagotchi/session_row.rb +200 -0
  159. data/lib/samagotchi/session_setup.rb +15 -12
  160. data/lib/samagotchi/sessions_command.rb +11 -129
  161. data/lib/samagotchi/share_command.rb +1 -1
  162. data/lib/samagotchi/starting_marker.rb +65 -0
  163. data/lib/samagotchi/steer_cut_after.rb +7 -14
  164. data/lib/samagotchi/system_one/recorder.rb +49 -0
  165. data/lib/samagotchi/terminal_ui/attached_loop.rb +6 -26
  166. data/lib/samagotchi/terminal_ui/exit_lines.rb +1 -1
  167. data/lib/samagotchi/terminal_ui/join_replay.rb +86 -8
  168. data/lib/samagotchi/terminal_ui/local_link.rb +2 -2
  169. data/lib/samagotchi/terminal_ui.rb +5 -7
  170. data/lib/samagotchi/testing/install.rb +2 -1
  171. data/lib/samagotchi/testing/sessions.rb +1 -1
  172. data/lib/samagotchi/thinking.rb +16 -21
  173. data/lib/samagotchi/token_usage.rb +1 -1
  174. data/lib/samagotchi/tool_activity.rb +10 -4
  175. data/lib/samagotchi/tool_declarations.rb +2 -2
  176. data/lib/samagotchi/tool_output_cut.rb +17 -0
  177. data/lib/samagotchi/tool_response.rb +5 -3
  178. data/lib/samagotchi/tool_runner.rb +25 -5
  179. data/lib/samagotchi/toolkit_command.rb +1 -1
  180. data/lib/samagotchi/tools/delegate.rb +55 -45
  181. data/lib/samagotchi/tools/delegate_relay.rb +5 -5
  182. data/lib/samagotchi/tools/delegate_result.rb +5 -5
  183. data/lib/samagotchi/tools/delegate_wait.rb +41 -17
  184. data/lib/samagotchi/tools/index_hint.rb +14 -0
  185. data/lib/samagotchi/tools/list_sessions.rb +7 -7
  186. data/lib/samagotchi/tools/registry.rb +28 -11
  187. data/lib/samagotchi/tools/send_note.rb +4 -6
  188. data/lib/samagotchi/tools/tool_index.rb +52 -51
  189. data/lib/samagotchi/tools/tool_surface.rb +22 -8
  190. data/lib/samagotchi/turn_context.rb +2 -1
  191. data/lib/samagotchi/turn_stages/context_judge.rb +38 -36
  192. data/lib/samagotchi/turn_stages/objectives.rb +56 -52
  193. data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +13 -17
  194. data/lib/samagotchi/turn_stages/verify_anchors.rb +9 -10
  195. data/lib/samagotchi/turn_stages/warmup.rb +14 -15
  196. data/lib/samagotchi/update_command.rb +11 -7
  197. data/lib/samagotchi/verify_fresh.rb +7 -11
  198. data/lib/samagotchi/version.rb +1 -1
  199. data/lib/samagotchi/vision_support.rb +11 -14
  200. data/lib/samagotchi/web/app.rb +45 -28
  201. data/lib/samagotchi/web/message_display.rb +57 -7
  202. data/lib/samagotchi/web/participant_access.rb +3 -3
  203. data/lib/samagotchi/web/public/activity.js +16 -7
  204. data/lib/samagotchi/web/public/app.js +29 -8
  205. data/lib/samagotchi/web/public/bubbles.js +8 -3
  206. data/lib/samagotchi/web/public/cards_place.js +15 -0
  207. data/lib/samagotchi/web/public/commands.js +5 -1
  208. data/lib/samagotchi/web/public/data.js +4 -2
  209. data/lib/samagotchi/web/public/history_shape.js +220 -0
  210. data/lib/samagotchi/web/public/recap_stamp.js +17 -0
  211. data/lib/samagotchi/web/public/reload_check.js +71 -0
  212. data/lib/samagotchi/web/public/stage_model.js +7 -5
  213. data/lib/samagotchi/web/public/stage_view.js +7 -3
  214. data/lib/samagotchi/web/public/stream_handlers.js +8 -2
  215. data/lib/samagotchi/web/public/timing.js +43 -2
  216. data/lib/samagotchi/web/public/turn_block.js +215 -30
  217. data/lib/samagotchi/web/public/turn_drawing.js +43 -0
  218. data/lib/samagotchi/web/public/turn_events.js +10 -1
  219. data/lib/samagotchi/web/public/turn_pieces.js +29 -0
  220. data/lib/samagotchi/web/reload_check_route.rb +95 -0
  221. data/lib/samagotchi/web/server.rb +4 -2
  222. data/lib/samagotchi/web/session_access_routes.rb +6 -6
  223. data/lib/samagotchi/web/session_control.rb +4 -4
  224. data/lib/samagotchi/web/session_create.rb +3 -3
  225. data/lib/samagotchi/web/session_hub.rb +9 -17
  226. data/lib/samagotchi/web/session_show.rb +67 -5
  227. data/lib/samagotchi/web/session_summary.rb +39 -28
  228. data/lib/samagotchi/worker.rb +83 -32
  229. data/lib/samagotchi/worker_boot.rb +184 -0
  230. data/lib/samagotchi/worker_link.rb +3 -0
  231. data/lib/samagotchi/worker_signals.rb +1 -1
  232. data/lib/samagotchi/worker_task_wakes.rb +2 -2
  233. metadata +32 -4
  234. data/lib/samagotchi/send_command/wait_target.rb +0 -85
data/docs/testing.md CHANGED
@@ -75,6 +75,47 @@ spec/engine_turn_golden_spec.rb`, then read `git diff spec/fixtures/turn_goldens
75
75
  to `tmp/turn_goldens/<golden>.actual` and names the `diff` to run. Changes to the turn path (engine, kernel loop, chat loop,
76
76
  tool runner, gate, guardrail wiring, system prompt, tool declarations, registry) pick it in `script/affected-specs`.
77
77
 
78
+ The model-setting characterizations (`spec/engine_effective_model_characterization_spec.rb`,
79
+ `spec/engine_session_model_settings_characterization_spec.rb`, `spec/web/app_session_settings_characterization_spec.rb`
80
+ over `spec/fixtures/effective_model/`, and `spec/model_setting_precedence_characterization_spec.rb`'s ladders over
81
+ `spec/fixtures/model_setting_precedence/`) pin the settings on the code as it was before the `ModelLookup` refactor
82
+ (`UPDATE_GOLDENS=1` regenerates them after an intended change only). They are retired after their refactor: the
83
+ next round folds the first three into one wiring example each in `effective_model_spec.rb`,
84
+ `session_model_settings_spec.rb` and `session_setting_spec.rb`, and the ladders the round after into the setting
85
+ modules' and `model_lookup_spec.rb`.
86
+
87
+ The session file goldens (`spec/session_file_goldens_spec.rb`, `spec/fixtures/session_goldens/`) pin the bytes `Session#save`
88
+ writes: every field away from its default, a new session, a newer chi's file, an old chi's file in a git repo, every key
89
+ present as `null`, the plain writers' raw values, a running turn's checkpoint and the scrub (the `*_in.json` files are
90
+ hand-written inputs, never regenerated); and the file each path that makes or rewrites a session writes (a fork, a
91
+ continue, a delegate child, a plain spawn, a review, a Verify fresh child, a resume, a respawn, an archive), with
92
+ `carry_matrix.json`, what each keeps of its source. They are pinned before Session's fields became one table and are kept
93
+ as the file format's pins, not retired: a diff is a change to every session file on disk, so only a commit that means to
94
+ change the format regenerates them (`UPDATE_GOLDENS=1 bundle exec rspec spec/session_file_goldens_spec.rb`).
95
+
96
+ `UPDATE_GOLDENS=1` is the characterization specs' variable (the prompt and turn goldens above use `UPDATE_PROMPTS`):
97
+ the model-setting ones above, the session file goldens, claude-peer's goldens and the bundle-state corpus below. Regenerate only for an intended
98
+ change, then read the diff.
99
+
100
+ The bundle-state corpus (`spec/samagotchi/memory_bundle/file_state_corpus/`, `spec/fixtures/bundle_states/`) pins what
101
+ every bundle file state does under every command that reads or writes it. Each case in `memory_cases.yml` (a memory:
102
+ owned, edited, behind, missing, dropped, the user's own, another bundle's too…) and `asset_cases.yml` (a rule, the
103
+ plugin, a script, a hook) is written directly as a starting tree, in the 0.155 and the 0.149 record shape, and run
104
+ through install, `--force`, `--dry-run`, upgrade and its flags, chi update's plan, Remerge, the system bundle's restore,
105
+ uninstall and status. A cell holds only what a user or the next command sees (the bytes, the trash, base and checksum,
106
+ the mark, the summary's word, status's label, the warnings), never the Installer's result strings. `expect/*.yml` are
107
+ generated (`UPDATE_GOLDENS=1 bundle exec rspec spec/samagotchi/memory_bundle/file_state_corpus/`, which also writes
108
+ `tmp/bundle_states/review.<file>.txt`, every cell in words); a mismatch names the case, command and shape and writes
109
+ `tmp/bundle_states/<file>.yml.actual` to diff. A regeneration asserts nothing, so run the folder again without
110
+ `UPDATE_GOLDENS`: that run checks the new cells against `cases_spec.rb`, which holds what every regeneration keeps. No
111
+ user bytes (neither the bundle's text nor what the record vouches for, its checksum first) are changed or removed without
112
+ a copy in the trash first; the two record shapes give one answer except on a marked case (an allowlist entry needs a
113
+ note); and for every asset, chi update's plan lists as replaced exactly the files the upgrade copies to the trash. To stay
114
+ under 3 s, `upgrade_spec.rb` runs the 0.149 shape for marked cases only (`MARKED_ONLY_149`), where the shapes can differ. A regenerated cell is
115
+ reviewed: a refactor that turns the corpus red stops and asks, and only a commit that means to change a cell
116
+ regenerates, its changed cells read one by one; a cell that looks wrong today carries a `divergent:` tag and a note in
117
+ its case.
118
+
78
119
  The web specs run in plain node. One that loads lit-html, itself or through a module it imports (`turn_block.js`,
79
120
  `turn_html.js`, `card.js`, `copy.js`, `images.js`, `scope.js`, `command_view.js`, `diff_view.js`, `read_view.js`,
80
121
  `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`,
@@ -124,7 +165,15 @@ Each level owns its own share; a detail is asserted once, at the lowest level th
124
165
  - **e2e** is for real-browser behaviour (layout, focus, reload, two tabs), one happy path per Ruby↔web mechanism and
125
166
  a few journeys. Before adding a test, extend an existing journey's step; a variant goes to node, a Ruby↔web shape
126
167
  variant to a shared fixture (`spec/fixtures/composer_routes.json`). A journey names its stages with `test.step`
127
- (the report names the one that failed: web.spec.js's notifications). The `lan` project's chi web is on the LAN only
168
+ (the report names the one that failed: web.spec.js's notifications). A turn test ends with
169
+ `expectSameAfterReload` (spec/e2e/support/history_shape.js): the history's shape live, then after a reload, the
170
+ same (live is the truth). Its allowlist is `history_shape.js ACCEPTED`, each entry with its reason; a test names
171
+ the entries its scenario has (only deliberate ones are left). A drawn turn's variants are node's: the shared
172
+ scenarios in `spec/shared/turn_drawings/` (written from `spec/support/turn_drawing_scenarios.rb`;
173
+ `UPDATE_TURN_DRAWINGS=1 bundle exec rspec spec/bridge/turn_drawings_spec.rb spec/web/session_show_drawings_spec.rb`
174
+ rewrites their lines and show after an intended change) feed `turn_drawing_parity.test.js`, which draws each one
175
+ live (the real stream handlers and stage) and from its drawing and compares the shapes with no allow list. The
176
+ `lan` project's chi web is on the LAN only
128
177
  when the machine has a private IPv4: without one its LAN tests skip and the participant review journey runs on
129
178
  loopback.
130
179
 
@@ -254,6 +303,7 @@ any other file requires it, so a spec file run alone still finds its requires. E
254
303
  | `lock_holder.rb` | *helper*: `spawn_lock_holder(session_dir)`, a stand-in worker process holding a session's owner lock, which says so on a pipe |
255
304
  | `integration_server.rb` | *helper*: the live model server `:integration` examples use, from `SAMAGOTCHI_INTEGRATION_*` ([Integration specs](#integration-specs)) |
256
305
  | `mcp_bundle.rb` | The shipped mcp bundle's path and the fake MCP server its specs start (`MCP_FAKE_COMMAND`, no RubyGems) |
306
+ | `model_setting_ladders.rb` | shared context "model setting ladders": per setting a config, env and CLI with every layer set, peeled top down (`peel`), against `spec/fixtures/model_setting_precedence/` |
257
307
  | `no_console.rb` | Loaded first (`ruby -r`) by spawned bin/chi children: `IO.console` is nil, so output never wraps at a terminal's width |
258
308
  | `recording_surface.rb` | `RecordingSurface`: a `TerminalUI::Surface` that records commits and slots instead of drawing |
259
309
  | `remote_backend.rb` | `RemoteBackend`: `SessionAccess::Remote` against chi web's Rack app in this process (no socket, LAN token gate) |
@@ -334,7 +384,8 @@ files it changed (`lib/samagotchi/foo/bar.rb` → `spec/foo/bar_spec.rb`, `bar_*
334
384
  `lib/samagotchi/foo/bar/baz.rb` → `spec/foo/bar_baz_spec.rb`), the specs that read
335
385
  docs or source as text (its `RULES`: add one when a full run catches what the names missed) and the golden of a changed
336
386
  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
387
+ words the attached TUI prints, pick its `event_renderer` and `attached_loop` specs by a rule; `memory_bundle/*.rb`,
388
+ `bundle_command.rb`, `memory_paths.rb`, `model_overlay.rb` and the corpus's fixtures pick the bundle-state corpus). Changes to `tool_declarations.rb` and `system_prompt.rb` also pick the
338
389
  effective-model, request, prompt-snapshot and full system-prompt goldens because they hash the request's prompt and
339
390
  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
340
391
  `toolkit.rb`; then one beside the file, so `plugin/context.rb` picks `spec/samagotchi/plugin/*context*` before
@@ -138,7 +138,8 @@ module Samagotchi
138
138
  end
139
139
  # Answered here, or no longer open (answered elsewhere, or a newer
140
140
  # question): what comes next either way.
141
- wait_for_reply(id, cursor: start.cursor, baseline: start.baseline, timeout: options[:timeout], turn: start.own_turn)
141
+ wait_for_reply(id, target: ReplyWait::Target.answer(start),
142
+ timeout: options[:timeout])
142
143
  end
143
144
 
144
145
  # The labels to send. A number is an option's place (1-based); an
@@ -4,6 +4,7 @@ require "json"
4
4
  require "fileutils"
5
5
  require "time"
6
6
  require_relative "atomic_file"
7
+ require_relative "session_paths"
7
8
 
8
9
  require_relative "session"
9
10
 
@@ -46,7 +47,7 @@ module Samagotchi
46
47
 
47
48
  # @return [Boolean] whether it was archived (and is not now)
48
49
  def self.unarchive(session_id, state_dir:)
49
- return false unless archived?(Session.session_dir(session_id, state_dir: state_dir))
50
+ return false unless archived?(SessionPaths.session_dir(session_id, state_dir: state_dir))
50
51
 
51
52
  write(session_id, { "unarchived_at" => Time.now.iso8601(3) }, state_dir: state_dir)
52
53
  end
@@ -63,9 +64,9 @@ module Samagotchi
63
64
  end
64
65
 
65
66
  def self.write(session_id, record, state_dir:)
66
- return false unless File.exist?(Session.session_file(session_id, state_dir: state_dir))
67
+ return false unless File.exist?(SessionPaths.session_file(session_id, state_dir: state_dir))
67
68
 
68
- dir = Session.session_dir(session_id, state_dir: state_dir)
69
+ dir = SessionPaths.session_dir(session_id, state_dir: state_dir)
69
70
  FileUtils.mkdir_p(dir)
70
71
  path = File.join(dir, FILE)
71
72
  AtomicFile.write(path, JSON.generate(record))
@@ -304,7 +304,7 @@ module Samagotchi
304
304
 
305
305
  def written_ok?(real, expected, name)
306
306
  parse(File.read(real)) == expected &&
307
- ConfigFile.hosts_config(env: @env, path: real).key?(name.downcase)
307
+ HostConfig.all(env: @env, path: real).key?(name.downcase)
308
308
  rescue StandardError
309
309
  false
310
310
  end
@@ -3,6 +3,7 @@
3
3
  require "rbconfig"
4
4
  require_relative "config"
5
5
  require_relative "model_profile"
6
+ require_relative "host_address"
6
7
  require_relative "host_registry"
7
8
  require_relative "host_keys"
8
9
  require_relative "chi_home"
@@ -342,7 +343,7 @@ module Samagotchi
342
343
  # The "loading the model?" hint fits a local server (a llama.cpp loading
343
344
  # weights); a remote provider's slow answer is the network, not a load.
344
345
  def loading_hint?(result)
345
- !HostRegistry.remote_address?(result.candidate.scheme, result.candidate.host)
346
+ !HostAddress.remote_address?(result.candidate.scheme, result.candidate.host)
346
347
  end
347
348
 
348
349
  # 0 written, 1 written but not as planned, nil with @exit on a refusal.
@@ -489,7 +490,7 @@ module Samagotchi
489
490
  when :skipped then "skipped (#{result.error})"
490
491
  else "failed (#{result.error})"
491
492
  end
492
- @stdout.puts("system bundle: #{text}")
493
+ @stdout.puts("system bundle: #{[text, *result.warnings].join("; ")}")
493
494
  result.status != :failed
494
495
  end
495
496
 
@@ -18,6 +18,15 @@ module Samagotchi
18
18
  # comes after (a prompt's turn_id, a line's opening_id), which the page
19
19
  # finds by its bubble, not by counting. Pure: no Engine, no file.
20
20
  class CardPlaces
21
+ # An opening's stamp: its turn_id (a prompt's, a wake note's), else its
22
+ # own opening_id (/continue no's reason line); nil for neither (a line
23
+ # an older chi wrote). The recap's too (RecapPlace).
24
+ # @return [String, nil]
25
+ def self.stamp(message)
26
+ id = message[:turn_id] || message["turn_id"] || message[:opening_id] || message["opening_id"]
27
+ id.is_a?(String) && !id.empty? ? id : nil
28
+ end
29
+
21
30
  # @param turn_log [Array<String>] the turn ids, oldest first
22
31
  # @param messages [Array<Hash>] the messages the page draws
23
32
  def initialize(turn_log, messages)
@@ -114,10 +123,7 @@ module Samagotchi
114
123
  position >= 0 ? @last_at_or_before[position] : nil
115
124
  end
116
125
 
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
126
+ def stamp_of(message) = self.class.stamp(message)
121
127
 
122
128
  def idless_run(opening)
123
129
  @ids.drop(opening + 1).take_while(&:nil?).size
@@ -147,6 +147,9 @@ module Samagotchi
147
147
  stamp = places&.opening_of(entry)
148
148
  out[:opening_id] = stamp if stamp
149
149
  out[:current] = current
150
+ # Whose row it is (its turn's id, +of_turn+): the page leaves a drawn
151
+ # turn's own rows and cards to its drawing (Bridge::TurnDrawings).
152
+ out[:of_turn] = entry[:turn_id] if shown[:in_turn] && entry[:turn_id]
150
153
  out[:during] = true if entry[:during]
151
154
  out[:after_failed] = true if entry[:after_failed] && entry[:turn_id] == @failed_turn
152
155
  out[:label] = "from #{entry[:command]}" if entry[:command]
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../events"
4
4
  require_relative "../kernel_loop"
5
+ require_relative "../log"
5
6
  require_relative "../steer"
6
7
  require_relative "../tool_output_cut"
7
8
  require_relative "../tool_row_fields"
@@ -23,10 +24,17 @@ module Samagotchi
23
24
  # its head and tail: a reload's cap, so a joined row is the row a
24
25
  # reload draws
25
26
  # @param clock [#call] monotonic seconds: a tool call's duration
27
+ # @param on_turn_end [#call, nil] (turn, end_event): the turn's final
28
+ # fold (a deep copy, #current_turn's shape without elapsed_ms) and its
29
+ # end event, at the end event, before the turn is cleared (the Bridge's
30
+ # TurnDrawings#append). It runs under this accumulator's mutex and the
31
+ # SessionObserver's lock: it must never call back in here. An error is
32
+ # logged and the fold goes on.
26
33
  def initialize(max_output_chars: ToolOutputCut::WEB_PREVIEW_CHARS,
27
- clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) })
34
+ clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }, on_turn_end: nil)
28
35
  @max_output_chars = max_output_chars
29
36
  @clock = clock
37
+ @on_turn_end = on_turn_end
30
38
  @mutex = Mutex.new
31
39
  @turn = nil
32
40
  # When each running tool call started, by [iteration, call_index].
@@ -47,13 +55,14 @@ module Samagotchi
47
55
 
48
56
  # @return [Hash, nil] a copy of the turn in progress: prompt, turn_id, origin,
49
57
  # continue, at (its wall time), ordered parts (generation / thinking / text / tool /
50
- # input / steer / reminder / notice), pending_question and the last event_seq folded
58
+ # input / steer / reminder / notice / card), pending_question and the last event_seq folded
51
59
  # in. A finished tool part keeps the live row's action and duration_ms
52
60
  # too, a running one the time it has run so far (elapsed_ms), so a
53
61
  # join times its row from its start. A notice part holds the event
54
62
  # of one of the turn's rows (TurnNotice: a hook's notice, an
55
63
  # empty-answer retry, a question and its answer), which a UI replays
56
- # through its live handler.
64
+ # through its live handler. A card part is a card the turn showed in
65
+ # its step (in_turn), its latest version in the place of its first.
57
66
  def current_turn
58
67
  @mutex.synchronize do
59
68
  next nil unless @turn
@@ -132,10 +141,13 @@ module Samagotchi
132
141
  # the live activity: action, tool, params, status), a merged_input
133
142
  # (the one synthetic type) per merged prompt, a steer-only
134
143
  # pending_input_merged per steer, a reminder_injected, and a notice
135
- # part's own event.
144
+ # part's own event. Card parts only with +cards+ (a finished turn's
145
+ # drawing, chi web's unfold of it): a running turn's join gets its cards
146
+ # from CardStore's snapshot.
136
147
  # @param started_at [String, nil] the turn's start, on turn_started
148
+ # @param cards [Boolean] replay card parts as their card events
137
149
  # @return [Array<Hash>] symbol keys and types; [] with no turn
138
- def self.replay_events(turn, started_at: nil)
150
+ def self.replay_events(turn, started_at: nil, cards: false)
139
151
  return [] unless turn
140
152
 
141
153
  started = { type: :turn_started, prompt: turn[:prompt], origin: turn[:origin], continue: !!turn[:continue] }
@@ -175,14 +187,66 @@ module Samagotchi
175
187
  when "steer"
176
188
  close_text.call
177
189
  events << { type: :pending_input_merged, count: 0, content: nil, steers: [{ source: part[:source], text: part[:text] }] }
190
+ when "card"
191
+ next unless cards && part[:event].is_a?(Hash)
192
+
193
+ close_text.call
194
+ events << part[:event].merge(type: :card, in_turn: true)
178
195
  end
179
196
  end
180
197
  events
181
198
  end
182
199
 
200
+ # The page's unfold of a session view (chi web's show sends it as
201
+ # `replay`; turn_events.js snapshotEvents was its JS twin): the turn in
202
+ # progress as #replay_events has it, its turn_started with what the
203
+ # page also reads (its wall time `at`, the run's `cut_after`, and every
204
+ # author key, a pair turn's participant_mode too), then a turn_enqueued
205
+ # per prompt queued behind it and a command_queued per command waiting
206
+ # for its end (spec/shared/turn_snapshot.json pins both sides). The TUI
207
+ # keeps #replay_events.
208
+ # @param current_turn [Hash, nil] #current_turn's shape, symbol keys
209
+ # (a snapshot's: .from_wire)
210
+ # @param queued [Array<Hash>] #queued's entries
211
+ # @param queued_commands [Array<Hash>] #queued_commands' entries
212
+ # @return [Array<Hash>] symbol keys and types
213
+ def self.join_events(current_turn:, queued: [], queued_commands: [], started_at: nil)
214
+ events = replay_events(current_turn, started_at: started_at)
215
+ if (started = events.first)
216
+ started[:at] = current_turn[:at] if current_turn[:at].is_a?(String)
217
+ started[:cut_after] = current_turn[:cut_after] unless current_turn[:cut_after].nil?
218
+ Steer::AUTHOR_KEYS.each { |key| started.delete(key) }
219
+ Steer::AUTHOR_KEYS.each { |key| started[key] = current_turn[key] if current_turn[key] } if current_turn[:author]
220
+ end
221
+ Array(queued).each do |entry|
222
+ enqueued = { type: :turn_enqueued, enqueued_id: entry[:enqueued_id], client_id: entry[:client_id], prompt: entry[:prompt] }
223
+ enqueued[:delivery] = entry[:delivery] if entry[:delivery]
224
+ enqueued[:author] = entry[:author] if entry[:author]
225
+ events << with_images(enqueued, entry[:images])
226
+ end
227
+ Array(queued_commands).each do |entry|
228
+ command = { type: :command_queued, command_id: entry[:command_id], client_id: entry[:client_id], line: entry[:line],
229
+ waits: "turn_end" }
230
+ command[:author] = entry[:author] if entry[:author]
231
+ events << command
232
+ end
233
+ events
234
+ end
235
+
236
+ # A Bridge snapshot's part (JSON: string keys) as the accumulator's own
237
+ # shapes: symbol keys all the way down, values as they are.
238
+ def self.from_wire(value)
239
+ case value
240
+ when Hash then value.to_h { |key, item| [key.is_a?(String) ? key.to_sym : key, from_wire(item)] }
241
+ when Array then value.map { |item| from_wire(item) }
242
+ else value
243
+ end
244
+ end
245
+
183
246
  def self.tool_replay_events(part)
184
247
  call = { iteration: part[:iteration], call_index: part[:call_index], tool: part[:tool] }
185
248
  call[:label] = part[:label] if part[:label]
249
+ call[:tool_id] = part[:tool_id] if part[:tool_id]
186
250
  # Its row fields (ToolRowFields), each in its place: title and, for
187
251
  # an alias's call, the name the model used, also in the activity.
188
252
  title = part.slice(*ToolRowFields::ACTIVITY_KEYS).compact
@@ -296,6 +360,7 @@ module Samagotchi
296
360
 
297
361
  fold_turn_event(event)
298
362
  if Events::TURN_END.include?(type)
363
+ turn_ended(event)
299
364
  @turn = nil
300
365
  @merged_origins = []
301
366
  @working_set = nil
@@ -326,6 +391,8 @@ module Samagotchi
326
391
  part = { kind: "tool", iteration: event[:iteration], call_index: event[:call_index],
327
392
  tool: event[:tool], params: event[:params], status: "running" }
328
393
  part[:label] = event[:label] if event[:label]
394
+ # The id its output is saved under (ToolIds; not a row field).
395
+ part[:tool_id] = event[:tool_id] if event[:tool_id]
329
396
  ToolRowFields::KEYS.each { |key| part[key] = event[key] if event[key] }
330
397
  parts << part
331
398
  @tool_started_at[[event[:iteration], event[:call_index]]] = @clock.call
@@ -342,16 +409,26 @@ module Samagotchi
342
409
  # Only System 1's auto-allow is drawn on the row (the web's label).
343
410
  guardrail = event.dig(:activity, :guardrail)
344
411
  tool[:guardrail] = guardrail if guardrail.is_a?(Hash) && guardrail[:decided_by] == "system one"
345
- tool[:output] = capped ? ToolOutputCut.preview(output, cap: @max_output_chars, tool: event[:tool]) : output.dup
412
+ # The event's web preview (ToolRunner: the reload's cut) when it
413
+ # has one; an older worker's event is cut here the same way.
414
+ tool[:output] = if event[:preview] then event[:preview].to_s.dup
415
+ elsif capped then ToolOutputCut.preview(output, cap: @max_output_chars, tool: event[:tool])
416
+ else output.dup
417
+ end
346
418
  tool[:output_truncated] = capped || !!event[:output_truncated]
347
419
  tool[:images] = event[:images] if event[:images]
348
420
  tool[:diff] = event[:diff] if event[:diff]
349
421
  action = event.dig(:activity, :action)
350
422
  tool[:action] = action.to_s if action
351
- # As the live row times it (EventRenderer): from tool_call_started,
352
- # less an approval wait.
423
+ # The event's (ToolRunner's: the figure every UI shows); without
424
+ # one (an older worker's event), as the live row times it: from
425
+ # tool_call_started, less an approval wait.
353
426
  started_at = @tool_started_at.delete([event[:iteration], event[:call_index]])
354
- tool[:duration_ms] = [((@clock.call - started_at) * 1000) - event[:waited_ms].to_f, 0].max.round if started_at
427
+ if event[:duration_ms].is_a?(Integer)
428
+ tool[:duration_ms] = event[:duration_ms]
429
+ elsif started_at
430
+ tool[:duration_ms] = [((@clock.call - started_at) * 1000) - event[:waited_ms].to_f, 0].max.round
431
+ end
355
432
  when :pending_input_merged
356
433
  # A steer-only merge (count 0) has no user part: the origins stay
357
434
  # for the user lines' own merge.
@@ -366,6 +443,8 @@ module Samagotchi
366
443
  end
367
444
  when :working_set_updated
368
445
  @working_set = { items: event[:items], at: event[:at] }
446
+ when :card
447
+ fold_card(parts, event) if event[:in_turn]
369
448
  when :reminder_injected
370
449
  parts << { kind: "reminder", reminders: Array(event[:reminders]).map(&:dup) }
371
450
  when :question_requested
@@ -382,6 +461,27 @@ module Samagotchi
382
461
  parts << { kind: "notice", event: TurnNotice.slice(event) } if TurnNotice.notice?(event)
383
462
  end
384
463
 
464
+ # The card fields a UI draws (CardStore keeps the same), a copy.
465
+ CARD_FIELDS = %i[id source title body level actions].freeze
466
+ private_constant :CARD_FIELDS
467
+
468
+ # A card the turn shows in its step; one with an earlier card's id
469
+ # replaces that part where it was.
470
+ def fold_card(parts, event)
471
+ card = { kind: "card", id: event[:id], event: Marshal.load(Marshal.dump(event.slice(*CARD_FIELDS))) }
472
+ index = parts.index { |part| part[:kind] == "card" && part[:id] == card[:id] }
473
+ index ? parts[index] = card : parts << card
474
+ end
475
+
476
+ # The turn's final fold to the hook (#initialize's on_turn_end).
477
+ def turn_ended(event)
478
+ return unless @on_turn_end
479
+
480
+ @on_turn_end.call(Marshal.load(Marshal.dump(@turn)), event)
481
+ rescue StandardError => e
482
+ Log.warn(:bridge, "turn_end_hook_failed", error: "#{e.class}: #{e.message}")
483
+ end
484
+
385
485
  def append_text(iteration, kind, text)
386
486
  return if text.nil? || text.empty?
387
487