samagotchi 0.3.0 → 0.4.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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +83 -1
  3. data/README.md +16 -0
  4. data/bin/chi +32 -31
  5. data/docs/cli.md +77 -5
  6. data/docs/configuration.md +104 -2
  7. data/docs/desktop.md +39 -4
  8. data/docs/guardrails.md +11 -0
  9. data/docs/hooks.md +88 -6
  10. data/docs/releasing.md +6 -6
  11. data/docs/sessions.md +19 -17
  12. data/lib/samagotchi/bridge/turn_accumulator.rb +1 -0
  13. data/lib/samagotchi/bridge.rb +4 -1
  14. data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +178 -5
  15. data/lib/samagotchi/bundles/source-links/manifest.yml +3 -3
  16. data/lib/samagotchi/bundles/source-links/source_links.md +1 -1
  17. data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
  18. data/lib/samagotchi/bundles/system/delegated.md +6 -7
  19. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  20. data/lib/samagotchi/client.rb +9 -6
  21. data/lib/samagotchi/commands/registry.rb +8 -0
  22. data/lib/samagotchi/config.rb +64 -20
  23. data/lib/samagotchi/desktop/macos/App.swift +12 -8
  24. data/lib/samagotchi/desktop/macos/ChiRunner.swift +4 -2
  25. data/lib/samagotchi/desktop/macos/Images.swift +113 -0
  26. data/lib/samagotchi/desktop/macos/Info.plist.erb +6 -0
  27. data/lib/samagotchi/desktop/macos/Panel.swift +112 -9
  28. data/lib/samagotchi/desktop/macos.rb +59 -8
  29. data/lib/samagotchi/desktop_command.rb +6 -3
  30. data/lib/samagotchi/edit_preview.rb +82 -0
  31. data/lib/samagotchi/engine.rb +201 -104
  32. data/lib/samagotchi/gem_update.rb +89 -0
  33. data/lib/samagotchi/guardrails/approval.rb +26 -4
  34. data/lib/samagotchi/guardrails/load_failures.rb +9 -3
  35. data/lib/samagotchi/host_registry.rb +8 -12
  36. data/lib/samagotchi/idle_client.rb +24 -15
  37. data/lib/samagotchi/idle_reminders.rb +2 -2
  38. data/lib/samagotchi/image_store.rb +10 -6
  39. data/lib/samagotchi/kernel_loop.rb +26 -79
  40. data/lib/samagotchi/live_versions.rb +59 -0
  41. data/lib/samagotchi/llm/api_key.rb +41 -0
  42. data/lib/samagotchi/llm/chat_loop.rb +77 -13
  43. data/lib/samagotchi/llm/errors.rb +21 -7
  44. data/lib/samagotchi/llm/http.rb +15 -4
  45. data/lib/samagotchi/llm/openai_chat.rb +5 -26
  46. data/lib/samagotchi/memory_bundle/installer.rb +65 -63
  47. data/lib/samagotchi/memory_bundle/provenance.rb +51 -12
  48. data/lib/samagotchi/memory_bundle/shipped_update.rb +157 -0
  49. data/lib/samagotchi/memory_bundle/status.rb +4 -1
  50. data/lib/samagotchi/memory_bundle/system_bundle.rb +81 -53
  51. data/lib/samagotchi/model_profile.rb +23 -0
  52. data/lib/samagotchi/prompt.rb +4 -2
  53. data/lib/samagotchi/reminder_store.rb +1 -9
  54. data/lib/samagotchi/self_report.rb +17 -3
  55. data/lib/samagotchi/send_command.rb +107 -12
  56. data/lib/samagotchi/session_commands.rb +38 -8
  57. data/lib/samagotchi/session_manager.rb +1 -16
  58. data/lib/samagotchi/terminal_ui/attached_loop.rb +21 -25
  59. data/lib/samagotchi/terminal_ui/event_renderer.rb +8 -3
  60. data/lib/samagotchi/terminal_ui/formatting.rb +9 -0
  61. data/lib/samagotchi/terminal_ui/input_support.rb +4 -19
  62. data/lib/samagotchi/terminal_ui/question_prompt.rb +35 -0
  63. data/lib/samagotchi/terminal_ui.rb +62 -248
  64. data/lib/samagotchi/text_diff.rb +181 -0
  65. data/lib/samagotchi/thinking.rb +115 -0
  66. data/lib/samagotchi/tool_runner.rb +34 -1
  67. data/lib/samagotchi/tools/ask_user_question.rb +41 -33
  68. data/lib/samagotchi/tools/edit.rb +23 -9
  69. data/lib/samagotchi/tools/write.rb +4 -0
  70. data/lib/samagotchi/turn_flow.rb +12 -2
  71. data/lib/samagotchi/update_command.rb +308 -0
  72. data/lib/samagotchi/update_hint.rb +59 -0
  73. data/lib/samagotchi/version.rb +1 -1
  74. data/lib/samagotchi/vision_support.rb +6 -4
  75. data/lib/samagotchi/web/app.rb +3 -3
  76. data/lib/samagotchi/web/message_parts.rb +8 -3
  77. data/lib/samagotchi/web/public/activity.js +3 -0
  78. data/lib/samagotchi/web/public/app.js +36 -24
  79. data/lib/samagotchi/web/public/chat_view.js +3 -0
  80. data/lib/samagotchi/web/public/data.js +2 -0
  81. data/lib/samagotchi/web/public/diff_view.js +58 -0
  82. data/lib/samagotchi/web/public/index.html +22 -1
  83. data/lib/samagotchi/web/public/question_card.js +3 -1
  84. data/lib/samagotchi/web/public/turn_events.js +29 -5
  85. data/lib/samagotchi/web/public/turn_view.js +2 -1
  86. data/lib/samagotchi/worker.rb +5 -4
  87. metadata +12 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 950fbee37d2110efad37f8d3f8981144f49af9a2ccf75649d5953b1fe7773260
