samagotchi 0.22.0 → 0.23.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 (154) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -1
  3. data/bin/chi +9 -4
  4. data/docs/configuration.md +4 -4
  5. data/docs/releasing.md +1 -1
  6. data/docs/testing.md +6 -0
  7. data/lib/samagotchi/answer_command.rb +4 -2
  8. data/lib/samagotchi/bootstrap/config_writer.rb +2 -1
  9. data/lib/samagotchi/bootstrap/probe.rb +1 -1
  10. data/lib/samagotchi/bootstrap_command.rb +2 -2
  11. data/lib/samagotchi/bridge/bounded_queue.rb +0 -2
  12. data/lib/samagotchi/bridge/sse_writer.rb +0 -6
  13. data/lib/samagotchi/bridge/turn_accumulator.rb +2 -2
  14. data/lib/samagotchi/bridge.rb +12 -9
  15. data/lib/samagotchi/bridge_client/event_stream.rb +2 -2
  16. data/lib/samagotchi/bridge_client.rb +1 -1
  17. data/lib/samagotchi/bundle_command.rb +39 -29
  18. data/lib/samagotchi/bundles/btw/manifest.yml +2 -2
  19. data/lib/samagotchi/bundles/btw/plugin.rb +2 -0
  20. data/lib/samagotchi/bundles/check-in/manifest.yml +2 -2
  21. data/lib/samagotchi/bundles/check-in/plugin.rb +2 -0
  22. data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +6 -5
  23. data/lib/samagotchi/bundles/known-names/manifest.yml +2 -2
  24. data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
  25. data/lib/samagotchi/bundles/loop-guard/plugin.rb +2 -0
  26. data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
  27. data/lib/samagotchi/bundles/mcp/plugin.rb +2 -0
  28. data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
  29. data/lib/samagotchi/bundles/skills/plugin.rb +5 -3
  30. data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +7 -7
  31. data/lib/samagotchi/bundles/source-links/manifest.yml +2 -2
  32. data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
  33. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  34. data/lib/samagotchi/cancellation_controller.rb +1 -1
  35. data/lib/samagotchi/cli/command.rb +1 -1
  36. data/lib/samagotchi/client.rb +3 -5
  37. data/lib/samagotchi/commands/registry.rb +2 -2
  38. data/lib/samagotchi/config.rb +38 -14
  39. data/lib/samagotchi/context_note.rb +1 -1
  40. data/lib/samagotchi/context_status.rb +6 -6
  41. data/lib/samagotchi/continue_offer.rb +15 -8
  42. data/lib/samagotchi/debug_log.rb +1 -1
  43. data/lib/samagotchi/desktop/macos.rb +5 -4
  44. data/lib/samagotchi/empty_answer_retry.rb +1 -1
  45. data/lib/samagotchi/engine.rb +17 -17
  46. data/lib/samagotchi/gem_update.rb +2 -1
  47. data/lib/samagotchi/guardrail_wiring.rb +1 -1
  48. data/lib/samagotchi/guardrails/approval.rb +5 -5
  49. data/lib/samagotchi/guardrails/approvals.rb +2 -2
  50. data/lib/samagotchi/guardrails/gate.rb +2 -2
  51. data/lib/samagotchi/guardrails/read_only_shell.rb +5 -3
  52. data/lib/samagotchi/guardrails/shell_git_dirs.rb +1 -1
  53. data/lib/samagotchi/guardrails/shell_lex.rb +2 -2
  54. data/lib/samagotchi/guardrails/shell_paths.rb +2 -2
  55. data/lib/samagotchi/hooks/bundle_loader.rb +9 -6
  56. data/lib/samagotchi/hooks/loader.rb +18 -16
  57. data/lib/samagotchi/hooks/registry.rb +10 -2
  58. data/lib/samagotchi/host_registry.rb +1 -3
  59. data/lib/samagotchi/idle_recap.rb +18 -15
  60. data/lib/samagotchi/idle_reminders.rb +6 -3
  61. data/lib/samagotchi/image_store.rb +5 -2
  62. data/lib/samagotchi/kernel_loop.rb +31 -31
  63. data/lib/samagotchi/llm/chat_loop.rb +9 -6
  64. data/lib/samagotchi/llm/errors.rb +2 -2
  65. data/lib/samagotchi/llm/http.rb +12 -12
  66. data/lib/samagotchi/llm/openai_chat.rb +39 -28
  67. data/lib/samagotchi/llm/prompt_cache.rb +1 -1
  68. data/lib/samagotchi/log.rb +2 -2
  69. data/lib/samagotchi/log_subscriber.rb +2 -1
  70. data/lib/samagotchi/memory_bundle/builder.rb +9 -3
  71. data/lib/samagotchi/memory_bundle/index_updater.rb +13 -1
  72. data/lib/samagotchi/memory_bundle/installer.rb +67 -57
  73. data/lib/samagotchi/memory_bundle/listing.rb +5 -1
  74. data/lib/samagotchi/memory_bundle/manifest.rb +14 -4
  75. data/lib/samagotchi/memory_bundle/merger.rb +6 -4
  76. data/lib/samagotchi/memory_bundle/provenance.rb +16 -13
  77. data/lib/samagotchi/memory_bundle/source.rb +7 -3
  78. data/lib/samagotchi/memory_bundle/status.rb +3 -1
  79. data/lib/samagotchi/memory_bundle/system_bundle.rb +3 -2
  80. data/lib/samagotchi/memory_bundle/trash.rb +5 -2
  81. data/lib/samagotchi/memory_bundle/uninstaller.rb +9 -6
  82. data/lib/samagotchi/memory_bundle.rb +1 -0
  83. data/lib/samagotchi/model_overlay.rb +3 -5
  84. data/lib/samagotchi/model_profile.rb +12 -8
  85. data/lib/samagotchi/note_command.rb +2 -1
  86. data/lib/samagotchi/output_formatter.rb +18 -17
  87. data/lib/samagotchi/owner_lock.rb +2 -1
  88. data/lib/samagotchi/plugin/api.rb +8 -5
  89. data/lib/samagotchi/plugin/context.rb +1 -3
  90. data/lib/samagotchi/plugin/loader.rb +1 -1
  91. data/lib/samagotchi/plugin/service.rb +1 -1
  92. data/lib/samagotchi/plugin/sessions.rb +1 -0
  93. data/lib/samagotchi/plugin_tasks.rb +1 -1
  94. data/lib/samagotchi/prompt.rb +68 -12
  95. data/lib/samagotchi/prompt_literal_guard.rb +1 -1
  96. data/lib/samagotchi/question_desk.rb +7 -7
  97. data/lib/samagotchi/recap_setup.rb +1 -1
  98. data/lib/samagotchi/relay_desk.rb +1 -1
  99. data/lib/samagotchi/reminder_store.rb +2 -1
  100. data/lib/samagotchi/send_command.rb +4 -2
  101. data/lib/samagotchi/session.rb +2 -2
  102. data/lib/samagotchi/session_archive_command.rb +5 -1
  103. data/lib/samagotchi/session_commands.rb +1 -3
  104. data/lib/samagotchi/session_manager.rb +2 -4
  105. data/lib/samagotchi/session_metrics.rb +10 -10
  106. data/lib/samagotchi/session_observer.rb +3 -2
  107. data/lib/samagotchi/session_retention.rb +2 -2
  108. data/lib/samagotchi/sessions_command.rb +14 -5
  109. data/lib/samagotchi/system_prompt.rb +19 -8
  110. data/lib/samagotchi/terminal_ui/attach_launcher.rb +5 -5
  111. data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -2
  112. data/lib/samagotchi/terminal_ui/event_renderer.rb +1 -2
  113. data/lib/samagotchi/terminal_ui/formatting.rb +10 -10
  114. data/lib/samagotchi/terminal_ui/input_support.rb +2 -2
  115. data/lib/samagotchi/terminal_ui/line_reader.rb +3 -3
  116. data/lib/samagotchi/terminal_ui/question_prompt.rb +23 -23
  117. data/lib/samagotchi/terminal_ui/question_slot.rb +10 -10
  118. data/lib/samagotchi/terminal_ui/reline_seam.rb +1 -1
  119. data/lib/samagotchi/terminal_ui/repl_input.rb +3 -2
  120. data/lib/samagotchi/terminal_ui.rb +9 -5
  121. data/lib/samagotchi/text_diff.rb +12 -12
  122. data/lib/samagotchi/thinking.rb +6 -2
  123. data/lib/samagotchi/thought_stream_splitter.rb +6 -3
  124. data/lib/samagotchi/tool_activity.rb +1 -0
  125. data/lib/samagotchi/tool_call_parser.rb +13 -14
  126. data/lib/samagotchi/tool_declarations.rb +36 -35
  127. data/lib/samagotchi/tools/ask_user_question.rb +3 -4
  128. data/lib/samagotchi/tools/builtin_calls.rb +1 -1
  129. data/lib/samagotchi/tools/delegate.rb +1 -1
  130. data/lib/samagotchi/tools/delegate_relay.rb +1 -1
  131. data/lib/samagotchi/tools/delegate_wait.rb +0 -1
  132. data/lib/samagotchi/tools/edit.rb +14 -13
  133. data/lib/samagotchi/tools/execute.rb +3 -5
  134. data/lib/samagotchi/tools/list_reminders.rb +1 -1
  135. data/lib/samagotchi/tools/memory.rb +21 -22
  136. data/lib/samagotchi/tools/read.rb +7 -7
  137. data/lib/samagotchi/tools/task_create.rb +1 -1
  138. data/lib/samagotchi/tools/task_get.rb +1 -1
  139. data/lib/samagotchi/tools/task_list.rb +1 -1
  140. data/lib/samagotchi/tools/task_runtime.rb +8 -9
  141. data/lib/samagotchi/tools/task_stop.rb +1 -1
  142. data/lib/samagotchi/tools/task_wait.rb +3 -4
  143. data/lib/samagotchi/tools/web_fetch.rb +2 -4
  144. data/lib/samagotchi/tools/write.rb +1 -1
  145. data/lib/samagotchi/turn_flow.rb +8 -8
  146. data/lib/samagotchi/turn_note.rb +5 -1
  147. data/lib/samagotchi/version.rb +1 -1
  148. data/lib/samagotchi/vision_support.rb +2 -2
  149. data/lib/samagotchi/web/app.rb +13 -6
  150. data/lib/samagotchi/web/markdown_renderer.rb +2 -1
  151. data/lib/samagotchi/web/qr.rb +7 -1
  152. data/lib/samagotchi/web/server.rb +6 -2
  153. data/lib/samagotchi/worker.rb +10 -2
  154. metadata +20 -20
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9019626f95da411558ec5350303433de58fd07f9db6f9e22323856e4e2ed31a0
4
- data.tar.gz: 015df1a469fb64ff5556ce6f683ee23abf039865a34584fff07e3f3c32e4724e
3
+ metadata.gz: 236e301c873eb2b96520aeeb3e7668e535f50174b1afb94d28a149cef161d418
4
+ data.tar.gz: adc3281cace9460e307abe5abc33719041863898e848f10b290d5d2d3e4c93d5
5
5
  SHA512:
