samagotchi 0.2.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 (243) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +43 -0
  3. data/LICENSE +21 -0
  4. data/README.md +126 -0
  5. data/bin/chi +1140 -0
  6. data/docs/architecture.md +299 -0
  7. data/docs/cli.md +490 -0
  8. data/docs/configuration.md +494 -0
  9. data/docs/desktop.md +97 -0
  10. data/docs/guardrails.md +218 -0
  11. data/docs/hooks.md +309 -0
  12. data/docs/internals/background-tasks.md +26 -0
  13. data/docs/internals/context-telemetry.md +36 -0
  14. data/docs/internals/gemma4-contract.md +23 -0
  15. data/docs/internals/tool-guardrails.md +45 -0
  16. data/docs/memory.md +85 -0
  17. data/docs/plugins.md +819 -0
  18. data/docs/releasing.md +135 -0
  19. data/docs/sessions.md +155 -0
  20. data/lib/samagotchi/bridge/bounded_queue.rb +70 -0
  21. data/lib/samagotchi/bridge/card_store.rb +126 -0
  22. data/lib/samagotchi/bridge/event_id.rb +25 -0
  23. data/lib/samagotchi/bridge/ring_buffer.rb +63 -0
  24. data/lib/samagotchi/bridge/sse_writer.rb +248 -0
  25. data/lib/samagotchi/bridge/turn_accumulator.rb +189 -0
  26. data/lib/samagotchi/bridge.rb +993 -0
  27. data/lib/samagotchi/bridge_client/event_stream.rb +158 -0
  28. data/lib/samagotchi/bridge_client/sse_parser.rb +51 -0
  29. data/lib/samagotchi/bridge_client.rb +330 -0
  30. data/lib/samagotchi/bundle_needs.rb +97 -0
  31. data/lib/samagotchi/bundles/btw/manifest.yml +10 -0
  32. data/lib/samagotchi/bundles/btw/plugin.rb +100 -0
  33. data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +82 -0
  34. data/lib/samagotchi/bundles/guardrails/guardrails.md +14 -0
  35. data/lib/samagotchi/bundles/guardrails/manifest.yml +8 -0
  36. data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +210 -0
  37. data/lib/samagotchi/bundles/known-names/known_names.md +3 -0
  38. data/lib/samagotchi/bundles/known-names/manifest.yml +14 -0
  39. data/lib/samagotchi/bundles/loop-guard/manifest.yml +10 -0
  40. data/lib/samagotchi/bundles/loop-guard/plugin.rb +158 -0
  41. data/lib/samagotchi/bundles/mcp/manifest.yml +11 -0
  42. data/lib/samagotchi/bundles/mcp/plugin.rb +631 -0
  43. data/lib/samagotchi/bundles/system/config_modification_protocol.md +149 -0
  44. data/lib/samagotchi/bundles/system/delegated.md +10 -0
  45. data/lib/samagotchi/bundles/system/identity.md +7 -0
  46. data/lib/samagotchi/bundles/system/manifest.yml +11 -0
  47. data/lib/samagotchi/bundles/system/memory_guide.md +107 -0
  48. data/lib/samagotchi/bundles/system/self_map.md +55 -0
  49. data/lib/samagotchi/cancellation_controller.rb +78 -0
  50. data/lib/samagotchi/client.rb +429 -0
  51. data/lib/samagotchi/commands/registry.rb +112 -0
  52. data/lib/samagotchi/config.rb +910 -0
  53. data/lib/samagotchi/context_note.rb +77 -0
  54. data/lib/samagotchi/context_quote.rb +21 -0
  55. data/lib/samagotchi/context_usage.rb +66 -0
  56. data/lib/samagotchi/context_window.rb +76 -0
  57. data/lib/samagotchi/debug_log.rb +110 -0
  58. data/lib/samagotchi/desktop/macos/App.swift +102 -0
  59. data/lib/samagotchi/desktop/macos/ChiRunner.swift +201 -0
  60. data/lib/samagotchi/desktop/macos/Hotkey.swift +42 -0
  61. data/lib/samagotchi/desktop/macos/Info.plist.erb +42 -0
  62. data/lib/samagotchi/desktop/macos/Panel.swift +383 -0
  63. data/lib/samagotchi/desktop/macos.rb +255 -0
  64. data/lib/samagotchi/desktop.rb +21 -0
  65. data/lib/samagotchi/desktop_command.rb +143 -0
  66. data/lib/samagotchi/engine.rb +2807 -0
  67. data/lib/samagotchi/guardrails/approval.rb +125 -0
  68. data/lib/samagotchi/guardrails/approvals.rb +177 -0
  69. data/lib/samagotchi/guardrails/context.rb +71 -0
  70. data/lib/samagotchi/guardrails/gate.rb +125 -0
  71. data/lib/samagotchi/guardrails/load_failures.rb +46 -0
  72. data/lib/samagotchi/guardrails/protected_paths.rb +77 -0
  73. data/lib/samagotchi/guardrails/rules.rb +199 -0
  74. data/lib/samagotchi/guardrails/targets.rb +119 -0
  75. data/lib/samagotchi/guardrails/verdict.rb +134 -0
  76. data/lib/samagotchi/guardrails.rb +18 -0
  77. data/lib/samagotchi/hooks/bundle_loader.rb +158 -0
  78. data/lib/samagotchi/hooks/loader.rb +162 -0
  79. data/lib/samagotchi/hooks/registry.rb +261 -0
  80. data/lib/samagotchi/hooks.rb +30 -0
  81. data/lib/samagotchi/host_registry.rb +315 -0
  82. data/lib/samagotchi/idle_client.rb +147 -0
  83. data/lib/samagotchi/idle_recap.rb +549 -0
  84. data/lib/samagotchi/idle_reminders.rb +101 -0
  85. data/lib/samagotchi/idle_scheduler.rb +76 -0
  86. data/lib/samagotchi/image_store.rb +393 -0
  87. data/lib/samagotchi/installed_gem.rb +38 -0
  88. data/lib/samagotchi/kernel_loop.rb +1017 -0
  89. data/lib/samagotchi/launch_mode.rb +34 -0
  90. data/lib/samagotchi/llm/backend.rb +28 -0
  91. data/lib/samagotchi/llm/chat_loop.rb +450 -0
  92. data/lib/samagotchi/llm/errors.rb +329 -0
  93. data/lib/samagotchi/llm/http.rb +412 -0
  94. data/lib/samagotchi/llm/model_result.rb +72 -0
  95. data/lib/samagotchi/llm/native_backend.rb +50 -0
  96. data/lib/samagotchi/llm/native_tool_normalizer.rb +277 -0
  97. data/lib/samagotchi/llm/openai_chat.rb +403 -0
  98. data/lib/samagotchi/llm/usage.rb +79 -0
  99. data/lib/samagotchi/log.rb +200 -0
  100. data/lib/samagotchi/log_line.rb +127 -0
  101. data/lib/samagotchi/log_path.rb +31 -0
  102. data/lib/samagotchi/log_subscriber.rb +163 -0
  103. data/lib/samagotchi/memory_bundle/builder.rb +364 -0
  104. data/lib/samagotchi/memory_bundle/index_updater.rb +123 -0
  105. data/lib/samagotchi/memory_bundle/installer.rb +528 -0
  106. data/lib/samagotchi/memory_bundle/listing.rb +72 -0
  107. data/lib/samagotchi/memory_bundle/manifest.rb +225 -0
  108. data/lib/samagotchi/memory_bundle/merger.rb +52 -0
  109. data/lib/samagotchi/memory_bundle/placeholder.rb +37 -0
  110. data/lib/samagotchi/memory_bundle/provenance.rb +257 -0
  111. data/lib/samagotchi/memory_bundle/source.rb +153 -0
  112. data/lib/samagotchi/memory_bundle/status.rb +107 -0
  113. data/lib/samagotchi/memory_bundle/system_bundle.rb +161 -0
  114. data/lib/samagotchi/memory_bundle/uninstaller.rb +128 -0
  115. data/lib/samagotchi/memory_bundle.rb +17 -0
  116. data/lib/samagotchi/memory_paths.rb +101 -0
  117. data/lib/samagotchi/model_overlay.rb +53 -0
  118. data/lib/samagotchi/model_profile.rb +309 -0
  119. data/lib/samagotchi/muted_memories.rb +66 -0
  120. data/lib/samagotchi/note_command.rb +163 -0
  121. data/lib/samagotchi/output_formatter.rb +100 -0
  122. data/lib/samagotchi/owner_lock.rb +110 -0
  123. data/lib/samagotchi/pending_input_queue.rb +48 -0
  124. data/lib/samagotchi/plugin/api.rb +362 -0
  125. data/lib/samagotchi/plugin/context.rb +193 -0
  126. data/lib/samagotchi/plugin/loader.rb +126 -0
  127. data/lib/samagotchi/plugin/service.rb +117 -0
  128. data/lib/samagotchi/plugin/sessions.rb +150 -0
  129. data/lib/samagotchi/plugin/side_question.rb +60 -0
  130. data/lib/samagotchi/plugin/tool_result.rb +24 -0
  131. data/lib/samagotchi/project_scope.rb +25 -0
  132. data/lib/samagotchi/prompt.rb +119 -0
  133. data/lib/samagotchi/prompt_literal_guard.rb +70 -0
  134. data/lib/samagotchi/recap_store.rb +92 -0
  135. data/lib/samagotchi/reminder_store.rb +165 -0
  136. data/lib/samagotchi/self_report.rb +195 -0
  137. data/lib/samagotchi/send_command.rb +170 -0
  138. data/lib/samagotchi/served_model.rb +32 -0
  139. data/lib/samagotchi/session.rb +508 -0
  140. data/lib/samagotchi/session_commands.rb +527 -0
  141. data/lib/samagotchi/session_delete_command.rb +105 -0
  142. data/lib/samagotchi/session_manager.rb +1049 -0
  143. data/lib/samagotchi/session_metrics.rb +466 -0
  144. data/lib/samagotchi/session_observer.rb +117 -0
  145. data/lib/samagotchi/terminal_ui/attach_launcher.rb +118 -0
  146. data/lib/samagotchi/terminal_ui/attached_loop.rb +1037 -0
  147. data/lib/samagotchi/terminal_ui/attached_view.rb +264 -0
  148. data/lib/samagotchi/terminal_ui/event_renderer.rb +192 -0
  149. data/lib/samagotchi/terminal_ui/formatting.rb +291 -0
  150. data/lib/samagotchi/terminal_ui/image_input.rb +36 -0
  151. data/lib/samagotchi/terminal_ui/input_support.rb +324 -0
  152. data/lib/samagotchi/terminal_ui/legacy_surface.rb +111 -0
  153. data/lib/samagotchi/terminal_ui/line_reader.rb +113 -0
  154. data/lib/samagotchi/terminal_ui/live_region.rb +36 -0
  155. data/lib/samagotchi/terminal_ui/plain_surface.rb +51 -0
  156. data/lib/samagotchi/terminal_ui/question_prompt.rb +153 -0
  157. data/lib/samagotchi/terminal_ui/question_slot.rb +131 -0
  158. data/lib/samagotchi/terminal_ui/reline_seam.rb +216 -0
  159. data/lib/samagotchi/terminal_ui/repl_input.rb +138 -0
  160. data/lib/samagotchi/terminal_ui/screen.rb +316 -0
  161. data/lib/samagotchi/terminal_ui/surface.rb +47 -0
  162. data/lib/samagotchi/terminal_ui/thinking_line.rb +101 -0
  163. data/lib/samagotchi/terminal_ui.rb +1992 -0
  164. data/lib/samagotchi/thinking_ticker.rb +110 -0
  165. data/lib/samagotchi/thought_stream_splitter.rb +149 -0
  166. data/lib/samagotchi/token_usage.rb +88 -0
  167. data/lib/samagotchi/tool_activity.rb +216 -0
  168. data/lib/samagotchi/tool_call_parser.rb +637 -0
  169. data/lib/samagotchi/tool_declarations.rb +561 -0
  170. data/lib/samagotchi/tool_runner.rb +211 -0
  171. data/lib/samagotchi/tools/args.rb +259 -0
  172. data/lib/samagotchi/tools/ask_user_question.rb +152 -0
  173. data/lib/samagotchi/tools/builtins.rb +122 -0
  174. data/lib/samagotchi/tools/cancel_reminder.rb +21 -0
  175. data/lib/samagotchi/tools/delegate.rb +167 -0
  176. data/lib/samagotchi/tools/delegate_result.rb +53 -0
  177. data/lib/samagotchi/tools/delegate_wait.rb +153 -0
  178. data/lib/samagotchi/tools/edit.rb +155 -0
  179. data/lib/samagotchi/tools/execute.rb +214 -0
  180. data/lib/samagotchi/tools/list_reminders.rb +20 -0
  181. data/lib/samagotchi/tools/list_sessions.rb +74 -0
  182. data/lib/samagotchi/tools/memory.rb +256 -0
  183. data/lib/samagotchi/tools/output_guardrails.rb +93 -0
  184. data/lib/samagotchi/tools/peers.rb +18 -0
  185. data/lib/samagotchi/tools/read.rb +182 -0
  186. data/lib/samagotchi/tools/register_reminder.rb +53 -0
  187. data/lib/samagotchi/tools/registry.rb +60 -0
  188. data/lib/samagotchi/tools/send_note.rb +49 -0
  189. data/lib/samagotchi/tools/task_create.rb +29 -0
  190. data/lib/samagotchi/tools/task_get.rb +39 -0
  191. data/lib/samagotchi/tools/task_list.rb +43 -0
  192. data/lib/samagotchi/tools/task_runtime.rb +311 -0
  193. data/lib/samagotchi/tools/task_stop.rb +29 -0
  194. data/lib/samagotchi/tools/task_wait.rb +104 -0
  195. data/lib/samagotchi/tools/tool_path.rb +18 -0
  196. data/lib/samagotchi/tools/web_fetch.rb +163 -0
  197. data/lib/samagotchi/tools/write.rb +26 -0
  198. data/lib/samagotchi/turn_flow.rb +242 -0
  199. data/lib/samagotchi/turn_note.rb +76 -0
  200. data/lib/samagotchi/turn_tally.rb +101 -0
  201. data/lib/samagotchi/version.rb +7 -0
  202. data/lib/samagotchi/vision_context.rb +132 -0
  203. data/lib/samagotchi/vision_support.rb +109 -0
  204. data/lib/samagotchi/web/app.rb +1349 -0
  205. data/lib/samagotchi/web/markdown_renderer.rb +107 -0
  206. data/lib/samagotchi/web/message_parts.rb +169 -0
  207. data/lib/samagotchi/web/public/activity.js +100 -0
  208. data/lib/samagotchi/web/public/annotations.js +67 -0
  209. data/lib/samagotchi/web/public/app.js +2382 -0
  210. data/lib/samagotchi/web/public/card.js +74 -0
  211. data/lib/samagotchi/web/public/chat_view.js +360 -0
  212. data/lib/samagotchi/web/public/chunk_router.js +25 -0
  213. data/lib/samagotchi/web/public/command_complete.js +39 -0
  214. data/lib/samagotchi/web/public/composer_size.js +19 -0
  215. data/lib/samagotchi/web/public/copy.js +142 -0
  216. data/lib/samagotchi/web/public/ctx.js +35 -0
  217. data/lib/samagotchi/web/public/data.js +256 -0
  218. data/lib/samagotchi/web/public/format.js +232 -0
  219. data/lib/samagotchi/web/public/hold.js +78 -0
  220. data/lib/samagotchi/web/public/images.js +77 -0
  221. data/lib/samagotchi/web/public/index.html +568 -0
  222. data/lib/samagotchi/web/public/init_row.js +60 -0
  223. data/lib/samagotchi/web/public/model_pick.js +23 -0
  224. data/lib/samagotchi/web/public/question_card.js +100 -0
  225. data/lib/samagotchi/web/public/route.js +17 -0
  226. data/lib/samagotchi/web/public/scope.js +36 -0
  227. data/lib/samagotchi/web/public/scroll.js +24 -0
  228. data/lib/samagotchi/web/public/sentences.js +88 -0
  229. data/lib/samagotchi/web/public/sessions_list.js +60 -0
  230. data/lib/samagotchi/web/public/strip.js +25 -0
  231. data/lib/samagotchi/web/public/tally.js +37 -0
  232. data/lib/samagotchi/web/public/thinking_ticker.js +79 -0
  233. data/lib/samagotchi/web/public/timing.js +185 -0
  234. data/lib/samagotchi/web/public/turn_events.js +209 -0
  235. data/lib/samagotchi/web/public/turn_model.js +204 -0
  236. data/lib/samagotchi/web/public/turn_view.js +587 -0
  237. data/lib/samagotchi/web/server.rb +183 -0
  238. data/lib/samagotchi/web/session_hub.rb +329 -0
  239. data/lib/samagotchi/web/session_summary.rb +85 -0
  240. data/lib/samagotchi/worker.rb +635 -0
  241. data/lib/samagotchi/worker_idle_exit.rb +87 -0
  242. data/lib/samagotchi.rb +12 -0
  243. metadata +374 -0
