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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +33 -1
- data/bin/chi +9 -4
- data/docs/configuration.md +4 -4
- data/docs/releasing.md +1 -1
- data/docs/testing.md +6 -0
- data/lib/samagotchi/answer_command.rb +4 -2
- data/lib/samagotchi/bootstrap/config_writer.rb +2 -1
- data/lib/samagotchi/bootstrap/probe.rb +1 -1
- data/lib/samagotchi/bootstrap_command.rb +2 -2
- data/lib/samagotchi/bridge/bounded_queue.rb +0 -2
- data/lib/samagotchi/bridge/sse_writer.rb +0 -6
- data/lib/samagotchi/bridge/turn_accumulator.rb +2 -2
- data/lib/samagotchi/bridge.rb +12 -9
- data/lib/samagotchi/bridge_client/event_stream.rb +2 -2
- data/lib/samagotchi/bridge_client.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +39 -29
- data/lib/samagotchi/bundles/btw/manifest.yml +2 -2
- data/lib/samagotchi/bundles/btw/plugin.rb +2 -0
- data/lib/samagotchi/bundles/check-in/manifest.yml +2 -2
- data/lib/samagotchi/bundles/check-in/plugin.rb +2 -0
- data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +6 -5
- data/lib/samagotchi/bundles/known-names/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/plugin.rb +2 -0
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/plugin.rb +2 -0
- data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/plugin.rb +5 -3
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +7 -7
- data/lib/samagotchi/bundles/source-links/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/cancellation_controller.rb +1 -1
- data/lib/samagotchi/cli/command.rb +1 -1
- data/lib/samagotchi/client.rb +3 -5
- data/lib/samagotchi/commands/registry.rb +2 -2
- data/lib/samagotchi/config.rb +38 -14
- data/lib/samagotchi/context_note.rb +1 -1
- data/lib/samagotchi/context_status.rb +6 -6
- data/lib/samagotchi/continue_offer.rb +15 -8
- data/lib/samagotchi/debug_log.rb +1 -1
- data/lib/samagotchi/desktop/macos.rb +5 -4
- data/lib/samagotchi/empty_answer_retry.rb +1 -1
- data/lib/samagotchi/engine.rb +17 -17
- data/lib/samagotchi/gem_update.rb +2 -1
- data/lib/samagotchi/guardrail_wiring.rb +1 -1
- data/lib/samagotchi/guardrails/approval.rb +5 -5
- data/lib/samagotchi/guardrails/approvals.rb +2 -2
- data/lib/samagotchi/guardrails/gate.rb +2 -2
- data/lib/samagotchi/guardrails/read_only_shell.rb +5 -3
- data/lib/samagotchi/guardrails/shell_git_dirs.rb +1 -1
- data/lib/samagotchi/guardrails/shell_lex.rb +2 -2
- data/lib/samagotchi/guardrails/shell_paths.rb +2 -2
- data/lib/samagotchi/hooks/bundle_loader.rb +9 -6
- data/lib/samagotchi/hooks/loader.rb +18 -16
- data/lib/samagotchi/hooks/registry.rb +10 -2
- data/lib/samagotchi/host_registry.rb +1 -3
- data/lib/samagotchi/idle_recap.rb +18 -15
- data/lib/samagotchi/idle_reminders.rb +6 -3
- data/lib/samagotchi/image_store.rb +5 -2
- data/lib/samagotchi/kernel_loop.rb +31 -31
- data/lib/samagotchi/llm/chat_loop.rb +9 -6
- data/lib/samagotchi/llm/errors.rb +2 -2
- data/lib/samagotchi/llm/http.rb +12 -12
- data/lib/samagotchi/llm/openai_chat.rb +39 -28
- data/lib/samagotchi/llm/prompt_cache.rb +1 -1
- data/lib/samagotchi/log.rb +2 -2
- data/lib/samagotchi/log_subscriber.rb +2 -1
- data/lib/samagotchi/memory_bundle/builder.rb +9 -3
- data/lib/samagotchi/memory_bundle/index_updater.rb +13 -1
- data/lib/samagotchi/memory_bundle/installer.rb +67 -57
- data/lib/samagotchi/memory_bundle/listing.rb +5 -1
- data/lib/samagotchi/memory_bundle/manifest.rb +14 -4
- data/lib/samagotchi/memory_bundle/merger.rb +6 -4
- data/lib/samagotchi/memory_bundle/provenance.rb +16 -13
- data/lib/samagotchi/memory_bundle/source.rb +7 -3
- data/lib/samagotchi/memory_bundle/status.rb +3 -1
- data/lib/samagotchi/memory_bundle/system_bundle.rb +3 -2
- data/lib/samagotchi/memory_bundle/trash.rb +5 -2
- data/lib/samagotchi/memory_bundle/uninstaller.rb +9 -6
- data/lib/samagotchi/memory_bundle.rb +1 -0
- data/lib/samagotchi/model_overlay.rb +3 -5
- data/lib/samagotchi/model_profile.rb +12 -8
- data/lib/samagotchi/note_command.rb +2 -1
- data/lib/samagotchi/output_formatter.rb +18 -17
- data/lib/samagotchi/owner_lock.rb +2 -1
- data/lib/samagotchi/plugin/api.rb +8 -5
- data/lib/samagotchi/plugin/context.rb +1 -3
- data/lib/samagotchi/plugin/loader.rb +1 -1
- data/lib/samagotchi/plugin/service.rb +1 -1
- data/lib/samagotchi/plugin/sessions.rb +1 -0
- data/lib/samagotchi/plugin_tasks.rb +1 -1
- data/lib/samagotchi/prompt.rb +68 -12
- data/lib/samagotchi/prompt_literal_guard.rb +1 -1
- data/lib/samagotchi/question_desk.rb +7 -7
- data/lib/samagotchi/recap_setup.rb +1 -1
- data/lib/samagotchi/relay_desk.rb +1 -1
- data/lib/samagotchi/reminder_store.rb +2 -1
- data/lib/samagotchi/send_command.rb +4 -2
- data/lib/samagotchi/session.rb +2 -2
- data/lib/samagotchi/session_archive_command.rb +5 -1
- data/lib/samagotchi/session_commands.rb +1 -3
- data/lib/samagotchi/session_manager.rb +2 -4
- data/lib/samagotchi/session_metrics.rb +10 -10
- data/lib/samagotchi/session_observer.rb +3 -2
- data/lib/samagotchi/session_retention.rb +2 -2
- data/lib/samagotchi/sessions_command.rb +14 -5
- data/lib/samagotchi/system_prompt.rb +19 -8
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +5 -5
- data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -2
- data/lib/samagotchi/terminal_ui/event_renderer.rb +1 -2
- data/lib/samagotchi/terminal_ui/formatting.rb +10 -10
- data/lib/samagotchi/terminal_ui/input_support.rb +2 -2
- data/lib/samagotchi/terminal_ui/line_reader.rb +3 -3
- data/lib/samagotchi/terminal_ui/question_prompt.rb +23 -23
- data/lib/samagotchi/terminal_ui/question_slot.rb +10 -10
- data/lib/samagotchi/terminal_ui/reline_seam.rb +1 -1
- data/lib/samagotchi/terminal_ui/repl_input.rb +3 -2
- data/lib/samagotchi/terminal_ui.rb +9 -5
- data/lib/samagotchi/text_diff.rb +12 -12
- data/lib/samagotchi/thinking.rb +6 -2
- data/lib/samagotchi/thought_stream_splitter.rb +6 -3
- data/lib/samagotchi/tool_activity.rb +1 -0
- data/lib/samagotchi/tool_call_parser.rb +13 -14
- data/lib/samagotchi/tool_declarations.rb +36 -35
- data/lib/samagotchi/tools/ask_user_question.rb +3 -4
- data/lib/samagotchi/tools/builtin_calls.rb +1 -1
- data/lib/samagotchi/tools/delegate.rb +1 -1
- data/lib/samagotchi/tools/delegate_relay.rb +1 -1
- data/lib/samagotchi/tools/delegate_wait.rb +0 -1
- data/lib/samagotchi/tools/edit.rb +14 -13
- data/lib/samagotchi/tools/execute.rb +3 -5
- data/lib/samagotchi/tools/list_reminders.rb +1 -1
- data/lib/samagotchi/tools/memory.rb +21 -22
- data/lib/samagotchi/tools/read.rb +7 -7
- data/lib/samagotchi/tools/task_create.rb +1 -1
- data/lib/samagotchi/tools/task_get.rb +1 -1
- data/lib/samagotchi/tools/task_list.rb +1 -1
- data/lib/samagotchi/tools/task_runtime.rb +8 -9
- data/lib/samagotchi/tools/task_stop.rb +1 -1
- data/lib/samagotchi/tools/task_wait.rb +3 -4
- data/lib/samagotchi/tools/web_fetch.rb +2 -4
- data/lib/samagotchi/tools/write.rb +1 -1
- data/lib/samagotchi/turn_flow.rb +8 -8
- data/lib/samagotchi/turn_note.rb +5 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +2 -2
- data/lib/samagotchi/web/app.rb +13 -6
- data/lib/samagotchi/web/markdown_renderer.rb +2 -1
- data/lib/samagotchi/web/qr.rb +7 -1
- data/lib/samagotchi/web/server.rb +6 -2
- data/lib/samagotchi/worker.rb +10 -2
- metadata +20 -20
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 236e301c873eb2b96520aeeb3e7668e535f50174b1afb94d28a149cef161d418
|
|
4
|
+
data.tar.gz: adc3281cace9460e307abe5abc33719041863898e848f10b290d5d2d3e4c93d5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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?)
|
data/docs/configuration.md
CHANGED
|
@@ -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-
|
|
312
|
-
short selector such as `gemma-
|
|
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-
|
|
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.
|
|
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,
|
|
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(
|
|
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
|
|
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",
|
|
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}#{
|
|
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#{
|
|
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
|
|
|
@@ -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
|
-
|
|
234
|
-
@turn[:event_seq] = event[:event_seq]
|
|
233
|
+
elsif event[:event_seq]
|
|
234
|
+
@turn[:event_seq] = event[:event_seq]
|
|
235
235
|
end
|
|
236
236
|
end
|
|
237
237
|
|
data/lib/samagotchi/bridge.rb
CHANGED
|
@@ -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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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 << [
|
|
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
|
|
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
|
|
290
|
+
rescue IOError, Errno::ECONNRESET, Errno::ECONNREFUSED # EOFError is an IOError
|
|
291
291
|
break
|
|
292
292
|
end
|
|
293
293
|
ensure
|