6
- metadata.gz: 1bd652f5d5c2d549dce4eba12cc6577f34cb80ae2a95ea4d63492b4581ed510a5f4eeb8b741f86da7af39198dbfc4981e5d6e7d019dfff7d90f8c0a19722cd6b
7
- data.tar.gz: fe6e4cbc412b112d157ad16a1f176fa1308ed094498eb982770b5a6b87859a89c9d544b9c0791d953e0823e51e09396f13e484eccdb240cdd0a0daad8dfe3f8e
6
+ metadata.gz: 6e97629a0a1459a7c9b403a495aea87807f4b2570421e369a56a01afd140830b8284e7d25a86132f14ebdab95275913f66caa8aea48680d23b507dec9d57b519
7
+ data.tar.gz: 284540411644a596492ad2d91c36eecb23f8eb435b95e06781e387b4933a6210680e15b6d79f035b1abd029b726aa9489bdee39816a9d509bfb7a1810b92b00e
data/CHANGELOG.md CHANGED
@@ -8,6 +8,37 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.23.0] - 2026-10-04
12
+
13
+ ### Changed
14
+
15
+ - chi no longer strips the blank lines Splash before 1.2.0 put at the start of an answer (incoai/splash#254, fixed
16
+ in Splash 1.2.0): upgrade Splash with `brew upgrade incoai/tap/splash`.
17
+ - Requests to OpenRouter send `max_tokens: 32768` (a request's own limit, as a recap's, still wins). OpenRouter
18
+ holds each running request's estimated cost against the balance, counting the output `max_tokens` allows (a
19
+ fixed per-request cap without one), so concurrent requests hit "would exceed your available credits given your
20
+ current in-flight requests" (402) sooner without a limit. `default.max_tokens` raises or lowers that limit.
21
+ - `default.n_predict` is renamed `default.max_tokens` (env `SAMAGOTCHI_DEFAULT_MAX_TOKENS`, flag
22
+ `--default-max-tokens`), with no alias: an old `n_predict:` in config.yml warns as an unknown key. It now
23
+ limits chat turns on OpenAI-compatible hosts too, sent as `max_tokens` on any host; it was used only by native
24
+ hosts (where it is still sent as llama.cpp's `n_predict`).
25
+
26
+ ### Fixed
27
+
28
+ - A Ctrl-C (or SIGTERM) while chi loads bundle hooks and plugins stops chi again; before, it was logged as a
29
+ failed hook and chi carried on. A hook's or plugin's own error, `exit` or syntax error is still only reported.
30
+ - A continue turn that fails before it begins (its session save, say) asks the step-limit question again
31
+ instead of crashing the session's worker.
32
+ - Native Gemma 4 prompts follow Gemma 4's chat template: turns end with `<turn|>` (not Gemma 3's
33
+ `<end_of_turn>`, which Gemma 4 reads as plain text), so the stop sequence matches; tool results stay
34
+ inside the model's turn as `response:NAME{value:…}` blocks, one per call; tools are declared in the
35
+ template's compact form at the end of the system turn; an answer's thought is dropped once the next
36
+ user turn starts; and thinking off prefills Gemma's empty thought channel.
37
+
38
+ Update with `chi update`: btw 0.1.2, check-in 0.2.2, known-names 0.1.5, loop-guard 0.3.2, mcp 0.4.2, skills 0.1.6 and
39
+ source-links 0.3.3 move (code style only, no behaviour change). If your config.yml has `default: n_predict:`, rename it
40
+ to `max_tokens:`. Restart `chi web` and running sessions afterwards (`chi sessions restart ID`).
41
+
11
42
  ## [0.22.0] - 2026-10-04