@@ -0,0 +1,299 @@
1
+ # Samagotchi Architecture
2
+
3
+ A compact visual overview of the current architecture, then the core API in prose
4
+ (see [Core and UI](#core-and-ui)).
5
+
6
+ ## System map
7
+
8
+ ```
9
+ ┌─────────────────────────────────────────┐
10
+ │ bin/chi │
11
+ │ (CLI entry point) │
12
+ └───────────────────┬─────────────────────┘
13
+ │
14
+ ┌──────────────────────────┴──────────────┐
15
+ │ TerminalUI │ ← REPL (Reline),
16
+ │ render · status line · REPL commands │ rendering, commands
17
+ └──────────────────────────┬──────────────┘
18
+ │ delegates
19
+ ┌───────────────────────────┴───────────────────────┐
20
+ │ Engine (core logic) │
21
+ │ system prompt · memory injection │
22
+ │ tool declarations · session lifecycle │
23
+ │ model↔tool loop — `run_turn` │
24
+ └───────────────────────────┬───────────────────────┘
25
+ │ delegates
26
+ ┌────────────────────────────┴──────────────┐
27
+ │ Web::App (Rack) │ ← browser UI
28
+ │ session hub · /api/events · /api/* │ via SessionManager file IPC
29
+ └───────────────────────────┬───────────────┘
30
+ │
31
+ ┌───────────────────────────────┬──────────┴───────────────┬──────────────────┐
32
+ │ │ │ │
33
+ ┌─────┴─────┐ ┌───────┴───────────┐ ┌────────┴───────┐ ┌───────┴────────┐
34
+ │ KernelLoop │◀── drives────────▶│ Model API │ │ Session │ │ SessionManager │
35
+ │ (the loop)│ │ (LLM HTTP call) │ │ (state, │ │ (bg workers) │
36
+ └─────┬──────┘ └─────────────────────┘ │ history) │ └────────────────┘
37
+ │ └──────────────────┘ └────────────────┘
38
+ │ run_turn events (turn_started, turn_completed,
39
+ │ turn_canceled, raw KernelLoop events)
40
+ ▼
41
+ ┌──────────────────────────────────────────────────────────────────────────────────┐
42
+ │ Tools │
43
+ │ execute · read · edit · write · memory · web_fetch · task_create/get/list/ │
44
+ │ task_stop/wait │
45
+ │ declared via tool_declarations.rb │
46
+ └──────────────────────────────────────────────────────────────────────────────────┘
47
+ │
48
+ ▼
49
+ ┌──────────────────────────────────────────────────────────────────────────────────┐
50
+ │ Persistence │
51
+ │ Project: ~/.config/samagotchi/memories/projects/<repo>_<hash>/ (per git repo) │
52
+ │ System: ~/.config/samagotchi/memories/ │
53
+ └──────────────────────────────────────────────────────────────────────────────────┘
54
+ ```
55
+
56
+ ## The two-layer split
57
+
58
+ ```
59
+ TerminalUI ── delegates ──▶ Engine
60
+ (REPL / render / commands) (pure logic, no terminal)
61
+ ▲ │
62
+ └────────── on_event: ◀────────────┘
63
+ (turn_started, turn_completed,
64
+ turn_canceled, raw KernelLoop events)
65
+ ```
66
+
67
+ - **`Engine`** (`lib/samagotchi/engine.rb`) owns *all* agent logic and knows nothing
68
+ about the terminal.
69
+ - **`TerminalUI`** (`lib/samagotchi/terminal_ui.rb`) owns the REPL and rendering; it
70
+ delegates all core work to an `Engine`.
71
+ - The `on_event:` seam on `run_turn` exposes raw `KernelLoop` events plus the
72
+ higher-level turn events, so any new UI can render without terminal coupling.
73
+
74
+ ## Request / turn flow
75
+
76
+ ```
77
+ bin/chi ─▶ TerminalUI ─▶ Engine#run_turn ─▶ KernelLoop ──┬─▶ Model API (LLM)
78
+ (builds) │ └─▶ Tool call(s) ─▶ Tool
79
+ │ │
80
+ └── on_event: ─▶ render update ────┘
81
+ (turn started / completed / canceled)
82
+ ```
83
+
84
+ ## Layers at a glance
85
+
86
+ | Layer | Class(es) | Responsibility |
87
+ |-------|-----------|----------------|
88
+ | Core | `Samagotchi::Engine` | System prompt, memory injection, tool declarations, session lifecycle, model↔tool loop (`run_turn`). No terminal coupling. |
89
+ | UI | `Samagotchi::TerminalUI` | REPL (Reline), rendering, REPL commands. Delegates all core work to an `Engine`. |
90
+ | Model loops | `KernelLoop` (via `LLM::NativeBackend`), `LLM::ChatLoop` | The model↔tool loop: raw prompt or OpenAI chat API, chosen per host (see below). |
91
+ | Adapters | `Samagotchi::Client`, `LLM::OpenAIChat`, `LLM::HTTP` | Raw-prompt servers, the OpenAI chat API, and the HTTP both share. |
92
+ | Tools | `lib/samagotchi/tools/*` | Execute, read, edit, write, memory, task_*, web_fetch, plus runtime/output-guardrails. |
93
+ | Background | `Samagotchi::SessionManager` | Builds `Engine` directly (no terminal rendering) for workers. |
94
+ | Web | `Samagotchi::Web::App`, `Samagotchi::Web::Server`, `Samagotchi::Web::SessionHub` | Rack+WEBrick single-port `127.0.0.1:4567` (index.html + `/api/*` + SSE). The hub is chi web's projection of the session list, pushed to every tab over `GET /api/events`. |
95
+ | Sessions | `Samagotchi::Session`, `SessionManager` | File `sessions/<uuid>.json` + sidecar `input/`/`output/`/`pid`; retention 14d/500, `updated_at desc`, lazy sweep. |
96
+
97
+ ## Entry points
98
+
99
+ - `LaunchMode.resolve` picks the terminal's mode. By default (`session.shared: true`)
100
+ plain `bin/chi`, `-p` and `--resume ID` run attached, like `--shared`; `--no-shared`,
101
+ `session.shared: false`, `--non-interactive` and `--verbose` run the REPL. `--memory`
102
+ and `--mute` are session fields (`preloaded_memory_names`, `muted_memory_names`) the
103
+ worker reads when it builds its `Engine`; `MutedMemories` filters the prompt's index and
104
+ the kernel's `memory_read`.
105
+ - The REPL → builds `TerminalUI`. `TerminalUI#run` is the single dispatch
106
+ for the REPL, `-p`/`--prompt`, `--non-interactive`, and `--resume`.
107
+ - Attached (`bin/chi`, `--attach ID`, `--shared [--resume ID]`) → `TerminalUI::AttachLauncher`: no `Engine`
108
+ and no `OwnerLock`; finds or starts the session's worker and runs `TerminalUI::AttachedLoop`
109
+ as a client of its Bridge (`BridgeClient#follow`, `post_turn`, `post_command`, `cancel`, `answer`,
110
+ `dismiss_question`). The worker runs in the session's `working_directory`, so `!cmd` and
111
+ the tools don't depend on where the terminal attached from.
112
+ - `bin/chi web` → builds `Web::Server` (Rack+WEBrick on `127.0.0.1:4567`, `--port`/`SAMAGOTCHI_WEB_PORT`, `--open`).
113
+ - `bin/chi sessions {list,stop,delete,prune,clean}` → retention & ordering (`Session.prune`, `updated_at desc`, dry-run, test-only); `stop` is `SessionManager.stop_session(wait:)`, which waits for the worker to release `owner.lock`; `delete` (`SessionDeleteCommand`) is `SessionManager.delete_session(stop:)`, which the TUI's `/exit --delete` and the web's `DELETE /api/sessions/:id` use too.
114
+ - `--prompt`, `--non-interactive`, and `SessionManager` workers build `Engine` directly.
115
+
116
+ ## Session retention & ordering
117
+
118
+ - **Files:** `~/.local/state/samagotchi/sessions/<uuid>.json` + `<uuid>/input|output|pid|owner.lock|bridge.json` (XDG-aware).
119
+ - **Single owner:** the process running a session's Engine (worker or in-process TUI) holds a flock on `owner.lock` (`OwnerLock`); a second owner backs off, and the web answers 409 for a TUI-owned session.
120
+ - **Status:** `status` is turn state (`idle`/`running`); liveness is the lock.
121
+ - **Retention:** 14 days / 500 cap (env `SAMAGOTCHI_SESSION_RETENTION_DAYS`/`MAX_COUNT`, optional `KEEP_STATUS`), live-owner guard, only when `*.json` present; lazy sweep ≤1/24h from the session hub's full-probe tick (and on `GET /api/sessions`, which the page no longer calls) & `Dashboard#render_list`, manual via `bin/chi sessions prune --dry-run`.
122
+ - **Ordering:** `Session.list(sort:,order:,limit:,offset:)` and `GET /api/sessions?sort=&order=&limit=&offset=` default `updated_at desc`; Web UI sort/filter/pagination.
123
+ - **Test hygiene:** `test_run` flag when `SAMAGOTCHI_ENV=test`/`RACK_ENV=test`/`CI`, targetable via `prune --test-only` / `clean`.
124
+
125
+ ## Core and UI
126
+
127
+ Samagotchi is split into a **core engine** and a **terminal UI**. The core holds all
128
+ agent logic and can be used without any terminal rendering; the UI is a thin layer on top.
129
+
130
+ | Layer | Class | Responsibility |
131
+ |-------|-------|----------------|
132
+ | Core | `Samagotchi::Engine` | System prompt, memory injection, tool declarations, session lifecycle, the model↔tool loop (`run_turn`). No terminal coupling. |
133
+ | UI | `Samagotchi::TerminalUI` | Interactive REPL (Reline), rendering (ANSI, spinner, status line), REPL commands. Delegates all core work to an `Engine`. |
134
+ | Model loops and adapters | `KernelLoop`, `LLM::ChatLoop`, `Samagotchi::Client`, `LLM::OpenAIChat`, `LLM::HTTP` | The model↔tool loops and the HTTP adapters they talk through (see "Model loops and adapters"). |
135
+ | Bridge (SSE/HTTP) | `Samagotchi::Bridge`, `SessionManager` | The **single live client transport**: an SSE read stream + HTTP POST turn/cancel/answer surface that attaches to a worker's existing `Engine` via `Engine#subscribe`. Every session worker starts it (bound `127.0.0.1`, no auth, localhost-only). |
136
+ | Web (Rack) | `Samagotchi::Web::App`, `SessionManager` | Single-port `127.0.0.1:4567` control plane via `rack`+`webrick` (serve `index.html` + `/api/*`; `/stream` proxies each session's Bridge). `bin/chi web` entrypoint. |
137
+ | Sessions | `Samagotchi::Session`, `SessionManager` | File-based `~/.local/state/samagotchi/sessions/<uuid>.json` + sidecar `input/`/`output/`/`pid`; retention (14d/500) + ordering (`updated_at desc`). |
138
+
139
+ - `bin/chi` in REPL mode (see `LaunchMode` above) builds `TerminalUI`. `TerminalUI#run` is the single
140
+ dispatch for the REPL, `-p`/`--prompt`, `--non-interactive`, and `--resume`: it
141
+ builds the working session once, runs a single prompt turn when `-p` is given,
142
+ then either exits (`--non-interactive`) or drops into the REPL carrying the
143
+ post-turn conversation.
144
+ - `SessionManager` background workers build `Engine` directly (no terminal rendering).
145
+ - `bin/chi` in attached mode (the default, `--attach`, `--shared`) builds no `Engine`: `TerminalUI::AttachLauncher` finds
146
+ or starts the worker, and `TerminalUI::AttachedLoop` is a client of its Bridge
147
+ (`BridgeClient#follow` for events, `post_turn`/`cancel`/`answer` for input). It
148
+ renders through the same `EventRenderer` as the REPL, on an `AttachedView` that
149
+ draws on a `Screen`: a live region at the bottom of the terminal (activity row,
150
+ prompt, status/notes/hints) under normal scrollback. From a turn's 3rd tool call the
151
+ activity slot gets a second, dim row: the turn's tool tally (`TurnTally`, seeded from the
152
+ snapshot's tool parts on a mid-turn join). Reline still reads the input,
153
+ but `RelineSeam` (prepended to `Reline::LineEditor`) sends its drawing to the
154
+ `Screen`. Without a capable terminal it falls back to `PlainSurface` (append-only).
155
+
156
+ #### Using the core
157
+
158
+ ```ruby
159
+ engine = Samagotchi::Engine.new(mode: :assist, model_name: "gemma4", memories: [])
160
+ session = Samagotchi::Session.new_session(mode: "assist", model_name: "gemma4", working_directory: Dir.pwd)
161
+
162
+ engine.run_turn(session, "hello", on_event: nil) # => KernelLoop::Result (`.output`)
163
+ ```
164
+
165
+ #### The `on_event` seam
166
+
167
+ `run_turn` accepts an optional `on_event:` callable that receives an event stream. It
168
+ forwards the raw `KernelLoop` events unchanged (the low-level contract) and adds a few
169
+ higher-level events so UIs get clean turn boundaries without inferring them:
170
+
171
+ - `:turn_started` — `{ session_id:, prompt: }`
172
+ - `:turn_completed` — `{ result: }` (the final `KernelLoop::Result`)
173
+ - `:turn_canceled` — `{ cancellation_reason: }`
174
+
175
+ Every event is a `Hash` with a `:type` symbol key; the sink must not raise (the Engine
176
+ rescues sink errors). A new UI (web, API) supplies its own `on_event` and
177
+ renders whatever it needs from the stream + final `Result`. The public Engine API:
178
+
179
+ ```ruby
180
+ engine.run_turn(session, prompt, on_event: nil, max_iterations: 100, cancel_controller: nil)
181
+ engine.run(session: nil, prompt: "...", on_event: nil) # create/resume session + run
182
+ engine.system_prompt # fully built system prompt string
183
+ engine.session # current session (Engine owns create/resume)
184
+ ```
185
+
186
+ #### Subscribing to the live stream (and the bridge)
187
+
188
+ For an **always-on** consumer (an external SSE client, a second UI), use
189
+ `Engine#subscribe` rather than passing `on_event:` to a single turn. It is a thread-safe,
190
+ error-isolated fan-out with a monotonic `event_seq` on every event:
191
+
192
+ ```ruby
193
+ handle = engine.subscribe(observer: ->(event) { ... }) # observer receives {..., event_seq:}
194
+ engine.unsubscribe(handle: handle)
195
+ engine.session_state_snapshot # => { status:, message_count:, last_prompt:, event_seq: }
196
+ ```
197
+
198
+ `Engine#subscribe` is the seam the SSE bridge (`Samagotchi::Bridge`) rides on. The bridge
199
+ is the **single live transport** and runs **inside the forked session worker** (the same
200
+ process that already owns the `Engine`); every worker starts it, and it exposes:
201
+
202
+ - `GET /session/:id/stream` — SSE stream of engine + kernel events, each with an
203
+ `id: <event_seq>-<epoch>` cursor (the epoch is drawn per worker's Bridge, since `event_seq` starts
204
+ over in each worker; snapshots and `/state` carry it as `event_id`); resume via `Last-Event-ID` /
205
+ `?from_seq=` (a plain `event_seq` is still accepted); a `: ping` heartbeat keeps idle proxies alive;
206
+ too-old reconnects, and cursors from another worker's epoch, receive a `reset` marker carrying
207
+ `session_state_snapshot`. `?snapshot=1` joins with a snapshot frame instead of a replay;
208
+ `?client_id=` names whose stream it is (`Bridge#open_streams_except`, used by `POST /exit`).
209
+ - `POST /session/:id/turn` — fire-and-forget turn creation; returns `202` with an `enqueued_id`
210
+ (delivery is at-least-once via the worker's file-IPC input path — it never calls `run_turn`
211
+ across the HTTP boundary). Inspect results through the read surface, not the turn response.
212
+ An optional `deadline` (epoch seconds; `BridgeClient` sends 5/6 of its read timeout ahead) makes
213
+ a request read after it (a worker frozen by sleep or SIGSTOP) answer `408 deadline_passed` and
214
+ not run: a client that timed out has said the message was not sent. `/answer`,
215
+ `/question/dismiss` and `/command` take the same `deadline` (a command is checked with the event
216
+ log held, as a turn is), and the Bridge logs `turn_expired`, `answer_expired`, `dismiss_expired`
217
+ or `command_expired`. The web app answers either kind of timeout with `504 worker_timeout`
218
+ ("… so the command was not run"). `/cancel`, `/recap` and `/exit` take none.
219
+ - `POST /session/:id/cancel` — cancel the running turn; `202`, or `409` when none runs.
220
+ - `POST /session/:id/answer` — answer the pending question; `200`, `409` when another client
221
+ answered first or it is gone, `400` for an invalid selection.
222
+ - `POST /session/:id/question/dismiss` — leave the question unanswered (an approval: denied);
223
+ `200`, or `409` when it is no longer pending.
224
+ - `POST /session/:id/command` — a session command (`/model`, `/models`, `!rollback`, `!cmd`,
225
+ `/continue`) for the worker loop; `202` with a `command_id` its `:command_ran` names, `400` when
226
+ the line isn't one.
227
+ - `POST /session/:id/exit` — ask the worker to exit now (`{client_id:}`). The worker checks with
228
+ the event log held (`WorkerIdleExit#hold_for_request`): `200 {status: "exiting"}` and it leaves
229
+ like an idle exit, or `409 {status: "held", reason:}` with `turn_running`, `input_queued`,
230
+ `continue_offered`, `client_connected` (a stream not named by the asker), `reminders` or
231
+ `starting`.
232
+ - `GET /session/:id/state` — `session_state_snapshot` (JSON).
233
+ - `GET /session/:id/stats` — `Engine#stats_snapshot` for attached `/stats`: the metrics, with the context window and prompt profile asked from the server before the first turn.
234
+ - `GET /session/:id/snapshot` — the snapshot frame's content as one request (the web server renders
235
+ the messages itself, then streams from its `event_seq`).
236
+ - `OPTIONS *` — CORS preflight (`Access-Control-Allow-Origin: *`).
237
+
238
+ The per-session port is OS-assigned (bound to `0`) and published to a `bridge.json` sidecar
239
+ for client discovery. `chi web`'s `GET /api/sessions/:id/stream` proxies this bridge
240
+ (503 `not_live` when the worker is not running; full history of any session is served by
241
+ `GET /api/sessions/:id/output`). Resume/ring-buffer state is **in-memory** (v1) — durable
242
+ cross-process resume is a staged next step, not part of v1.
243
+
244
+ **Session hub.** The cross-session layer (which sessions exist, who owns them, what changed)
245
+ never pulls from the page: `chi web` runs one `Samagotchi::Web::SessionHub` (a thread inside
246
+ the server, no daemon) that keeps an in-memory projection of the session list and pushes
247
+ changes to every open tab over `GET /api/events` (SSE: a `snapshot` frame on every connect,
248
+ then `session` for an upsert and `session_gone` for a removal, `: ping` while idle, no replay).
249
+ Files stay the source of truth and workers don't know the hub. Its watcher is a 1 s tick that
250
+ stats the sessions dir (every session writer goes tmp + rename, which bumps the dir's mtime) and
251
+ each `<id>/` folder (recap.json, bridge.json), re-parsing only the files whose mtime or size
252
+ moved through `Session.summary_from_file`. Liveness is probed, since a killed owner leaves no
253
+ file trace: the owner lock every tick for the sessions the projection believes owned, and every
254
+ session every 10 s, so a `kill -9` shows within a second. The summary (`Web::SessionSummary`,
255
+ shared with `/api/sessions` and the session view) carries `owner`, `project_root` and
256
+ `bridge_up` (the sidecar is there *and* the lock is held: the page attaches its stream on it).
257
+ `POST/DELETE /api/sessions` and `/stop` rescan the session before answering (`SessionHub#touch`).
258
+ Without a hub (`App.new` alone) `/api/events` answers `503 no_hub` and the page falls back to
259
+ fetching the list.
260
+
261
+ ## Model loops and adapters
262
+
263
+ Engine picks the loop from the effective model's host (`HostRegistry#resolve`):
264
+
265
+ | Loop | Class | Host | Talks through |
266
+ |---|---|---|---|
267
+ | Raw prompt | `KernelLoop`, wrapped by `LLM::NativeBackend` | no `api:`, or `llama_cpp`/`mlx`/`omlx` | `Client` (`/completion` or `/v1/completions`), chi's own Gemma/Qwen prompt and tool-call parsing |
268
+ | Chat | `LLM::ChatLoop` | `api: openai` | `LLM::OpenAIChat` (`/v1/chat/completions`, streamed, native tool calls) |
269
+
270
+ Both return an `LLM::ModelResult` and emit the same stream events; tool calls in
271
+ both go through `ToolRunner` (events, hooks, veto, output cap) and
272
+ `KernelLoop#dispatch_tool_call`. The chat loop's `generation_chunk` carries
273
+ `thinking:` (the server's `reasoning_content`), `text:` and `content:` (both);
274
+ its model turns keep their `tool_calls` and tool results their `tool_call_id`,
275
+ so later requests and resumed sessions pair them. It has its own system prompt
276
+ (no raw-prompt tool declarations; the tools go as JSON schemas).
277
+
278
+ **Adapters.** `Client` (raw-prompt servers) and `LLM::OpenAIChat` (one per host,
279
+ `HostRegistry#adapter_for`) share `LLM::HTTP`: timeouts, TLS for https, a line
280
+ reader for streamed bodies, the retry loop (`retry.*`; network errors, 429 and
281
+ 500/502/503/504/529, honouring `Retry-After`; never after a stream has produced
282
+ output) and cancel. Cancel closes the in-flight socket from the
283
+ `CancellationController` listener, so it works on any thread.
284
+
285
+ **Errors.** A failed request raises an `LLM::ProviderError` of one kind:
286
+ `ConnectionError` (`RetryExhausted`), `RateLimited`, `ServerError`, `AuthError`,
287
+ `BadRequest` (`context_overflow?`) or `ProtocolError`. Engine keeps the turn's
288
+ conversation (the prompt plus completed tool iterations) and emits
289
+ `:turn_failed` with `error_kind:`, `retryable:`, `host:` and a one-line
290
+ `summary:`, which the REPL, the attached TUI and the web show.
291
+
292
+ **Usage and models.** `ModelResult#usage` is an `LLM::Usage` (server counts, else
293
+ a chars/4 estimate, else zeros; never nil). Model lists are `LLM::ModelInfo`
294
+ (`HostRegistry#list_all_models`); the context window comes from the running
295
+ server (`/props`), then the host's model list, then config.
296
+
297
+ **Keys.** A host's API key comes only from the environment variable its
298
+ `api_key_env:` names; it never reaches config.yml, `HOSTS_JSON`, `chi self`,
299
+ logs or events.