4
- data.tar.gz: aaad7adb0e9aa3c81b601df48fde4ebab965862ed954454c8b79e2de841f0789
3
+ metadata.gz: 63f5d3415cd7f3cfe7f2811ab19bcabca4eafe62e349fd9d0d28fcd157c38340
4
+ data.tar.gz: a2fea7402c8a1cba19e795320c134e12a5cf58b026934cc954c4ecd80e6d765c
5
5
  SHA512:
6
- metadata.gz: 1b4d092f263192c242dead3ae0c556758a38af2f01285dc590362d698696a31d38801c70e64ff85daa33067ceda92be7687915024d1e81802bd0a2c365cc292d
7
- data.tar.gz: 7a06a4d47dea9168e40b0ae0ba7bc6cbfe1b377ba9ac29a998d19f0ee17a766de3186786894deea8b6765a78e21b14e3e7ece8238ab401354c43a5eba4d1815b
6
+ metadata.gz: b8c4eb982683f47f10b9f77af3bc0b32518b8707860fcb796377870bb838ba13d326ece7b2e28f675097507478ec3723206ecd654fe8720e8653bf9a234f6930
7
+ data.tar.gz: fd059521495cca9d033924c818c8b35da945e6354b7806acdf65af7f275e51f9b20f5b228ff00dd81c2f16d73abd8de881d505b7eb3620e0a70792e28f4d7965
data/CHANGELOG.md CHANGED
@@ -8,6 +8,87 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.4.0] - 2026-09-30
12
+
13
+ ### Added
14
+
15
+ - `chi update`: brings an installed chi up to date in one command: the gem
16
+ from rubygems, then the system bundle, the shipped bundles you installed and
17
+ the desktop helper (rebuilt only when its sources changed), shown in one
18
+ table. Your edits to bundle files are kept and reported, running workers and
19
+ an old `chi web` are only reported, `--dry-run` shows the plan, and
20
+ `--no-gem` / `--no-bundles` / `--no-desktop` (or `update.*` in config.yml)
21
+ skip parts. The first start of a new chi version says in one line when
22
+ something can be updated.
23
+ - A thinking level per model, host or run: `thinking: off | low | medium |
24
+ high | default` (`models.<key>.thinking`, `hosts.<name>.thinking`,
25
+ `thinking.level`, `--thinking LEVEL`). Chat hosts get the matching request
26
+ fields; native Qwen and Gemma turn thinking off. chi says once when a level
27
+ can't work on a host, and a host that refuses the thinking fields (gpt-oss
28
+ on OpenRouter) is asked again without them. `/model` and `chi self` show
29
+ the level.
30
+ - Edit previews: an edit/write approval shows the diff it would make, in the
31
+ web card and in the terminal, and every edit/write row gets a
32
+ "diff +3 −1" that opens to the change (live, after a reload and on a join).
33
+ - `chi send --image PATH` (repeatable, up to 20): images go with the message
34
+ as attachments, the same as the web composer's chips; converted and
35
+ downscaled once, then copied into each session. Works with `--new` (the
36
+ session starts idle, then gets the message with its images) and `--wait`.
37
+ A missing file or a non-image stops the send before anything goes out.
38
+ - The desktop helper takes images: a clipboard screenshot, image files from
39
+ Finder's Send to chi, other apps' image data, or a drop on the panel; they
40
+ show as thumbnails and go with the message.
41
+ - source-links 0.3.0: `#12` links to the current project's repo (from its git
42
+ remote) and `owner/repo#12` to that repo; `url:` templates take `{1}`,
43
+ `{name}`, `{repo}` and `{host}`, with `remote:` and `remote_host:` per
44
+ source, and the note lists each link once.
45
+ - The web shows when the provider is asked again after an error ("↻ retrying
46
+ (503) in 3 s, 1/2") or a turn waits for a plugin's setup.
47
+
48
+ ### Changed
49
+
50
+ - A model named with a host that isn't configured (`nosuch:org/model`, or a
51
+ provider's name like `openrouter:x`) is an error that names the host and
52
+ lists the configured ones, in the CLI, `chi send --new`, the web and
53
+ `/model`, instead of going to the default host.
54
+ - The terminal no longer rewrites `#word` into a memory reference: "PR #1"
55
+ and "#ff0000" reach the model as typed (Tab still completes `#name`).
56
+ - A question you dismiss reaches the model as dismissed, not as a tool
57
+ error, so it keeps asking when it should.
58
+ - `/quit` works in the REPL like `/exit`, and the web answers it too;
59
+ `/stats` and `/recap` take trailing words in both terminals.
60
+ - A delegated session gets reworded rules: the answer first, then evidence,
61
+ then what's unverified; follow-up messages arrive as new turns.
62
+
63
+ ### Fixed
64
+
65
+ - A llama.cpp server started with `--api-key` works: the key in
66
+ `api_key_env` is sent on every request (it got 401). A 401/403 from any host
67
+ now says which variable to check or to set `api_key_env`.
68
+ - A bundle upgrade that keeps your edited file no longer disables the
69
+ bundle's plugin, and the system bundle stops warning about the same edit on
70
+ every start.
71
+ - One bundle with a broken manifest.json no longer stops the hooks of the
72
+ bundles after it from loading.
73
+ - A long-running worker picks up edited guardrail rules.
74
+ - `write` without content fails instead of emptying the file.
75
+ - Per-model `vision:` follows a model alias, like `profile:` and `sampling:`.
76
+ - A timeout of 0 in config means the default on llama.cpp hosts too (it was a
77
+ 0-second timeout).
78
+ - A reminder turn drops a pending continue offer, so a later "no" can't roll
79
+ the reminder back.
80
+ - More than 8 options in `ask_user_question` is always an error (one path
81
+ silently used the first 8).
82
+ - A memory read with a comma list counts each name in the REPL and the web.
83
+ - `/archive` or `/quit` typed during a REPL turn no longer goes to the model
84
+ as text; `EXIT --DELETE` works in any case in an attached terminal.
85
+ - `chi bundle status` finds index lines again (it said "no-index" for every
86
+ file).
87
+
88
+ Update with `chi update` (new in this version: from 0.3.0, run
89
+ `gem install samagotchi` once, then `chi update`). Bundle moved:
90
+ source-links 0.3.0.
91
+
11
92
  ## [0.3.0] - 2026-09-29
12
93
 
13
94
  ### Added
@@ -153,6 +234,7 @@ and long-lived sessions.
153
234
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
154
235
  hotkey panel that send selected text or the clipboard to your sessions.
155
236
 
156
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.3.0...HEAD
237
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.4.0...HEAD
238
+ [0.4.0]: https://github.com/dm1try/samagotchi/compare/v0.3.0...v0.4.0
157
239
  [0.3.0]: https://github.com/dm1try/samagotchi/compare/v0.2.0...v0.3.0
158
240
  [0.2.0]: https://github.com/dm1try/samagotchi/releases/tag/v0.2.0
data/README.md CHANGED
@@ -32,6 +32,22 @@ bin/chi self
32
32
  `bin/chi` runs the checkout; `bundle exec rake gem:install` installs it as a
33
33
  local gem, which puts `chi` on your PATH.
34
34
 
35
+ ### Updating
36
+
37
+ ```sh
38
+ chi update --dry-run # what would change
39
+ chi update # the gem, then the bundles and the desktop helper
40
+ ```
41
+
42
+ `chi update` installs the newest samagotchi from rubygems.org (old versions
43
+ stay installed: running sessions still use them), then brings the rest up to
44
+ the new version: the system bundle, the bundles chi ships that you installed,
45
+ and the macOS helper (rebuilt only when its sources changed). Memory files you
46
+ edited are kept, and the table says which. Running sessions move to the new
47
+ chi when their worker idles out (30 min) or on `chi sessions stop ID`; a
48
+ running `chi web` needs a restart. From a checkout, `git pull` instead. See
49
+ [CLI: Updating](docs/cli.md#updating).
50
+
35
51
  ## Set up
36
52
 
37
53
  Point chi at your model server; it works out the rest and writes
data/bin/chi CHANGED
@@ -257,6 +257,12 @@ if ARGV[0] == "bootstrap"
257
257
  exit Samagotchi::BootstrapCommand.new(ARGV[1..] || []).run
258
258
  end
259
259
 
260
+ # `chi update`: bring an installed chi up to date, before OptionParser (its own flags)
261
+ if ARGV[0] == "update"
262
+ require "samagotchi/update_command"
263
+ exit Samagotchi::UpdateCommand.new(ARGV[1..] || []).run
264
+ end
265
+
260
266
  # `chi desktop`: the native "Send to chi" helper, before OptionParser (its own flags)
261
267
  if ARGV[0] == "desktop"
262
268
  require "samagotchi/desktop_command"
@@ -372,7 +378,7 @@ if ARGV[0] == "bundle"
372
378
  lines << ""
373
379
  lines << "Your task: resolve each conflict by editing the file in place using the memory_write tool."
374
380
  lines << "Use memory_read to inspect current content if needed. The correct scope for memory_write is the bundle's scope."
375
- lines << "When all conflicts are resolved, type /exit to finish. If you abort, the upgrade will not be applied."
381
+ lines << "When all conflicts are resolved, type /exit to finish. If you stop early, the files stay as they are (the rest of the upgrade is already applied)."
376
382
  lines.join("\n")
377
383
  end
378
384
 
@@ -529,7 +535,7 @@ if ARGV[0] == "bundle"
529
535
  ans = begin; $stdin.gets; rescue => _e; nil; end
530
536
  launch = ans && ans.strip.downcase.start_with?("y")
531
537
  else
532
- puts "Non-interactive terminal — aborting. Re-run with --force to overwrite or --agent in a TTY."
538
+ puts "Non-interactive terminal: kept your edits in the file(s) above; the rest is upgraded. Re-run with --force to take the bundle's version, or --agent in a TTY to merge."
533
539
  exit 2
534
540
  end
535
541
  if launch
@@ -537,37 +543,15 @@ if ARGV[0] == "bundle"
537
543
  puts "Launching interactive session for conflict resolution… (/exit when done)"
538
544
  require "samagotchi/terminal_ui"
539
545
  Samagotchi::TerminalUI.new(prompt: prompt).run
540
- # After session, re-check if conflicts resolved by re-reading current files vs incoming
541
- # Simple heuristic: if user edited files, current != base now but we assume resolved if file exists
542
- # Re-run upgrade without conflicts? User already edited in place, so we need to write provenance now
543
- # Update provenance to reflect resolved state
544
- if manifest
545
- # Re-collect provenance files from target dir for this bundle
546
- prov_files = {}
547
- target_scope = scope || provenance.read[:scope]&.to_s || "system"
548
- target_dir = Samagotchi::MemoryBundle::Installer.new(source: expanded_source, name: bundle_name, scope: target_scope).send(:resolve_target_dir, target_scope)
549
- # Actually reuse provenance write with current target files for bundle keys
550
- bundle_files = installer.conflicts.keys + installer.results.select { |_, r| %w[installed updated].include?(r[:status]) }.keys
551
- # Fallback: collect all md that exist and were in installer results
552
- all_keys = installer.results.keys
553
- all_keys.each do |k|
554
- p = File.join(target_dir, k)
555
- prov_files[k] = p if File.exist?(p)
556
- end
557
- if prov_files.any?
558
- Samagotchi::MemoryBundle::Provenance.new(name: bundle_name).write(
559
- files: prov_files,
560
- scope: target_scope,
561
- version: manifest.version,
562
- source_path: expanded_source
563
- )
564
- puts "Provenance updated after interactive resolution."
565
- end
566
- end
546
+ # The installer already recorded the upgrade (conflicted files
547
+ # kept their old base); the resolved files now start from the
548
+ # bundle's version.
549
+ Samagotchi::MemoryBundle::Provenance.new(name: bundle_name).resolve_conflicts(installer.conflicts)
550
+ puts "Provenance updated after interactive resolution."
567
551
  puts "Upgrade resolved interactively."
568
552
  exit 0
569
553
  else
570
- puts "Upgrade aborted due to conflicts. Resolve manually or re-run with --force."
554
+ puts "Kept your edits in the file(s) above; the rest is upgraded. chi bundle diff #{bundle_name} FILE shows the base; re-run with --force to take the bundle's version."
571
555
  exit 2
572
556
  end
573
557
  end
@@ -637,6 +621,7 @@ if ARGV[0] == "bundle"
637
621
  puts "Target: #{st[:target_dir]}"
638
622
  st[:files].each do |k, info|
639
623
  mods = []
624
+ mods << "conflict (kept your edits over v#{st[:provenance][:version]}: chi bundle diff #{name} #{k})" if info[:conflict]
640
625
  mods << "modified" if info[:modified]
641
626
  mods << "missing" if info[:missing]
642
627
  mods << "no-index" unless info[:index_present]
@@ -806,6 +791,7 @@ if ARGV[0] == "bundle"
806
791
  line += " (shipped v#{b.upgrade.version}: chi bundle upgrade #{b.upgrade.source})" if b.upgrade
807
792
  puts line
808
793
  end
794
+ puts " (or all at once: chi update)" if installed.any?(&:upgrade)
809
795
  end
810
796
  unless available.empty?
811
797
  puts ""
@@ -930,7 +916,7 @@ ARGV.each do |arg|
930
916
  base = arg.split("=", 2).first
931
917
  # If dashed version is a known flag (registry or hardcoded), reject underscore variant
932
918
  dashed = base.tr("_", "-")
933
- known = %w[--prompt --resume --attach --shared --no-shared --model --memory --mute --no-interrupt --non-interactive --verbose --no-default-input --port --open] + Samagotchi::Config.cli_entries.map(&:cli_flag)
919
+ known = %w[--prompt --resume --attach --shared --no-shared --model --thinking --memory --mute --no-interrupt --non-interactive --verbose --no-default-input --port --open] + Samagotchi::Config.cli_entries.map(&:cli_flag)
934
920
  if known.include?(dashed) || known.include?(base)
935
921
  warn "Unknown option: #{arg} (did you mean #{dashed}?)"
936
922
  exit 1
@@ -949,6 +935,7 @@ parser = OptionParser.new do |opts|
949
935
  chi bundle <install|upgrade|uninstall|status|diff|list|build>
950
936
  chi desktop <install|upgrade|uninstall|status> macOS "Send to chi" helper
951
937
  chi self version, paths, model and bundles
938
+ chi update [--dry-run] update this chi, its bundles and the desktop helper
952
939
  (each subcommand takes --help)
953
940
 
954
941
  BANNER
@@ -959,6 +946,8 @@ parser = OptionParser.new do |opts|
959
946
  opts.on("--model NAME", "Model selector (runtime only, overrides --resume) [alias for --default-model]") { |m| options[:model] = m; cli_overrides["default.model"] = m }
960
947
  opts.on("--profile NAME", Samagotchi::ModelProfile::NAMES,
961
948
  "Prompt profile for every model in this run (#{Samagotchi::ModelProfile::NAMES.join('|')}) [alias for --model-profile]") { |p| cli_overrides["model.profile"] = p }
949
+ 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; " \
950
+ "sessions already running keep theirs) [env SAMAGOTCHI_THINKING_LEVEL]") { |l| cli_overrides["thinking.level"] = l }
962
951
  opts.on("--memory NAME", "Preload a memory entry into the system prompt (repeatable; a comma list too)") { |m| (options[:memories] ||= []) << m }
963
952
  opts.on("--mute NAME", "Hide a memory from this session: not in the prompt, refused by memory_read (repeatable; a comma list too)") { |m| (options[:muted] ||= []) << m }
964
953
  opts.on("--no-interrupt", "Raise the tool call limit to 1000 iterations (useful for long tasks)") { options[:no_interrupt] = true; cli_overrides["no_interrupt"] = true }
@@ -1074,7 +1063,18 @@ when "web"
1074
1063
  end
1075
1064
  end
1076
1065
 
1066
+ # The first interactive start of a new installed chi says what `chi update` would update.
1067
+ chi_update_hint = lambda do
1068
+ require "samagotchi/installed_gem"
1069
+ require "samagotchi/update_hint"
1070
+ if Samagotchi::UpdateHint.wanted?(prompt: options[:prompt], non_interactive: options[:non_interactive],
1071
+ tty: $stdin.tty? && $stdout.tty?, installed: Samagotchi::InstalledGem.spec)
1072
+ Samagotchi::UpdateHint.show
1073
+ end
1074
+ end
1075
+
1077
1076
  if options[:web]
1077
+ chi_update_hint.call
1078
1078
  require "samagotchi/web/server"
1079
1079
  port = options[:web_port] || Samagotchi::Config.get("web.port") || 4567
1080
1080
  # legacy env fallback
@@ -1143,6 +1143,7 @@ if options[:memories] || options[:muted]
1143
1143
  end
1144
1144
  end
1145
1145
 
1146
+ chi_update_hint.call
1146
1147
  require "samagotchi/launch_mode"
1147
1148
  launch, launch_note = if scratch
1148
1149
  [:repl, nil]
data/docs/cli.md CHANGED
@@ -16,9 +16,10 @@
16
16
  - `chi web --no-web-turn-view` — show turns as the classic row of bubbles instead of the default turn view (each turn as one block of steps, the running one at the bottom); `?view=turn|chat` on the page URL overrides it (see [Web turn view](#web-turn-view))
17
17
  - `chi sessions list|stop|archive|unarchive|delete|prune|clean` — manage persisted sessions; `list` shows this git project's, `list --scope=all` every one, a delegated session with `↳ <parent>`, `list --archived` the archived ones too (see [Sessions](sessions.md))
18
18
  - `chi note [--source NAME] [-m TEXT] (ID|PREFIX)... | --all` — add a context note (TEXT or stdin) to sessions: background the model sees on its next turn; it starts no turn (see [Sessions: Context notes](sessions.md#context-notes))
19
- - `chi send [-m TEXT] (ID|PREFIX)...` — send a message to sessions as if typed there: a turn starts (or a running one picks it up); piped stdin goes above `-m` as quoted context (see [Sessions: Sending a message](sessions.md#sending-a-message)); `--new` starts a session with it instead, and `--wait` prints the answer (`--wait ID` with no message waits for the next reply without sending; see [Starting a session](sessions.md#starting-a-session))
19
+ - `chi send [-m TEXT] [--image PATH]... (ID|PREFIX)...` — send a message to sessions as if typed there: a turn starts (or a running one picks it up); piped stdin goes above `-m` as quoted context, and `--image` attaches images (see [Sessions: Sending a message](sessions.md#sending-a-message)); `--new` starts a session with it instead, and `--wait` prints the answer (`--wait ID` with no message waits for the next reply without sending; see [Starting a session](sessions.md#starting-a-session))
20
20
  - `chi desktop install|upgrade|uninstall|status` — the macOS "Send to chi" helper: a Service and a ⌃⌥⌘N hotkey that send text to live sessions as context notes (see [Desktop helper](desktop.md))
21
21
  - `chi self` — print version, source dir (checkout or installed gem), config/memory/session paths, model/host and bundles
22
+ - `chi update [--dry-run] [--no-gem] [--no-bundles] [--no-desktop]` — update an installed chi: the gem, the system bundle, the shipped bundles you installed and the desktop helper, in one table (see [Updating](#updating))
22
23
  - `chi bundle install|upgrade|uninstall|status|diff|list|build` — manage memory bundles (see [Bundle hooks](hooks.md#bundle-hooks-unified-workflow-bundle)); `list` shows the installed ones and the ones shipped with chi, which `install <name>` installs (see [Guardrails](guardrails.md), [Plugins](plugins.md#the-btw-bundle), [the mcp bundle](plugins.md#the-mcp-bundle) [the loop-guard bundle](plugins.md#the-loop-guard-bundle) and [the check-in bundle](plugins.md#the-check-in-bundle))
23
24
 
24
25
  ### First setup
@@ -57,6 +58,65 @@ chi bootstrap # try localhost 8080, 11434, 1234, 8000
57
58
  or with anchors gets the lines printed to paste instead. `--dry-run` shows
58
59
  what it would write.
59
60
 
61
+ ### Updating
62
+
63
+ `chi update` brings an installed chi up to date and prints one table:
64
+
65
+ ```
66
+ component from to status
67
+ chi (gem) 0.2.0 0.3.0 updated
68
+ system bundle 0.2.0 0.3.0 updated (kept your edits in identity.md: chi bundle diff samagotchi-system identity.md)
69
+ btw 0.1.1 up to date
70
+ known-names 0.1.0 0.1.1 updated
71
+ infra_tools 1.0.0 skipped (not from chi)
72
+ Chi Helper 0.2.0 up to date (launch file refreshed)
73
+ workers 2 live on 0.2.0: they move to 0.3.0 at idle exit (30 min) or chi sessions stop 2ea8c1f0 91b0d2aa
74
+ Also shipped, not installed: check-in, source-links (chi bundle install NAME)
75
+ done
76
+ ```
77
+
78
+ - **The gem.** It asks rubygems.org for the newest samagotchi (5 s timeout)
79
+ and, when that's newer, runs `gem install samagotchi` with the gem command
80
+ of the Ruby chi runs on (the real one, not a mise/rbenv/asdf shim). Then it
81
+ hands over to the new chi, which does the rest and prints the table. Old
82
+ versions stay installed: running workers and an old `chi web` still use
83
+ them (so don't `gem cleanup` while they run). Offline, the row says
84
+ "couldn't check" and the rest still runs; a failed install fails the row
85
+ and the rest runs on the current version. Under Bundler (`bundle exec`)
86
+ the row says `bundle update samagotchi` instead.
87
+ - **The system bundle** normally updated itself when the new chi started;
88
+ the row says what it did.
89
+ - **Shipped bundles**: each one you installed from chi (`chi bundle install
90
+ NAME`) is upgraded when chi ships a newer version. Memory files get the
91
+ 3-way merge of `chi bundle upgrade`: an unedited file is updated, an edited
92
+ one that the new version also changes is kept, and the row says so (`chi
93
+ bundle diff NAME FILE` shows it; `chi bundle upgrade NAME --force` takes the
94
+ bundle's). Hooks, rules and the plugin are replaced; an edited one didn't
95
+ load anyway (its sha no longer matched) and the row says it was replaced.
96
+ A bundle of the same name from elsewhere (a zip, git) is skipped ("not from
97
+ chi"), a newer installed one is left, one whose new version needs a newer
98
+ chi is skipped, and bundles you didn't install stay uninstalled.
99
+ - **The desktop helper** (macOS) is rebuilt and restarted only when its Swift
100
+ sources changed (or the Ruby it runs moved); otherwise only its launch file
101
+ is refreshed. See [Desktop helper](desktop.md).
102
+ - **Running processes** are reported, never stopped: live workers on another
103
+ version, and a `chi web` on `web.port` running an older chi (sessions it
104
+ starts run that version too: restart it).
105
+
106
+ `--dry-run` shows the table with "would update" and changes nothing. It is
107
+ this version's view: bundles that only a newer gem ships newer show up once
108
+ that gem is installed (the real run installs it first and hands over).
109
+ `--no-gem`, `--no-bundles` and `--no-desktop` leave a part alone for one run;
110
+ `update.gem`, `update.bundles` and `update.desktop: false` in config.yml turn
111
+ one off for good. It exits 0 when nothing failed (kept edits and skips are
112
+ fine), 1 when a part failed, 2 on a usage error. A second run changes nothing
113
+ and ends with "everything is up to date".
114
+
115
+ From a checkout it refuses (`git pull`, or `chi bundle upgrade NAME` for one
116
+ bundle). After a gem update, the first interactive start of the new version
117
+ (`chi`, `chi web`; not `-p` or `--non-interactive`) says in one line when
118
+ bundles or the helper can be updated.
119
+
60
120
  ## Flags
61
121
 
62
122
  Samagotchi exposes one flag that feeds a prompt (`-p`, `--prompt`) and one that
@@ -71,6 +131,7 @@ controls exit behavior (`--non-interactive`); `--resume` composes with both.
71
131
  | `--no-shared` | Run the plain in-process REPL for this run. |
72
132
  | `--attach SESSION_ID` | Attach to a session's worker, waking one if it has exited. |
73
133
  | `--model NAME` | Use this model for the run (overrides the configured default and a resumed session's model). |
134
+ | `--thinking LEVEL` | How much the model thinks this run: `off`, `low`, `medium`, `high` or `default` (env `SAMAGOTCHI_THINKING_LEVEL`), over the config's levels. A session already running keeps its own. See "Thinking" in configuration.md. |
74
135
  | `--profile NAME` | Prompt profile (`qwen36` or `gemma4`) for every model in this run, over config and the server's template (same as `--model-profile`, env `SAMAGOTCHI_MODEL_PROFILE`). See "Prompt profile" in configuration.md. |
75
136
  | `--memory NAME` | Preload a memory entry into the system prompt (repeatable; a comma list too). Merged under the config.yml `memories:` baseline. Works attached: the list is stored on the session, so its worker builds the same prompt on every respawn. |
76
137
  | `--mute NAME` | Hide a memory from this session (repeatable; a comma list too): its index line is not in the prompt, `memory_read` refuses it, the identity auto-load skips it, and it is dropped from the preloads (config baseline or `--memory`). A name matches in both scopes (`gh-helper`, `project/gh-helper` and `gh-helper.md` all hide `gh-helper`). Nothing on disk changes. See [Muting a memory](#muting-a-memory). |
@@ -279,7 +340,7 @@ REPL alike:
279
340
  typed comes back once it closes. The choices then go, and one line stays:
280
341
  `? Pick a fruit → Banana`.
281
342
  - Ctrl-C cancels the turn and keeps what you typed.
282
- - In the plain REPL, Ctrl-D on an empty prompt (or `exit`, `/exit`) mid-turn
343
+ - In the plain REPL, Ctrl-D on an empty prompt (or `exit`, `/exit`, `/quit`) mid-turn
283
344
  exits once the turn ends: `(exits after this turn; Ctrl-C cancels it)`
284
345
  (`/exit --delete` deletes the session then too). In an
285
346
  attached terminal it detaches at once and the turn goes on in the worker
@@ -290,7 +351,7 @@ turns.
290
351
 
291
352
  ### Images
292
353
 
293
- A model that can see images gets them three ways:
354
+ A model that can see images gets them these ways:
294
355
 
295
356
  - **`@path` in a prompt** (REPL, attached terminal, `-p`): `what's wrong in
296
357
  @shot.png?`, `@~/Desktop/a.jpg`, `@"my shot.png"`. Each `@` token that names an
@@ -305,6 +366,12 @@ A model that can see images gets them three ways:
305
366
  - **The Web UI**: paste or drop images into the composer. Each shows as a chip
306
367
  (× removes it) and is sent with the message; an image alone is sent as
307
368
  `[image: name]`. Messages show thumbnails; a click opens one full size.
369
+ - **`chi send --image PATH`** (repeatable, up to 20) with a message, from a
370
+ script or another terminal: `chi send --image shot.png -m "why is this red?"
371
+ 3fa2`. The attached terminal and the web show it like an image typed there.
372
+ - **The desktop panel** (macOS, [Desktop](desktop.md#images)): a screenshot on
373
+ the clipboard, an image selected in Finder, or one dropped on the panel goes
374
+ as an attachment with the message.
308
375
 
309
376
  Images are downscaled to a 1568 px long side (with `sips` on macOS or
310
377
  ImageMagick; without either, a larger image is refused with a hint) and stored
@@ -375,7 +442,10 @@ from the saved messages (each step's thinking, narration, tool parameters
375
442
  and output, the output capped at 2000 characters) and the timing records
376
443
  (status and duration per row). On an `api: openai` host the model's
377
444
  reasoning is saved with each step for this (never sent back to the model);
378
- steps saved before that have none, so they show no thinking.
445
+ steps saved before that have none, so they show no thinking. An `edit` or
446
+ `write` row has a closed `diff +3 −1` under it that opens to the change it
447
+ made (up to 120 lines or 8 KB), live and after a reload (see
448
+ [Guardrails](guardrails.md#ask) for the diff an approval shows first).
379
449
 
380
450
  ```sh
381
451
  chi web --no-web-turn-view # the classic chat view; --web-turn-view is the default
@@ -582,7 +652,9 @@ During assist-mode thinking (while the spinner is active), you can cancel an in-
582
652
  Behavior notes:
583
653
 
584
654
  - Cancellation returns control to the prompt immediately; what you typed there stays.
585
- - Partial model output from the canceled request is not committed as a completed model turn.
655
+ - Visible text the canceled request had streamed stays in the conversation, marked `[interrupted]`, so the next
656
+ message (or a continue) picks up from the half-finished reply; the canceled request's thinking and any unfinished
657
+ tool call are dropped.
586
658
 
587
659
  ## Iteration Limit Behavior
588
660
 
@@ -29,6 +29,7 @@ server: # the model server when there is no hosts: map below
29
29
  port: 8081
30
30
  thinking:
31
31
  ui: spinner
32
+ level: default # off | low | medium | high | default; see "Thinking"
32
33
 
33
34
  # Multi-host (optional): aggregated /models and per-model routing.
34
35
  # A bare default.model uses the default host; host:model pins to a host.
@@ -214,6 +215,20 @@ hosts:
214
215
 
215
216
  `chi self` shows the variable and whether it is set (`api key FIREWORKS_API_KEY (set)`).
216
217
 
218
+ Every host with `api_key_env:` sends `Authorization: Bearer <key>` on each request,
219
+ chat or raw-prompt alike, so a llama.cpp started with `--api-key` works as a native
220
+ host too (`chi bootstrap --key-env VAR` writes such an entry). An unset variable
221
+ fails the turn before any request (`set VAR`); a 401/403 names the variable to
222
+ check, or, on a host without `api_key_env:`, suggests adding it:
223
+
224
+ ```yaml
225
+ hosts:
226
+ box:
227
+ host: 192.0.2.20
228
+ port: 8080
229
+ api_key_env: BOX_LLAMA_KEY
230
+ ```
231
+
217
232
  For models on that host, chi uses the chat loop (its own OpenAI chat adapter): it
218
233
  takes the OpenAI base (`url:`, else `http://HOST:PORT/v1`) and streams messages plus
219
234
  function schemas from `/v1/chat/completions`; the model's reasoning (`reasoning_content`)
@@ -355,6 +370,70 @@ models:
355
370
  `SAMAGOTCHI_HOSTS_JSON`, and reads `models:` from the config file each turn: after changing a host's `sampling:`,
356
371
  stop the session's worker (`chi sessions stop`) for it to take effect.
357
372
 
373
+ ## Thinking
374
+
375
+ How much a model thinks before it answers. One level, `off`, `low`, `medium`, `high` or `default`, set per model,
376
+ per host or for everything:
377
+
378
+ ```yaml
379
+ thinking:
380
+ level: default # every model without its own level
381
+ hosts:
382
+ openrouter:
383
+ url: https://openrouter.ai/api/v1
384
+ api: openai
385
+ api_key_env: OPENROUTER_API_KEY
386
+ thinking: low
387
+ models:
388
+ qwen3.6-35b-a3b:
389
+ thinking: off # unquoted off works (YAML reads it as false)
390
+ ```
391
+
392
+ - `default` sends nothing: the provider's or the chat template's own default, which is chi's behaviour without the
393
+ setting. For some hybrid models that default is *no* thinking (DeepSeek V3.1 on OpenRouter); `medium` turns it on.
394
+ `on` isn't a level.
395
+ - Order, first set wins: `--thinking LEVEL` or `SAMAGOTCHI_THINKING_LEVEL`, then the `models:` entry (found the way
396
+ `profile:` is), then the `hosts:` entry, then `thinking.level` in the file, then `default`. Anything else than a
397
+ level warns once and counts as unset.
398
+ - The flag reaches the sessions that start with it; a session already running keeps its level. `models:` levels
399
+ and `thinking.level` in the file are read every turn; a host's `thinking:` reaches a worker when it starts, as
400
+ its `sampling:` does (`chi sessions stop` to change it).
401
+ - `/model` shows the level and where it came from (`thinking: off (models: qwen3.6-35b-a3b)`), `chi self` too.
402
+ - The idle recap and plugins' side questions always ask with thinking off, whatever the level.
403
+
404
+ What each backend gets:
405
+
406
+ | Backend | `off` | `low` / `medium` / `high` |
407
+ |---|---|---|
408
+ | native (`/completion`), `qwen36` | an empty thought after the assistant cue, and no turn preamble | no knob: thinking stays as the model has it, one notice |
409
+ | native, `gemma4` | no `<\|think\|>` token at the start of the system prompt | no knob, one notice |
410
+ | chat host (`api: openai`) | `chat_template_kwargs: {enable_thinking: false}` and `reasoning_effort: "none"` | `reasoning_effort: <level>` |
411
+
412
+ On chat hosts: llama.cpp honours both off switches but ignores the effort; Splash takes `reasoning_effort` (off only
413
+ through `none`) and scales with it; OpenRouter translates `reasoning_effort` per model (some can't turn thinking off:
414
+ Qwen3-30B-A3B thinks anyway, gpt-oss refuses).
415
+
416
+ When the model thinks although the level is `off`, chi says so once per session and host
417
+ (`thinking> warning: off wasn't honoured by … (N chars of thinking)`) and logs `thinking_not_honoured` each time.
418
+ When a host answers the thinking fields with an HTTP 400 about reasoning (gpt-oss: "Reasoning is mandatory"), chi
419
+ sends the request again without them, leaves them out for that model from then on, and says so once.
420
+
421
+ The fields go under the `sampling:` map ("Sampling"): a `sampling:` key wins over the level's, and
422
+ `chat_template_kwargs` merges per sub-key. A `null` there drops a field the level would send, at any depth, for a
423
+ host that refuses one of them:
424
+
425
+ ```yaml
426
+ hosts:
427
+ strict:
428
+ url: https://llm.example.com/v1
429
+ api: openai
430
+ thinking: off
431
+ sampling: { reasoning_effort: null } # sends only enable_thinking: false
432
+ ```
433
+
434
+ A different level changes a native model's system prompt (Gemma's token, Qwen's turn preamble), so the next turn
435
+ reads the whole context again once; on a chat host only the end of the prompt changes.
436
+
358
437
  ## Llama HTTP Timeouts
359
438
 
360
439
  Long-running llama.cpp completions can exceed Ruby's default HTTP read timeout.
@@ -362,6 +441,7 @@ Raise these to avoid premature request failures:
362
441
 
363
442
  - `server.open_timeout` (default: `10`, env `SAMAGOTCHI_SERVER_OPEN_TIMEOUT`) connection timeout in seconds.
364
443
  - `server.read_timeout` (default: `600`, env `SAMAGOTCHI_SERVER_READ_TIMEOUT`) response read timeout in seconds.
444
+ Either timeout at `0` (or anything not a positive number) is its default, on every host.
365
445
 
366
446
  ```yaml
367
447
  server:
@@ -409,15 +489,32 @@ the running server (llama.cpp's `/props`), else the window the host's model list
409
489
  gives (`context_length`, `context_window`, `max_model_len` or llama.cpp's
410
490
  `meta.n_ctx`), else `context.window_tokens`.
411
491
 
492
+ A `:` in a model name is often part of the id (`qwen3:8b`, `mistral:7b`,
493
+ `unsloth/Qwen3-8B-GGUF:Q4_K_M`), so the part before the first `:` picks a host
494
+ only when it is a configured host's name. An unknown prefix is refused with an
495
+ error naming it and the configured hosts (with a "did you mean" for a near
496
+ miss) when either
497
+ - the rest is an `org/model` id (`nosuch:anthropic/claude-sonnet-4`), or
498
+ - the prefix is a hosted provider's name: `openrouter`, `openai`, `anthropic`,
499
+ `google`, `gemini`, `groq`, `xai`, `together` or `fireworks`
500
+ (`openai:gpt-4o` with no `openai` host).
501
+
502
+ The check applies wherever the model comes in: `--model`, `default.model`, an
503
+ alias, `/model`, `chi send --new --model`, the web's new-session model and a
504
+ delegate's model. Any other unknown prefix (`nosuch:x`) is sent to the default
505
+ host as the model id.
506
+
412
507
  ## Llama Network Retry Behavior
413
508
 
414
509
  Transient network failures are retried automatically with exponential backoff.
415
510
 
416
511
  - Default retries: `5` (up to `6` total attempts including the first call).
417
512
  - Default backoff: `0.5s`, `1s`, `2s`, `4s`, `8s`.
418
- - Retry scope: transient network errors (timeouts, refused/reset connections, EOF/socket reachability failures),
513
+ - Retry scope: transient network errors (timeouts, reset connections, EOF/socket reachability failures),
419
514
  HTTP 429 and HTTP 500/502/503/504/529. A `Retry-After` header replaces the backoff delay; one longer than
420
515
  60s is not waited out and the error is reported instead.
516
+ - A refused connection (nothing listening) is not retried: the turn fails at once with `can't reach host <name> at
517
+ <address> (connection refused) — is the server running?`.
421
518
  - A stream that has already produced output is never retried (the retry would repeat it); it fails the turn.
422
519
  - Cancellation (`Ctrl-C`) is never retried.
423
520
 
@@ -455,7 +552,7 @@ message (before, a failed llama.cpp `/completion` ended the turn as
455
552
 
456
553
  | Kind | When | Retried |
457
554
  |---|---|---|
458
- | connection | refused, reset, timed out, dropped mid-stream | yes (network retry), not mid-stream |
555
+ | connection | reset, timed out, dropped mid-stream; refused | yes (network retry), not mid-stream; refused: no |
459
556
  | rate limited | HTTP 429 | yes, honouring `Retry-After` |
460
557
  | server | HTTP 5xx, llama.cpp's mid-stream `error:` event | 500/502/503/504/529 only |
461
558
  | auth | HTTP 401/403 | no |
@@ -651,11 +748,16 @@ described in their own sections.
651
748
  | `thinking.ui` | `spinner` | yes | `spinner` or `off`. |
652
749
  | `thinking.render_interval` | `0.08` | yes | Seconds between thinking redraws. |
653
750
  | `thinking.turn_preamble` | `true` | yes | Ask a `qwen36` model to open its thinking with a short `TURN:` line (the step label). |
751
+ | `thinking.level` | `default` | `--thinking` | `off`, `low`, `medium`, `high` or `default` for every model; the flag and env outrank the `models:`/`hosts:` entries, the file's value doesn't. See "Thinking". |
752
+ | `models.<key>.thinking`, `hosts.<name>.thinking` | none | | A model's or host's level. See "Thinking". |
654
753
  | `max_tool_output_chars` | `10000` | yes | Tool output kept in the conversation; a top-level key (see below). |
655
754
  | `retry.max` | `5` | yes | See "Llama Network Retry Behavior". |
656
755
  | `retry.base_delay` | `0.5` | yes | |
657
756
  | `retry.max_delay` | `8.0` | yes | |
658
757
  | `retry.empty_answer` | `1` | | Times a turn asks again after an empty answer (at most 3, `0` = off). See "Llama Network Retry Behavior". |
758
+ | `update.gem` | `true` | | `false`: `chi update` never installs a newer gem (`--no-gem` for one run). See [CLI: Updating](cli.md#updating). |
759
+ | `update.bundles` | `true` | | `false`: `chi update` leaves the shipped bundles to `chi bundle upgrade` (`--no-bundles`). |
760
+ | `update.desktop` | `true` | | `false`: `chi update` leaves the desktop helper alone (`--no-desktop`). |
659
761
  | `read.truncate_at_bytes` | `65536` | yes | A `read` result larger than this is cut to a preview. |
660
762
  | `read.preview_bytes` | `12288` | yes | Size of that preview. |
661
763
  | `read.hard_max_bytes` | `2097152` | yes | Largest file `read` opens. |