12
43
 
13
44
  ### Added
@@ -1273,7 +1304,8 @@ and long-lived sessions.
1273
1304
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
1274
1305
  hotkey panel that send selected text or the clipboard to your sessions.
1275
1306
 
1276
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.22.0...HEAD
1307
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.23.0...HEAD
1308
+ [0.23.0]: https://github.com/dm1try/samagotchi/compare/v0.22.0...v0.23.0
1277
1309
  [0.22.0]: https://github.com/dm1try/samagotchi/compare/v0.21.0...v0.22.0
1278
1310
  [0.21.0]: https://github.com/dm1try/samagotchi/compare/v0.20.0...v0.21.0
1279
1311
  [0.20.0]: https://github.com/dm1try/samagotchi/compare/v0.19.0...v0.20.0
data/bin/chi CHANGED
@@ -72,6 +72,7 @@ cli_overrides = {}
72
72
  # Strict kebab enforcement: --recap_base_url is unknown (OptionParser would otherwise accept _ as -)
73
73
  ARGV.each do |arg|
74
74
  next unless arg.start_with?("--") && arg.include?("_")
75
+
75
76
  # Allow --no-interrupt etc. already dash; underscore is always rejected for config flags
76
77
  # We check raw ARGV before OptionParser normalizes
77
78
  base = arg.split("=", 2).first
