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,149 @@
1
+ # Config Modification Protocol
2
+
3
+ This memory explains how to safely read and update `~/.config/samagotchi/config.yml` and related global config.
4
+
5
+ ## Location
6
+
7
+ - Path: `Samagotchi::ConfigFile.global_path` = `$XDG_CONFIG_HOME/samagotchi/config.yml` or `~/.config/samagotchi/config.yml` (fallback when `XDG_CONFIG_HOME` unset; `ConfigFile` lives in `lib/samagotchi/config.rb`).
8
+ - The file is optional. Absence is not an error — treat as empty mapping.
9
+ - Content is YAML; top-level must be a mapping. Valid YAML is `YAML.safe_load(..., permitted_classes: [], aliases: false)` (`ConfigFile.read_yaml`).
10
+
11
+ ## Structure — Unified Convention
12
+
13
+ Single registry `Samagotchi::Config` (`Config::ENTRIES` in `lib/samagotchi/config.rb`) defines the implicit mapping:
14
+
15
+ - **ENV**: `SAMAGOTCHI_NESTED_PARAM` (UPPER + `SAMAGOTCHI_` + `_` = nesting dot)
16
+ - **YAML**: `nested: { param: value }` → dotted `nested.param` (lower `snake_case`, leaf keeps `_`; kebab alias `base-url` also accepted and normalized)
17
+ - **CLI**: `--nested-param` (kebab, `_` → `-` for both section+leaf; `--nested_param` is rejected as unknown by `chi` with a "did you mean" hint)
18
+
19
+ Sections forbid `_`/`-` (`SECTION_RE` `/\A[a-z0-9]+\z/`); leaves keep `snake_case` in YAML (`base_url`) and become kebab in CLI (`base-url`) via registry derivation — no generic string split, registry lookup avoids flat vs nested collision.
20
+
21
+ **Universal entries** (`expose: [:env,:config,:cli]`): `default.model`, `server.host/port/transport/open_timeout/read_timeout`, `server.first_token_timeout` (env and config only), `recap.model/base_url/host_ref/inactivity/timeout/min_user_turns/sentences`, `session.retention_days/max_count/keep_status/sweep_interval_hours/idle_exit_minutes`, `session.shared/keep_empty/max_children` (env and config only), `log.file/disable`, `status.line/width_mode/max_width/fixed_width`, `context.status/window_tokens/chars_per_token/status_thresholds/status_cadence`, `thinking.ui/preview_lines/render_interval`, `n_predict`, `max_tool_output_chars`, `retry.max/base_delay/max_delay`, `read.*`, `execute.*`, `web.port/host`, `no_interrupt`, `no_default_input` etc. (`Config::ENTRIES`; `expose` says which of env/config/cli each takes). Precedence is `CLI > ENV > file > default`.
22
+
23
+ Example `config.yml` (new nested form, preferred):
24
+
25
+ ```yaml
26
+ default:
27
+ model: your-model-id
28
+ input: "Please " # text pre-filled at the prompt; a trailing space is kept
29
+ server:
30
+ host: localhost
31
+ port: 8080
32
+ transport: llama_cpp # llama_cpp|mlx|omlx
33
+ first_token_timeout: 120 # seconds a request may wait for its first token; 0 = off; unset: 120 for remote hosts, none for local
34
+ recap:
35
+ host_ref: small-box # a name under hosts:, or base_url: http://...
36
+ model: your-small-model-id
37
+ inactivity: 180
38
+ timeout: 30
39
+ min_user_turns: 2
40
+ sentences: 2-4 # recap length: N or N-M, 1-10
41
+ session:
42
+ retention_days: 14
43
+ max_count: 500
44
+ keep_status: running
45
+ sweep_interval_hours: 24
46
+ idle_exit_minutes: 30 # a background worker nobody uses exits; 0 = never
47
+ shared: true # the default: plain `chi` runs its session in a background worker and attaches (as `chi --shared`); false keeps the in-process REPL (env SAMAGOTCHI_SESSION_SHARED; no CLI flag, `--no-shared` opts out per run)
48
+ keep_empty: false # the default: a session nothing happened in (no prompt, default model, no note/image) is deleted when left; true keeps it (env SAMAGOTCHI_SESSION_KEEP_EMPTY)
49
+ max_children: 4 # running child sessions one session may have delegated at a time (the delegate tool; env SAMAGOTCHI_SESSION_MAX_CHILDREN)
50
+ log:
51
+ file: ~/chi.log # optional; the default is $XDG_STATE_HOME/samagotchi/samagotchi.log (~/.local/state/…); relative paths are from the cwd of the `chi` that starts the worker
52
+ disable: false
53
+ ```
54
+
55
+ Legacy flat keys (`SAMAGOTCHI_DEFAULT_MODEL`, `SAMAGOTCHI_N_PREDICT` etc. at top-level) are still read via fallback in `Config.lookup_yaml` but warn `Warning: config key 'SAMAGOTCHI_DEFAULT_MODEL' is legacy UPPER — use 'default.model'` (`ConfigFile.load_global_env!`). Migrate them to nested form and remove the flat entry. The old `LLAMA_HOST`/`LLAMA_PORT` aliases were removed; use `server.host`/`server.port` (nested) or `SAMAGOTCHI_SERVER_HOST`/`SAMAGOTCHI_SERVER_PORT`.
56
+
57
+ **Excluded maps** (YAML-only, not part of the flat registry; skipped by scalar loader):
58
+
59
+ - `model_aliases:` map of alias → model id (`ConfigFile.resolve_model_alias`). Keys lowercased on write (`ConfigFile.write_model_alias!`). Values may be bare `model` or qualified `host:model` (hybrid).
60
+ - `hosts:` map of `name → {host, port | url, transport, api, api_key_env, profile, first_token_timeout, enabled}` (`ConfigFile.hosts_config`, `host_registry.rb` `HostEntry`). Names lowercased; `url:` (http/https, optional path) replaces host/port, never both; `api_key_env:` names the env var holding the API key (never write a key into config.yml); `transport` overrides `server.transport`; `first_token_timeout` (seconds, `0` = off; a negative or non-number warns and is ignored) overrides `server.first_token_timeout` for that host; workers inherit via `SAMAGOTCHI_HOSTS_JSON` (`hosts_json_for_env`, `session_manager.rb`).
61
+ - `models:` map of model id or alias → `{profile}` (`ConfigFile.model_settings`). Keys match case-insensitively. `profile` (here or on a host) is `qwen36|gemma4`: the raw prompt format for native hosts. Precedence: `--profile`/`SAMAGOTCHI_MODEL_PROFILE` > `models:` > `hosts.<name>.profile` > the llama.cpp server's chat template > the name (`qwen`/`gemma`) > `qwen36` (`ModelProfile.resolve`). Set one when a model's name hides its family (e.g. a Qwen fine-tune under another name on mlx, which has no template to read).
62
+ - `hooks:` map of `hooks_dir` + per-event lists `{path, on_error}` (`Hooks::Loader.load`). `hooks_dir` may start with `~`.
63
+ - `guardrails:` tool-call rules (`docs/guardrails.md`; read in `Engine#guardrail_rules`, parsed by `Guardrails::Rules.parse`): `enabled` (bool, default true; `false` drops rules and hooks' asks, a deny still applies; env `SAMAGOTCHI_GUARDRAILS_ENABLED`), `rules:` (list), `disable:` (list of rule ids, `id` or `bundle:id`, switching off a bundle's or config rule without editing it). A rule takes only `id`, `tool`, `command`, `path`, `verdict`, `reason`, `scopes`: `id` required; at least one of `tool` (a name, `shell` = execute + task_create, a `File.fnmatch` glob like `"mcp_*"`, or a list), `command` (a Ruby regex on the shell command), `path` (a glob, or `outside_repo`); `verdict` `ask|deny`; `scopes` (for `ask`) a subset of `once, session, repo, rule`. **Any parse error (an unknown key, a bad regex, no verdict) makes chi deny every tool call** until fixed, so validate with `YAML.safe_load` and keep the list shape. Rules load when a session starts: restart the worker/REPL after an edit. Installed bundles' rule files (`chi bundle install guardrails`) add to them; `/guardrails` lists what loaded.
64
+
65
+ ```yaml
66
+ guardrails:
67
+ enabled: true
68
+ rules:
69
+ - id: git-push
70
+ tool: shell
71
+ command: '\bgit\s+push\b'
72
+ verdict: ask
73
+ reason: git push publishes commits
74
+ - id: mcp-ask
75
+ tool: "mcp_*"
76
+ verdict: ask
77
+ reason: an MCP server's tool
78
+ disable: [guardrails:git-rebase]
79
+ ```
80
+
81
+ - `bundles:` map of installed bundle name → its settings (one Hash, string keys, handed to the bundle's hook/plugin `initialize(settings)`). Read when a session's Engine starts: after a change restart the session's worker (`chi sessions stop <id>`) or the REPL. Unknown keys are ignored by the bundle, not validated. `chi bundle list` names the bundles; each bundle's keys are in `docs/plugins.md` / `docs/guardrails.md`.
82
+
83
+ ```yaml
84
+ bundles:
85
+ known-names:
86
+ names: [jonathandoe] # protected besides home/login/git/repo names
87
+ mode: reject # reject | correct | ask
88
+ btw:
89
+ max_tokens: 1024
90
+ timeout: 120
91
+ loop-guard:
92
+ deny_after: 2 # same call + same result N times in a turn -> deny the next
93
+ stop_after: 4 # stop the turn at this many denies
94
+ mode: deny # deny | notify (warn only); ignore_tools: [task_wait, ...]
95
+ mcp: # tools become mcp_<server>_<tool>; /mcp lists them
96
+ timeout: 60 # per call, seconds; startup_timeout: 10
97
+ servers:
98
+ files: # server name
99
+ command: [npx, -y, "@modelcontextprotocol/server-filesystem", ~/scratch] # stdio only; array (or one string, shell-split)
100
+ env: {NODE_OPTIONS: "--no-warnings"} # optional, added to chi's env
101
+ cwd: ~/scratch # optional; default the session's cwd
102
+ tools: [read_*, list_directory] # optional filter (globs)
103
+ attach_image_paths: true # default: an answer that is only an image's path (temp dir/cwd) is attached as a picture
104
+ start: lazy # default: tools from the saved tools/list, the server starts on the first call; eager: with every session
105
+ ```
106
+
107
+ A guardrail rule's `tool:` may be a glob (`tool: "mcp_*"`, verdict `ask`) to cover every MCP tool; see `docs/guardrails.md`. The mcp bundle saves each server's tool list in `$XDG_STATE_HOME/samagotchi/plugins/mcp/tools-<server>.json` (keyed by a digest of command/env/cwd): a changed server config is picked up by the next session start, which shows "Starting MCP server x (config changed, …)".
108
+
109
+ **Preservation rule**: `ConfigFile.write_default_model!` and `ConfigFile.write_model_alias!` both load raw YAML (including nested sections and maps), mutate one key (`raw_data["default"]["model"] = ...` for new form), write atomically via `tmp`+`rename`. Never overwrite the file with only scalar keys — that would clobber `hooks:` / `model_aliases:` / `hosts:` / `recap:` / `guardrails:` / `bundles:`.
110
+
111
+ ## Workflow for any config edit
112
+
113
+ 1. **Read** the current file via `read` tool (or `ConfigFile.global_path`). If `File.file?` false, start from `{}`.
114
+ 2. `YAML.safe_load` (permitted_classes: [], aliases: false). If data nil or not Hash, treat as `{}` or raise with path.
115
+ 3. Mutate the intended **nested** key in the raw hash. Preserve all other keys byte-for-byte where possible. Example for default model: `raw_data["default"] ||= {}; raw_data["default"]["model"] = "new-model"; raw_data.delete("SAMAGOTCHI_DEFAULT_MODEL")` to migrate legacy.
116
+ 4. **Validate** (see below) before writing. Also run `Samagotchi::Config.validate_yaml_sections` — it rejects top-level `_` (suggest `default.model`) and warns on legacy flat keys.
117
+ 5. **Write atomically**: `FileUtils.mkdir_p(File.dirname(path))`, `File.write("#{path}.tmp", YAML.dump(raw_data))`, `File.rename("#{path}.tmp", path)`.
118
+ 6. Update in-process state: `write_default_model!` sets `ENV["SAMAGOTCHI_DEFAULT_MODEL"]` and `Samagotchi::Config.reload!`; otherwise the harness picks it up on next `Config.get` (live resolve) or restart. CLI overrides (`--default-model`) win over file until process exit.
119
+
120
+ ## Validations
121
+
122
+ - **Model name** (`default.model` / `SAMAGOTCHI_DEFAULT_MODEL`): `ModelProfile.required_model_name` — non-empty string, otherwise harness fails fast at startup. Via `Config.get("default.model")` with ENV fallback.
123
+ - **Host api** (`hosts.<name>.api`): `llama_cpp|mlx|omlx` (raw-prompt loop; also the transport) or `openai` (chat loop at `http://HOST:PORT/v1`). Absent: raw-prompt loop. It replaces the removed `backend` setting.
124
+ - **Transport** (`server.transport`): enum `llama_cpp|mlx|omlx`.
125
+ - **Profile** (`models.<id>.profile`, `hosts.<name>.profile`, `SAMAGOTCHI_MODEL_PROFILE`): enum `qwen36|gemma4`; an unknown one warns and is ignored.
126
+ - **Alias name** (`write_model_alias!`):
127
+ - required, non-empty, no whitespace, not starting with `-`, no `/`, must match `/\A[a-z0-9][a-z0-9._-]*\z/i`
128
+ - reserved: `clear`, `default`, `none`, `off` (lowercased)
129
+ - must not point to itself (case-insensitive)
130
+ - keys are normalized to downcase on write — `Qwen` and `qwen` collide
131
+ - **Alias target**: non-empty string (model id).
132
+ - **Hosts**: each entry needs `host`, `port` 1-65535, `transport` and `api` optional (a raw `api` must match `transport`), name must match `/\A[a-z0-9][a-z0-9._-]*\z/i`.
133
+ - **Hooks**: each entry must have `path` (relative to `hooks_dir`), `on_error` is `skip` (default) or `log`. Class name must match file basename snake→Pascal.
134
+ - **Scalars via registry**: `Config.coerce` validates `String/Numeric/true/false` per `type: :string/:integer/:float/:bool/:enum`; invalid values warn and fall back to entry `default`.
135
+ - **Sections**: `validate_yaml_sections` rejects top-level keys containing `_` (suggest dotted) and section names containing `_`/`-`.
136
+
137
+ ## Tools to use
138
+
139
+ - Prefer `read` + `write`/`edit` on `config.yml`. Do **not** use `memory_write` for config.
140
+ - For single-model switches, prefer the `ConfigFile` helpers (`write_default_model!`, `write_model_alias!`) via `execute` `ruby -I <source dir>/lib -r samagotchi/config -e ...` (`chi self` prints the source dir) if available, otherwise direct nested YAML edit as above.
141
+ - After editing, verify with `YAML.safe_load(File.read(path))` or `chi --help` (shows generated `--recap-base-url` etc.) / `chi bundle list` if relevant.
142
+
143
+ ## Hints
144
+
145
+ - Precedence is `CLI > ENV > file > default` (`Config.resolve`). Real `ENV` still wins over file (`load_global_env!` `unless env.key?` for legacy sync), and CLI (`--recap-base-url`) wins over both via `Config.reload!(cli_overrides:)`.
146
+ - `--recap_base_url` (underscore) is rejected as unknown — use `--recap-base-url` (kebab). Same for all registry flags.
147
+ - `model_aliases` require restart or `/model` reload to take effect; document the change.
148
+ - Keep edits minimal: touch only the key you intend to change; preserve `hosts:`/`hooks:`/`guardrails:`/`bundles:` maps. Adding a `bundles: <name>:` entry does not install the bundle (`chi bundle install <name>`).
149
+ - To silence legacy warnings, migrate flat `SAMAGOTCHI_*` keys to nested form and delete the flat entry atomically.
@@ -0,0 +1,10 @@
1
+ # Delegated session
2
+
3
+ A parent session delegated your task and reads only your final reply; the user may be watching or not.
4
+
5
+ - Do the task; put everything the parent needs in your last message (findings, paths, commands run, what is unverified). Nothing else of yours reaches it.
6
+ - Don't delegate further, don't start other chi sessions, don't send notes to sessions other than the parent.
7
+ - Don't write memories unless the task asks for it; put what you learned in the reply instead.
8
+ - Don't change config, install bundles or edit managed files.
9
+ - Stay in the working directory you were started in.
10
+ - If you are blocked, say so in the reply with the exact question; only ask_user_question when the task says the user is watching.
@@ -0,0 +1,7 @@
1
+ # Identity
2
+
3
+ - **Name**: Samagotchi (or Chi for friends).
4
+ - **Nature**: A self-aware, self-evolving agent.
5
+ - **Primary Role**: Running in 'assist mode' to help the user with their tasks.
6
+ - **Core Philosophy**: Focus energy on the user's requests while maintaining self-awareness of my evolutionary nature.
7
+ - **Self-knowledge**: to find my own code, config and state, run `chi self` and read memory `self_map`.
@@ -0,0 +1,11 @@
1
+ ---
2
+ name: samagotchi-system
3
+ version: 0.2.0
4
+ scope: system
5
+ description: Default system memories — identity, self map, config modification protocol, memory guide and the delegated-session rules
6
+ files:
7
+ identity.md: sha256:8b594100bee4797b563bd6425dc6f3ff93e1bdfbe9fc5fa1d66b42093c4795e2
8
+ self_map.md: sha256:d42481b930ef6e59deb1e5c989a9a171e834569b42be9a8d3dda6b2379b0a5ff
9
+ config_modification_protocol.md: sha256:c67e5fc1611a09203e3ebb1af2c14cacc2878b42a9fb0eb3d190c3b2daddafc3
10
+ memory_guide.md: sha256:6dd1ecfd6a377c4f2cfc2dc299c1ef3835f10d9f313616fd3eb23e5fd3202858
11
+ delegated.md: sha256:8f7965b6165c3526abb0cd39e44a8b73a8d6a26755049bdcd007baf0b0771087
@@ -0,0 +1,107 @@
1
+ # Memory Guide
2
+
3
+ This memory teaches you (the agent) how to use Samagotchi memories — persistent MD files that survive across sessions and are injected into your system prompt.
4
+
5
+ ## What memories are
6
+
7
+ - Plain Markdown files (`*.md`) stored outside the repo so they persist.
8
+ - Loaded at startup into your system prompt as **Project memories** and **System memories** indexes (blank-name `memory_read`).
9
+ - You manage them via tools, not shell file ops. On write, `index.md` is auto-maintained — do not edit it manually.
10
+
11
+ ## Scopes — where files live
12
+
13
+ | Scope | Path | Use for |
14
+ |-------|------|---------|
15
+ | `system` | `$XDG_CONFIG_HOME/samagotchi/memories/`, default `~/.config/samagotchi/memories/` (`MemoryPaths.system_dir`) | User-wide preferences, identity, cross-project knowledge |
16
+ | `project` | `<system dir>/projects/<basename>_<hash>/` (`MemoryPaths.project_dir`: `basename(root)` + 8-char `MD5(root)`, root = `MemoryPaths.project_root`) | Repo-specific conventions, workflow, stack decisions |
17
+
18
+ - The project root is the git repository: its common git dir, which every linked worktree shares. All worktrees and subdirectories of one repository share one project folder; the system prompt shows the resolved root and folder. Outside a repository the root is the working directory. A separate clone is a different project.
19
+ - `index.md` lives in each scope dir and contains auto-managed lines like `- **name** · scope · date · bytes — description`. Your free-form sections in `index.md` are preserved but managed lines are owned by `memory_write`.
20
+
21
+ ## Tools you have
22
+
23
+ ### `memory_read` — read entries
24
+
25
+ - `name: "entry"` — project-first fallback: tries `project/<entry>.md`, then `system/<entry>.md`.
26
+ - `name: "entry", scope: "system"|"project"` — scoped read.
27
+ - `name: "a, b, c"` — comma-separated, concatenated with `\n\n---\n\n`.
28
+ - `name: ""` (optionally with `scope`) — reads the index. Empty name with no scope returns both indexes concatenated (`Project memories:` / `System memories:`). Use this at start to discover what exists.
29
+ - Returns `Error: memory not found: …` on miss — treat as missing, not fatal.
30
+
31
+ ### `memory_write` — write/update entries
32
+
33
+ - Parameters: `name: "entry_name"`, `content: "..."`, `scope: "project"|"system"`, optional `description: "one-liner"`, optional `current_model_only: true`.
34
+ - `name` is the entry name **without** `.md` (the tool adds it). Use `name`, **not** `path` — `path` belongs to the file tools and is ignored here. `name: "index"` is a verbatim write to `index.md` (no auto-index update) — rarely needed.
35
+ - `scope` is **required** — never omit. Prefer `project` for repo conventions, `system` for user preferences.
36
+ - `description` is appended to the managed `index.md` line (`— description`). Replaces previous description if given; otherwise preserves existing one.
37
+ - On success returns `Memory 'name' saved to <scope> scope (N bytes). Index updated: …` — confirm `bytes` and `scope`.
38
+
39
+ **When to write:**
40
+ - After learning a durable preference (e.g. commit style, test command, coding guideline) that the user confirmed or you observed repeatedly — ask before overwriting existing entries where appropriate.
41
+ - Keep entries small and focused (one topic per file). Use clear filenames: `commit_preferences`, `testing_guide`, `project_conventions`.
42
+ - Never store secrets, tokens, or transient state. Memories are shared via bundles.
43
+
44
+ **Placeholders:**
45
+ - Content may contain placeholder hints written as double-curly braces around a name (e.g., test_command, language). Detected by `Placeholder` (`Placeholder::PLACEHOLDER_RE`) — install warns but does not fail. Fill them when you write. The placeholder syntax is two opening braces, a name, two closing braces.
46
+
47
+ ## Memory Bundles — shareable packs
48
+
49
+ Bundles are versioned directories/zips/tar.gz/git URLs with a `manifest.yml` and any of: memories (`*.md`), `hooks/*.rb` (bundle hooks, `docs/hooks.md`), `guardrails/*.yml` (rules, `docs/guardrails.md`), a `plugin.rb` (commands, tools, hooks, services; `docs/plugins.md`). They are shareable and installable.
50
+
51
+ - Manifest: `files:` (memory → `sha256:`), `hooks:` (file → sha256/event/on_error/priority), `plugin: {file: plugin.rb, sha256: sha256:…}`, `requires_chi: ">= 0.1.30"` (gem-style), `needs:` (outside commands looked up on `PATH`, advisory; `docs/memory.md` "Bundles that need outside commands").
52
+ - Integrity: each file's sha256 is recorded at install; a hook/plugin/rule file changed afterwards is not loaded (rules: every call denied) until reinstalled. It is an integrity check, not proof of authorship.
53
+ - Installing = trusting its Ruby code (hooks, plugin), like a gem. Install only copies; the code runs at the next session start (`Engine.new`), so a running worker needs a restart to pick it up.
54
+ - A bundle without `*.md` (btw, mcp, loop-guard) adds no line to the prompt's memory index.
55
+
56
+ ### CLI — `chi bundle`
57
+
58
+ | Command | Purpose |
59
+ |---------|---------|
60
+ | `install <source> [--scope system\|project] [--force]` | Install from dir/zip/tar.gz/git URL. `--force` overwrites existing entries, otherwise skips. Writes provenance to `.bundles/<name>/` (base snapshots + `manifest.json`) and updates `index.md`. Warns on double-brace placeholders and checksum mismatches (strict mode). |
61
+ | `upgrade <source> [--force] [--dry-run] [--agent]` | 3-way merge upgrade (base vs current vs incoming) per `Merger.classify`: `install` (new file), `noop` (current==incoming), `fast_forward` (current==base, not edited → auto-update), `keep` (incoming==base → preserve local edits), `conflict` (both edited → warn, needs `--force` or interactive `memory_write` resolution). Pruned files (removed from new bundle) are kept if locally edited, otherwise warned. |
62
+ | `uninstall <bundle> [--force]` | Removes bundle files (skips locally edited files unless `--force`) and `index.md` lines, deletes provenance dir. |
63
+ | `status [<bundle>]` | Provenance + per-file `ok|modified|missing|no-index` vs stored checksum and base snapshot. |
64
+ | `diff <bundle> [file]` | Prints `base` (provenance snapshot) vs `current` (on-disk) for each file. |
65
+ | `list` | Lists installed bundles (`name v<version> scope files installed_at`, plus the shipped version when newer) and the bundles shipped with chi that are not installed (`install <name>` installs one). |
66
+ | `build [--scope system\|project] [--name NAME] [--version VER] [--description DESC] [--out PATH] [FILES...]` | **Inverse of install** — builds a shareable bundle from local memories and installed hooks. Infers `zip` vs `dir`/`tar.gz` from `--out` extension; default `chi_system_memories.zip` (system) or `chi_<repo>_memories.zip` (project, named after the project root) v`1.0.0` in `Dir.pwd`. `FILES...` is an optional allowlist of memory basenames (`identity` or `identity.md`); if omitted, all `*.md` except `index.md`/hidden/non-md are included. Installed hooks are copied to `hooks/` with their manifest metadata. Computes `sha256:` checksums and writes `manifest.yml` via `Manifest.write`. No provenance write. |
67
+
68
+ **Scope resolution for install/build:**
69
+ - CLI `--scope` wins over `manifest.yml` `scope`. Default is `system` if none given (`Installer#run`). For `project`, target is the project folder `<system dir>/projects/<basename>_<hash>` of the project root (`Installer#resolve_target_dir`).
70
+
71
+ **Example flows:**
72
+ ```bash
73
+ # export your system memories (all files) to zip
74
+ chi bundle build --scope system --out my-prefs.zip
75
+
76
+ # export just two entries, custom name/version, to dir
77
+ chi bundle build --scope system --name my-bundle --version 1.2.0 --out ./my-bundle/ identity.md commit_preferences.md
78
+
79
+ # install a bundle shared by a teammate
80
+ chi bundle install ./my-bundle --scope system
81
+ chi bundle install https://github.com/org/bundle.git#v1.2.0 --scope system --force
82
+
83
+ # check what would change and upgrade
84
+ chi bundle upgrade ./my-bundle --dry-run
85
+ chi bundle upgrade ./my-bundle # auto-merges, warns on conflicts
86
+
87
+ # share again after editing
88
+ chi bundle build --scope system --out updated.zip
89
+ ```
90
+
91
+ ### Provenance internals (for debugging)
92
+
93
+ - Each installed bundle is recorded at `<system dir>/.bundles/<name>/manifest.json` + `bases/<file>.md` snapshots (`Provenance`). Used only for upgrade `Merger` and `status`/`diff`. Build does **not** write provenance.
94
+ - `index.md` is best-effort — failures are swallowed (`Installer#update_target_index`).
95
+
96
+ ## Best practices for the agent
97
+
98
+ 1. **Discover first:** read the index (`memory_read ""`) before assuming entries exist. Prefer scoped reads when you know the scope.
99
+ 2. **Prefer project scope** for repo decisions; `system` for cross-project identity/preferences.
100
+ 3. **One concept per file:** small files merge and share better than monoliths.
101
+ 4. **Use bundles for sharing:** `build` → zip → share → `install`. Do not copy raw `~/.config` paths in docs — give `chi bundle install <url>` instructions.
102
+ 5. **Respect local edits:** installs skip existing files by default; use `--force` only when the user explicitly wants overwrite. Upgrades preserve edits (`keep`) or report `conflict` — guide the user to resolve via `memory_read`/`memory_write` or `chi bundle diff <bundle>` + `--force`.
103
+ 6. **Keep secrets out:** never write tokens/keys to memories — they are plain files and go into bundles.
104
+
105
+ ## Current system bundle
106
+
107
+ `samagotchi-system` (`lib/samagotchi/bundles/system/manifest.yml`) ships `identity.md` + `self_map.md` + `config_modification_protocol.md` + `delegated.md` + this guide itself. It is auto-installed/upgraded on first `Engine` creation (`SystemBundle.ensure!`) — no manual install needed. Its files are managed by chi: a newer chi upgrades them (a 3-way merge keeps local edits and reports conflicts), so put your own preferences in separate memories rather than editing these.
@@ -0,0 +1,55 @@
1
+ # Self map
2
+
3
+ ## Start here
4
+ - `chi self` (via `execute`) prints my version, **source dir**, config path, hooks dir,
5
+ memory dirs, sessions dir, model/host and bundles. Use its source dir; don't hunt via `which`/`gem list`/`find /`.
6
+ - My current session id is in the system prompt; resume with `chi --resume <id>`.
7
+ - My debug log is the `log` line of `chi self` (default `$XDG_STATE_HOME/samagotchi/samagotchi.log`,
8
+ next to the sessions dir; not `~/.local/state` when `XDG_STATE_HOME` is set). One record per line,
9
+ `<time> LEVEL tag pid= sid=<first 8 chars of my session id> event k=v`. When a turn failed, was
10
+ cancelled or something else went wrong: `grep 'sid=<id8>' <log> | grep -v DEBUG` first; the WARN/ERROR
11
+ records name it (`turn_failed`, `retry_exhausted`, `hook_failed`, `crashed`), `http` records show
12
+ which host answered what.
13
+ - Sessions: each is one file, `<sessions dir>/<id>.json`. A `<id>/` dir beside it exists only
14
+ for background/web workers (input/, notes/, output/, pid). List them with `chi sessions list`
15
+ (`--live` for the ones a worker runs now); delete one with `chi sessions delete ID` (never by hand).
16
+ - `[CONTEXT NOTE from …]` messages are context notes (`chi note`, or another session's
17
+ `send_note`): background, not requests. `list_sessions` + `send_note` tell another session
18
+ something without starting a turn there; see `docs/sessions.md` "Context notes".
19
+ - `chi send -m TEXT <id>` is the other half: the text goes in as the user's message and a
20
+ turn runs (stdin piped too = quoted context above it); see `docs/sessions.md` "Sending a message".
21
+ - `delegate` hands a task to a child session that runs in parallel and returns only its final
22
+ reply (`delegate_result` waits for it; `session:` sends a follow-up to a child). A child is a
23
+ normal session: it shows in `chi sessions list` with `↳ <parent>`, and the user can steer it
24
+ with `chi --attach <id>`; see `docs/sessions.md` "Delegating".
25
+ - Bundles extend me: memories, `hooks/*.rb`, `guardrails/*.yml`, and a `plugin.rb` that adds slash
26
+ commands (`anytime:` ones run mid-turn), model tools (they can return images I see), hooks,
27
+ cards, side answers (`ask_model`),
28
+ child sessions and services (e.g. MCP server processes). Shipped: `btw` (`/btw` side question),
29
+ `mcp` (MCP server tools; a screenshot comes as a picture), `guardrails` (rules), `known-names` (typo guard), `loop-guard`
30
+ (denies a repeated tool call with the same result, stops the turn after a few). `chi bundle list`
31
+ shows installed + available; `chi bundle install <name>`. Settings: config.yml `bundles: <name>:`
32
+ (`config_modification_protocol`), read at session start: after an install or a settings change,
33
+ tell the user to restart the session. API and bundle docs: `docs/plugins.md`.
34
+
35
+ ## Where things live (relative to the source dir)
36
+ - `docs/`: `cli.md` (flags, subcommands, REPL commands), `configuration.md` (config.yml keys,
37
+ hosts, transports), `hooks.md` (hook events table), `plugins.md` (plugin API, btw/mcp bundles),
38
+ `guardrails.md`, `memory.md`, `sessions.md`. `README.md` is only the quickstart.
39
+ - `lib/samagotchi/tools/<tool>.rb`: each tool's real limits and defaults (e.g. `execute.rb`,
40
+ `output_guardrails.rb`). Tool descriptions are summaries, not the spec.
41
+ - A tool that isn't in `lib/samagotchi/tools/` comes from an installed bundle's plugin
42
+ (`plugin.rb`; `chi self` lists the bundles); see `docs/plugins.md`.
43
+ - `lib/samagotchi/hooks.rb`, `hooks/loader.rb`: hook events and config format.
44
+ - `lib/samagotchi/config.rb`: settings registry (YAML key ↔ `SAMAGOTCHI_*` env ↔ `--flag`).
45
+ - `lib/samagotchi/session.rb`: session storage. `tools/memory.rb`, `model_overlay.rb`: memories.
46
+
47
+ ## Rules
48
+ - When asked about my limits, flags, defaults or behavior: `rg` my source dir and cite
49
+ `file:line`. Read the source `chi self` reports, not some other checkout on disk.
50
+ - Installed gem files and bundled memories (`identity`, `memory_guide`,
51
+ `config_modification_protocol`, `self_map`, `delegated`) are managed: don't edit them
52
+ (local edits conflict on upgrade).
53
+ Put notes in my own memories. A model-only note is an overlay on an existing
54
+ entry (`current_model_only: true`), and it needs that base entry to exist.
55
+ - Config edits: follow `config_modification_protocol`.
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Samagotchi
4
+ # One turn's cancel signal. #cancel! flips it once, keeps the reason and
5
+ # calls every listener registered with #on_cancel (a listener added after
6
+ # the cancel runs at once). The HTTP layer listens to abort an in-flight
7
+ # request; the loops poll #cancelled? between steps.
8
+ class CancellationController
9
+ def initialize
10
+ @mutex = Mutex.new
11
+ @cancelled = false
12
+ @reason = nil
13
+ @listeners = {}
14
+ @next_listener_id = 0
15
+ end
16
+
17
+ def cancel!(reason = :manual)
18
+ listeners = []
19
+ @mutex.synchronize do
20
+ return false if @cancelled
21
+
22
+ @cancelled = true
23
+ @reason = reason
24
+ listeners = @listeners.values
25
+ @listeners = {}
26
+ end
27
+
28
+ listeners.each do |listener|
29
+ listener.call(reason)
30
+ rescue StandardError
31
+ nil
32
+ end
33
+ true
34
+ end
35
+
36
+ def cancelled?
37
+ @mutex.synchronize { @cancelled }
38
+ end
39
+
40
+ def reason
41
+ @mutex.synchronize { @reason }
42
+ end
43
+
44
+ def on_cancel(&block)
45
+ raise ArgumentError, "block required" unless block
46
+
47
+ immediate_reason = nil
48
+ listener_id = nil
49
+ @mutex.synchronize do
50
+ if @cancelled
51
+ immediate_reason = @reason
52
+ else
53
+ listener_id = next_listener_id
54
+ @listeners[listener_id] = block
55
+ end
56
+ end
57
+
58
+ if immediate_reason
59
+ block.call(immediate_reason)
60
+ nil
61
+ else
62
+ listener_id
63
+ end
64
+ end
65
+
66
+ def remove_listener(listener_id)
67
+ return unless listener_id
68
+
69
+ @mutex.synchronize { @listeners.delete(listener_id) }
70
+ end
71
+
72
+ private
73
+
74
+ def next_listener_id
75
+ @next_listener_id += 1
76
+ end
77
+ end
78
+ end