@@ -108,7 +109,7 @@ parser = OptionParser.new do |opts|
108
109
  opts.on("--[no-]shared", "Run the session in a background worker and attach to it, so `chi web` can share it (the default, config session.shared; with --resume: that session); --no-shared runs a plain in-process REPL") { |v| options[:shared] = v }
109
110
  opts.on("--model NAME", "Model for this run, not saved (overrides --resume's; config default.model) [env SAMAGOTCHI_DEFAULT_MODEL]") { |m| options[:model] = m; cli_overrides["default.model"] = m }
110
111
  opts.on("--profile NAME", Samagotchi::ModelProfile::NAMES,
111
- "Prompt profile for every model in this run (#{Samagotchi::ModelProfile::NAMES.join('|')}) [alias for --model-profile]") { |p| cli_overrides["model.profile"] = p }
112
+ "Prompt profile for every model in this run (#{Samagotchi::ModelProfile::NAMES.join("|")}) [alias for --model-profile]") { |p| cli_overrides["model.profile"] = p }
112
113
  opts.on("--thinking LEVEL", Samagotchi::Thinking::LEVELS.map(&:to_s), "How much the model thinks this run: off|low|medium|high|default (outranks config.yml; " \
113
114
  "sessions already running keep theirs) [env SAMAGOTCHI_THINKING_LEVEL]") { |l| cli_overrides["thinking.level"] = l }
114
115
  opts.on("--memory NAME", "Preload a memory entry into the system prompt (repeatable; a comma list too)") { |m| (options[:memories] ||= []) << m }
@@ -133,13 +134,14 @@ parser = OptionParser.new do |opts|
133
134
  next if entry.key == "web.port" # already handled as --port
134
135
  next if entry.key == "default.model" # alias --model above
135
136
  next if entry.key == "web.markdown" # handled below with explicit inverse flags
137
+
136
138
  case entry.type
137
139
  when :bool
138
140
  # --[no-]: a bool that defaults to on is only useful switched off.
139
- opts.on("--[no-]#{entry.cli_flag.delete_prefix('--')}", "Set #{entry.key} (bool, default #{entry.default ? 'on' : 'off'}) [env #{entry.env_key}]") { |v| cli_overrides[entry.key] = v }
141
+ opts.on("--[no-]#{entry.cli_flag.delete_prefix("--")}", "Set #{entry.key} (bool, default #{entry.default ? "on" : "off"}) [env #{entry.env_key}]") { |v| cli_overrides[entry.key] = v }
140
142
  when :integer, :float, :string, :enum
141
143
  desc = "Set #{entry.key} [env #{entry.env_key}]"
142
- desc += " (#{entry.enum_values.join('|')})" if entry.type == :enum
144
+ desc += " (#{entry.enum_values.join("|")})" if entry.type == :enum
143
145
  desc += " (#{value_hints[entry.key]})" if value_hints.key?(entry.key)
144
146
  desc += "; only without a hosts: config" if %w[server.host server.port].include?(entry.key)
145
147
  desc += "; hosts: entries without a transport use it" if entry.key == "server.transport"
@@ -257,7 +259,10 @@ if options[:memories] || options[:muted]
257
259
  end
258
260
  memory_exists = lambda do |raw|
259
261
  scope, name = raw.split("/", 2)
260
- scope, name = nil, raw unless name && Samagotchi::Tools::VALID_SCOPES.include?(scope)
262
+ unless name && Samagotchi::Tools::VALID_SCOPES.include?(scope)
263
+ scope = nil
264
+ name = raw
265
+ end
261
266
  !Samagotchi::Tools::MemoryRead.call(name, scope: scope).start_with?("Error:")
262
267
  end
263
268
  cli_memories = (options[:memories] || []).flat_map { |m| m.split(",") }.map(&:strip).reject(&:empty?)
@@ -308,8 +308,8 @@ prompt profile (see "Prompt profile" below). How the selector reaches the reques
308
308
  (`model: Field required`) without it, so samagotchi forwards the selector
309
309
  resolved to the exact id listed in the server's `/v1/models` — matched by exact
310
310
  (case-insensitive) first, then substring, then passed through unchanged. That
311
- resolved id is usually prefixed (e.g. `mlx-community--gemma-3-4b-it-4bit`), so a
312
- short selector such as `gemma-3-4b-it-4bit` is what you set in
311
+ resolved id is usually prefixed (e.g. `mlx-community--gemma-4-e4b-it-4bit`), so a
312
+ short selector such as `gemma-4-e4b-it-4bit` is what you set in
313
313
  `default.model`. An unknown selector passes through raw and oMLX 404s,
314
314
  listing its available models; if `/v1/models` is unreachable, samagotchi falls
315
315
  back to the raw selector and lets the server decide (its own 400/404). Either
@@ -535,7 +535,7 @@ With several `hosts:`, `host:model` (or an alias naming a host) pins the host.
535
535
  An unqualified model name goes to the default host when its `/models` list has
536
536
  that exact id, else to the first host in `hosts:` order whose list has it, else
537
537
  to the default host. Only exact ids count, never a substring: `/model gemma` on
538
- a box that lists `gemma-3-12b` needs `box:gemma` or the exact id. The lists are
538
+ a box that lists `gemma-4-26b` needs `box:gemma` or the exact id. The lists are
539
539
  known only after `/models` ran (nothing is fetched before the first turn), so
540
540
  until then an unqualified name goes to the default host; use `host:model` to pin
541
541
  one. A **remote** host (an `https` url or a public address; see "Remote or local") keeps its model
@@ -837,7 +837,7 @@ described in their own sections.
837
837
  |---|---|---|---|
838
838
  | `default.model` | (required) | `--model` | The model a new session starts with; `host:model` pins a host. |
839
839
  | `default.input` | none | | Text pre-filled at the first prompt (a trailing space is kept); `--no-default-input` skips it. See [CLI](cli.md). |
840
- | `default.n_predict` | server's | yes | Most tokens one generation may produce (native hosts). |
840
+ | `default.max_tokens` | server's | yes | Most tokens one generation may produce (`n_predict` on a native llama.cpp host; OpenRouter: 32768 when unset). |
841
841
  | `model.profile` | none | `--profile` | Prompt profile for every model (`qwen36`, `gemma4`); env and CLI only. See "Prompt profile". |
842
842
  | `server.transport` | `llama_cpp` | yes | `llama_cpp`, `mlx` or `omlx`; see "Model Server Transport". |
843
843
  | `server.host` | `localhost` | yes | The model server when there is no `hosts:` map. |
data/docs/releasing.md CHANGED
@@ -52,7 +52,7 @@ as the GitHub release body.
52
52
  | `rake bundles:check` | Sha lines match; a bundle changed since the last `v*` tag has a higher version (skipped, with a note, before the first tag). Warns (doesn't fail) when such a bundle's `requires_chi` still admits the tagged chi: raise it if the change uses anything new. |
53
53
  | `rake release:draft_changelog` | A draft Unreleased section from the commit subjects since the last tag, grouped. Stdout only. |
54
54
  | `rake release:bump[X.Y.Z]` | VERSION, the system manifest, Gemfile.lock, CHANGELOG. No commit. |
55
- | `rake release:check` | Clean tracked tree, VERSION == system bundle, a CHANGELOG section, `bundles:check`, rspec + npm test, `gem build`, then a clean install into a temp GEM_HOME that runs `chi --version`, `chi self` and `chi bundle list` with a temp HOME. Needs the network (gem dependencies). |
55
+ | `rake release:check` | Clean tracked tree, VERSION == system bundle, a CHANGELOG section, `bundles:check`, `rake lint` (RuboCop), rspec + npm test, `gem build`, then a clean install into a temp GEM_HOME that runs `chi --version`, `chi self` and `chi bundle list` with a temp HOME. Needs the network (gem dependencies). |
56
56
  | `rake release:notes[X.Y.Z]` | Prints the CHANGELOG section (default: the current VERSION). |
57
57
 
58
58
  ## Runbook
data/docs/testing.md CHANGED
@@ -4,8 +4,14 @@
4
4
  bundle exec parallel_rspec -n 8 # Ruby specs, in parallel (or plain: bundle exec rspec)
5
5
  npm test # web frontend specs
6
6
  npm run e2e # web UI happy paths in Chromium, fake model (once: npx playwright install chromium)
7
+ bundle exec rake lint # RuboCop: no offenses (CI runs it)
7
8
  ```
8
9
 
10
+ Run `bundle exec rubocop -a` on the files you changed before committing: every enabled style cop follows the
11
+ code's own style and has a safe autocorrect, so `-a` fixes them. What it leaves is a Lint finding (a possible
12
+ bug) or a broken spec: fix it, or disable that line with the reason (`# rubocop:disable Cop -- why`).
13
+ `.rubocop.yml` says why each cop that is off is off.
14
+
9
15
  No spec reads your own setup. The suite points `XDG_CONFIG_HOME` and `XDG_STATE_HOME` at temp folders,
10
16
  clears your `SAMAGOTCHI_*` environment, and keeps every spec off the network (WebMock).
11
17
 
@@ -84,12 +84,14 @@ module Samagotchi
84
84
  options = parsed.options
85
85
  return usage_error("give one session id") unless parsed.args.size == 1
86
86
  return usage_error("--question QID is required (chi send --wait prints it)") if options[:question].to_s.strip.empty?
87
+
87
88
  if options[:dismiss]
88
89
  return usage_error("--dismiss takes no --option or --text") if !options[:options].empty? || options[:text]
89
90
  elsif options[:options].empty? && options[:text].nil?
90
91
  return usage_error("nothing to answer with: --option, --text or --dismiss")
91
92
  end
92
93
  return usage_error("--format takes text or json") if options[:format] && !FORMATS.include?(options[:format])
94
+
93
95
  if options[:timeout]
94
96
  options[:timeout] = Float(options[:timeout], exception: false)
95
97
  return usage_error("--timeout takes seconds") unless options[:timeout]&.positive?
@@ -155,7 +157,7 @@ module Samagotchi
155
157
  # (Guardrails::ParentApprovals, with this process's config.yml). A
156
158
  # Deny, text alone or a dismissal always passes: each one denies. The
157
159
  # worker checks it again with its own config.
158
- def approval_refusal(pending, selected, id)
160
+ def approval_refusal(pending, selected, _id)
159
161
  offered = Array(pending[:options]).map(&:to_s)
160
162
  indices = selected.map { |label| offered.index(label) }
161
163
  reason = Guardrails::ParentApprovals.refusal(pending, indices, setting: Guardrails::ParentApprovals.setting)
@@ -170,7 +172,7 @@ module Samagotchi
170
172
 
171
173
  # A step-limit question can't be left unanswered: the turn waits on it.
172
174
  def not_dismissable(pending)
173
- "the step-limit question can't be dismissed: answer #{Array(pending[:options]).join(' or ')} " \
175
+ "the step-limit question can't be dismissed: answer #{Array(pending[:options]).join(" or ")} " \
174
176
  "(--option Stop --text WHY stops it)"
175
177
  end
176
178
 
@@ -15,7 +15,8 @@ module Samagotchi
15
15
  # comments), after a backup, and is checked by parsing it again: every
16
16
  # other key must be unchanged, or the backup goes back.
17
17
  class ConfigWriter
18
- Error = Class.new(StandardError)
18
+ class Error < StandardError
19
+ end
19
20
 
20
21
  # kind: :new (a fresh file), :appended, :exists (the host is already
21
22
  # there as +existing+; nothing written), :snippet (a file chi can't
@@ -18,7 +18,7 @@ module Samagotchi
18
18
  # port answers at once instead of after chi's usual backoff.
19
19
  class Probe
20
20
  # The ports `chi bootstrap` with no target tries on localhost.
21
- LOCAL_PORTS = { 8080 => "llama.cpp", 11434 => "Ollama", 1234 => "LM Studio", 8000 => "vLLM" }.freeze
21
+ LOCAL_PORTS = { 8080 => "llama.cpp", 11_434 => "Ollama", 1234 => "LM Studio", 8000 => "vLLM" }.freeze
22
22
  DEFAULT_PORT = 8080
23
23
  # Connect and read seconds for /props and /v1/models: a remote https
24
24
  # handshake needs more than Client#server_props' 1 s / 2 s.
@@ -193,7 +193,7 @@ module Samagotchi
193
193
  def describe(result)
194
194
  if result.native?
195
195
  build = result.props["build_info"].to_s
196
- @stdout.puts("found: llama.cpp at #{result.candidate.root}#{build.empty? ? "" : " (build #{build})"}")
196
+ @stdout.puts("found: llama.cpp at #{result.candidate.root}#{" (build #{build})" unless build.empty?}")
197
197
  else
198
198
  @stdout.puts("found: an OpenAI-compatible API at #{result.candidate.base}")
199
199
  end
@@ -406,7 +406,7 @@ module Samagotchi
406
406
  shown.first(PICK_SHOWN).each_with_index { |item, i| @stdout.puts(format(" %2d) %s", i + 1, item)) }
407
407
  more = shown.size - PICK_SHOWN
408
408
  @stdout.puts(" … #{more} more: type part of a name to narrow the list") if more.positive?
409
- @stdout.print("#{noun} (number#{items.size > PICK_SHOWN ? " or text" : ""}, empty to stop): ")
409
+ @stdout.print("#{noun} (number#{" or text" if items.size > PICK_SHOWN}, empty to stop): ")
410
410
  answer = @stdin.gets.to_s.strip
411
411
  return nil if answer.empty?
412
412
 
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "thread"
4
-
5
3
  module Samagotchi
6
4
  class Bridge
7
5
  # A thread-safe bounded FIFO used to decouple the turn thread (which
@@ -217,8 +217,6 @@ module Samagotchi
217
217
  "\r\n"
218
218
  )
219
219
  io.flush
220
- rescue Errno::EPIPE, Errno::ECONNRESET, IOError
221
- raise
222
220
  end
223
221
 
224
222
  # Serialise one SSE event: an `id:` line (the replay cursor), an optional
@@ -232,15 +230,11 @@ module Samagotchi
232
230
  JSON.generate(data).each_line { |line| lines << "data: #{line.chomp}" }
233
231
  io.write(lines.join("\r\n") + "\r\n\r\n")
234
232
  io.flush
235
- rescue Errno::EPIPE, Errno::ECONNRESET, IOError
236
- raise
237
233
  end
238
234
 
239
235
  def write_heartbeat(io)
240
236
  io.write(": ping\r\n\r\n")
241
237
  io.flush
242
- rescue Errno::EPIPE, Errno::ECONNRESET, IOError
243
- raise
244
238
  end
245
239
 
246
240
  # Emit a control frame carrying the current snapshot so a client that
@@ -230,8 +230,8 @@ module Samagotchi
230
230
  if Events::TURN_END.include?(type)
231
231
  @turn = nil
232
232
  @merged_origins = []
233
- else
234
- @turn[:event_seq] = event[:event_seq] if event[:event_seq]
233
+ elsif event[:event_seq]
234
+ @turn[:event_seq] = event[:event_seq]
235
235
  end
236
236
  end
237
237
 
@@ -481,7 +481,7 @@ module Samagotchi
481
481
  %w[GET snapshot] => :handle_snapshot,
482
482
  %w[GET tail] => :handle_tail
483
483
  }.freeze
484
- ROUTE_PATH = %r|\A/session/([^/]+)/(.+)\z|u
484
+ ROUTE_PATH = %r{\A/session/([^/]+)/(.+)\z}u
485
485
 
486
486
  # @return [Array(Symbol, String), nil] the handler and the session id, or
487
487
  # nil when no route takes this method and path
@@ -509,7 +509,7 @@ module Samagotchi
509
509
  # /recap in an attached TUI: the saved recap, and a new one asked for at
510
510
  # once (it arrives as :recap_ready). Answers mid-turn too.
511
511
  # 200 {enabled:, saved:, request:, min_user_turns:}. Returns [headers, status, body].
512
- def handle_recap(session_id, _body = nil)
512
+ def handle_recap(_session_id, _body = nil)
513
513
  recap = @engine.recap
514
514
  return [{}, 200, { enabled: false }] unless recap
515
515
 
@@ -519,7 +519,7 @@ module Samagotchi
519
519
 
520
520
  # Cancel the active turn on this session's engine, if any.
521
521
  # Returns [headers, status, body].
522
- def handle_cancel(session_id, body)
522
+ def handle_cancel(_session_id, body)
523
523
  # Optional reason from JSON body
524
524
  reason = :manual
525
525
  if body && !body.strip.empty?
@@ -546,6 +546,7 @@ module Samagotchi
546
546
  unless parsed.is_a?(Hash)
547
547
  return [{ "Allow" => "POST" }, 400, { error: "invalid_json" }]
548
548
  end
549
+
549
550
  qid = fetched(parsed, "id")
550
551
  selected = fetched(parsed, "selected")
551
552
  freeform = fetched(parsed, "freeform")
@@ -556,6 +557,7 @@ module Samagotchi
556
557
  if qid.to_s.strip.empty?
557
558
  return [{ "Allow" => "POST" }, 400, { error: "missing_fields", detail: "id required" }]
558
559
  end
560
+
559
561
  deadline = fetched(parsed, "deadline")
560
562
  return BAD_DEADLINE unless deadline_valid?(deadline)
561
563
  return deadline_passed("answer") if expired?("answer_expired", deadline, sid: session_id, id: qid)
@@ -722,6 +724,7 @@ module Samagotchi
722
724
  @on_exit_request.call(client_id, **options)
723
725
  end
724
726
  return deadline_passed("exit request") if reason == :expired
727
+
725
728
  if reason.nil?
726
729
  return [{}, 200, { status: "restarting", session_id: @session_id }] if restart
727
730
 
@@ -745,7 +748,7 @@ module Samagotchi
745
748
  # dropped with 408 deadline_passed. It is checked with the event log held,
746
749
  # right before the write, so nothing that holds the log (an exit check)
747
750
  # can delay an accepted turn past it. No deadline (an older client): taken.
748
- def handle_post_turn(session_id, body)
751
+ def handle_post_turn(_session_id, body)
749
752
  parsed = parse_json(body)
750
753
  unless parsed.is_a?(Hash)
751
754
  return [{ "Allow" => "POST" }, 400, { error: "invalid_json" }]
@@ -801,7 +804,7 @@ module Samagotchi
801
804
 
802
805
  # Read-only snapshot surface. AC #4: too-old reconnects re-derive state
803
806
  # from here.
804
- def handle_state(session_id, _body = nil)
807
+ def handle_state(_session_id, _body = nil)
805
808
  state = @engine.session_state_snapshot
806
809
  [{}, 200, { session_id: @session_id, session_state_snapshot: state.merge(event_id: event_id(state[:event_seq])) }]
807
810
  end
@@ -809,7 +812,7 @@ module Samagotchi
809
812
  # /stats for an attached client: the metrics, with the window and prompt
810
813
  # profile asked from the server when no turn has reported them yet (so,
811
814
  # unlike /state, it may wait on one short /props GET).
812
- def handle_stats(session_id, _body = nil)
815
+ def handle_stats(_session_id, _body = nil)
813
816
  [{}, 200, { session_id: @session_id, metrics: @engine.stats_snapshot }]
814
817
  end
815
818
 
@@ -817,7 +820,7 @@ module Samagotchi
817
820
  # the messages elsewhere (the web server strips and formats them): it then
818
821
  # streams from the snapshot's event_seq, and the ring replays what came
819
822
  # after (or the stream resets). Returns [headers, status, body].
820
- def handle_snapshot(session_id, _body = nil)
823
+ def handle_snapshot(_session_id, _body = nil)
821
824
  [{}, 200, snapshot_frame]
822
825
  end
823
826
 
@@ -825,7 +828,7 @@ module Samagotchi
825
828
  # that turn's answer (a queued turn's page re-reads late). The one
826
829
  # handler that gets the query (#dispatch); an older worker's route
827
830
  # ignores it and sends the newest answer.
828
- def handle_tail(session_id, _body = nil, query = nil)
831
+ def handle_tail(_session_id, _body = nil, query = nil)
829
832
  turn_id = query && URI.decode_www_form(query).to_h["turn_id"].to_s
830
833
  [{}, 200, tail_frame(turn_id: turn_id.to_s.empty? ? nil : turn_id)]
831
834
  rescue ArgumentError
@@ -885,7 +888,7 @@ module Samagotchi
885
888
  headers = {}
886
889
  content_length = 0
887
890
  while (line = io.gets)
888
- break if line == "\r\n" || line == "\n"
891
+ break if ["\r\n", "\n"].include?(line)
889
892
 
890
893
  key, value = line.split(":", 2)
891
894
  next unless key
@@ -47,7 +47,7 @@ module Samagotchi
47
47
  @client = client
48
48
  @rediscover = rediscover
49
49
  params = []
50
- params << ["snapshot", "1"] if snapshot
50
+ params << %w[snapshot 1] if snapshot
51
51
  params << ["client_id", client_id] if client_id
52
52
  @query = params.empty? ? "" : "?#{URI.encode_www_form(params)}"
53
53
  @reconnect_delays = reconnect_delays
@@ -138,7 +138,7 @@ module Samagotchi
138
138
  @on_event.call(event)
139
139
  end
140
140
  end
141
- rescue EOFError, IOError, SystemCallError
141
+ rescue IOError, SystemCallError # EOFError is an IOError
142
142
  yielded ? :events : :unreachable
143
143
  ensure
144
144
  detach(sock)
@@ -287,7 +287,7 @@ module Samagotchi
287
287
  end
288
288
  chunk = sock.readpartial(4096)
289
289
  yield chunk
290
- rescue EOFError, IOError, Errno::ECONNRESET, Errno::ECONNREFUSED
290
+ rescue IOError, Errno::ECONNRESET, Errno::ECONNREFUSED # EOFError is an IOError
291
291
  break
292
292
  end
293
293
  ensure