samagotchi 0.6.0 → 0.8.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 (105) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +72 -1
  3. data/README.md +7 -3
  4. data/docs/architecture.md +4 -4
  5. data/docs/cli.md +19 -4
  6. data/docs/configuration.md +20 -0
  7. data/docs/desktop.md +49 -0
  8. data/docs/guardrails.md +2 -2
  9. data/docs/hooks.md +2 -2
  10. data/docs/internals/context-telemetry.md +3 -1
  11. data/docs/memory.md +48 -1
  12. data/docs/plugins.md +7 -5
  13. data/docs/releasing.md +5 -1
  14. data/docs/sessions.md +2 -2
  15. data/lib/samagotchi/archive_store.rb +3 -3
  16. data/lib/samagotchi/atomic_file.rb +34 -0
  17. data/lib/samagotchi/bootstrap/bundles.rb +37 -0
  18. data/lib/samagotchi/bootstrap/config_writer.rb +4 -6
  19. data/lib/samagotchi/bootstrap/probe.rb +9 -5
  20. data/lib/samagotchi/bootstrap_command.rb +78 -8
  21. data/lib/samagotchi/bridge/pending_card.rb +2 -3
  22. data/lib/samagotchi/bridge.rb +56 -116
  23. data/lib/samagotchi/bridge_client.rb +7 -11
  24. data/lib/samagotchi/bundle_command.rb +81 -21
  25. data/lib/samagotchi/bundle_needs.rb +2 -11
  26. data/lib/samagotchi/bundles/core/manifest.yml +11 -0
  27. data/lib/samagotchi/bundles/dev/manifest.yml +13 -0
  28. data/lib/samagotchi/bundles/system/identity.md +1 -0
  29. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  30. data/lib/samagotchi/bundles/system/memory_guide.md +3 -2
  31. data/lib/samagotchi/client.rb +3 -23
  32. data/lib/samagotchi/config.rb +11 -11
  33. data/lib/samagotchi/context_note.rb +1 -1
  34. data/lib/samagotchi/context_status.rb +255 -0
  35. data/lib/samagotchi/desktop/macos/App.swift +5 -1
  36. data/lib/samagotchi/desktop/macos/ChiRunner.swift +7 -63
  37. data/lib/samagotchi/desktop/macos/Images.swift +38 -3
  38. data/lib/samagotchi/desktop/macos/Kitty.swift +346 -0
  39. data/lib/samagotchi/desktop/macos/Panel.swift +233 -55
  40. data/lib/samagotchi/desktop/macos/ProcessRunner.swift +74 -0
  41. data/lib/samagotchi/desktop/macos.rb +25 -11
  42. data/lib/samagotchi/desktop_command.rb +1 -1
  43. data/lib/samagotchi/engine.rb +6 -7
  44. data/lib/samagotchi/guardrail_wiring.rb +2 -5
  45. data/lib/samagotchi/guardrails/approvals.rb +2 -7
  46. data/lib/samagotchi/hooks/bundle_loader.rb +6 -3
  47. data/lib/samagotchi/hooks/loader.rb +2 -1
  48. data/lib/samagotchi/hooks/registry.rb +21 -0
  49. data/lib/samagotchi/image_store.rb +2 -3
  50. data/lib/samagotchi/kernel_loop.rb +21 -283
  51. data/lib/samagotchi/live_versions.rb +9 -13
  52. data/lib/samagotchi/llm/chat_loop.rb +1 -1
  53. data/lib/samagotchi/llm/native_backend.rb +8 -8
  54. data/lib/samagotchi/llm/openai_chat.rb +4 -17
  55. data/lib/samagotchi/llm/utf8_scrub.rb +32 -0
  56. data/lib/samagotchi/log_path.rb +2 -3
  57. data/lib/samagotchi/memory_bundle/index_updater.rb +2 -11
  58. data/lib/samagotchi/memory_bundle/installer.rb +5 -7
  59. data/lib/samagotchi/memory_bundle/listing.rb +27 -20
  60. data/lib/samagotchi/memory_bundle/manifest.rb +32 -1
  61. data/lib/samagotchi/memory_bundle/profile.rb +145 -0
  62. data/lib/samagotchi/memory_bundle/provenance.rb +42 -53
  63. data/lib/samagotchi/memory_bundle/shipped_update.rb +52 -27
  64. data/lib/samagotchi/memory_bundle/source.rb +2 -0
  65. data/lib/samagotchi/memory_bundle/status.rb +1 -2
  66. data/lib/samagotchi/memory_bundle.rb +1 -0
  67. data/lib/samagotchi/note_command.rb +5 -4
  68. data/lib/samagotchi/owner_lock.rb +0 -6
  69. data/lib/samagotchi/paths.rb +32 -0
  70. data/lib/samagotchi/plugin/api.rb +3 -6
  71. data/lib/samagotchi/plugin/context.rb +2 -3
  72. data/lib/samagotchi/plugin/loader.rb +1 -1
  73. data/lib/samagotchi/question_desk.rb +3 -2
  74. data/lib/samagotchi/recap_store.rb +2 -2
  75. data/lib/samagotchi/reply_wait.rb +2 -1
  76. data/lib/samagotchi/self_report.rb +2 -3
  77. data/lib/samagotchi/send_command.rb +3 -2
  78. data/lib/samagotchi/session.rb +41 -143
  79. data/lib/samagotchi/session_inbox.rb +175 -0
  80. data/lib/samagotchi/session_manager.rb +62 -363
  81. data/lib/samagotchi/session_metrics.rb +2 -3
  82. data/lib/samagotchi/session_retention.rb +259 -0
  83. data/lib/samagotchi/sessions_command.rb +3 -2
  84. data/lib/samagotchi/terminal_ui/attached_loop.rb +1 -2
  85. data/lib/samagotchi/terminal_ui/input_support.rb +2 -9
  86. data/lib/samagotchi/terminal_ui.rb +10 -20
  87. data/lib/samagotchi/tools/memory.rb +3 -2
  88. data/lib/samagotchi/tools/send_note.rb +4 -3
  89. data/lib/samagotchi/tools/task_runtime.rb +2 -3
  90. data/lib/samagotchi/turn_note.rb +13 -2
  91. data/lib/samagotchi/update_command.rb +10 -3
  92. data/lib/samagotchi/version.rb +1 -1
  93. data/lib/samagotchi/web/app.rb +44 -76
  94. data/lib/samagotchi/web/info_probe.rb +33 -0
  95. data/lib/samagotchi/web/public/app.js +1 -1
  96. data/lib/samagotchi/web/public/index.html +1 -0
  97. data/lib/samagotchi/web/public/stage_model.js +22 -3
  98. data/lib/samagotchi/web/public/stage_view.js +25 -10
  99. data/lib/samagotchi/web/server.rb +8 -11
  100. data/lib/samagotchi/web/session_hub.rb +8 -7
  101. data/lib/samagotchi/web/session_summary.rb +4 -3
  102. data/lib/samagotchi/web/token.rb +2 -5
  103. data/lib/samagotchi/worker.rb +34 -20
  104. data/lib/samagotchi/worker_sidecar.rb +42 -0
  105. metadata +15 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 220ee751308788beff6b5185350bafe29023706f2e63daf45c8bffab023e173e
4
- data.tar.gz: 184c4bc6f92476939901813d8a9b20353a73bb64eb5725d3564ca302bf27559e
3
+ metadata.gz: 4db526cc3c79a4e5aa8061f124ff09e65e289415074fcb51c71c25ccee1ab0c9
4
+ data.tar.gz: c30c2094cb357df9fe4e5618956e0c1a559ea0a2765709605488d03c5febbb87
5
5
  SHA512:
6
- metadata.gz: 7349948ddc41e8ccce71e742d4fc81f51f52290b56a2943efc690adc6e73e4c41fe37fa94dee41c50e20a0c910b3c66a55176955048bf3efa2003e9520c36642
7
- data.tar.gz: a2059d2df83be31064bfb05f311c7088e777d2901fa00695f4f52208af3b21038471d7180ad26761b6f3f280a7b2f347f560ca39517e0bfcd52715247ba88477
6
+ metadata.gz: 2ad7fd71ecfee430c9dec6d9b51971b1e7fdc38c099c5f4a868ad07add50a21ac293f6c686e5f8aca9b0d3bd919ee49e1d79336ac1fe1ac4926d6ace2de24adb
7
+ data.tar.gz: 5856a9fe2fcb23b4802626ee918e7015cc718d74b4d17828265377a606cb76fdf80badb3f5d0e314c76bcd3ceb4629fc3f79ab735275fd102c1a2895b54b4f3d
data/CHANGELOG.md CHANGED
@@ -8,6 +8,75 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.8.0] - 2026-10-01
12
+
13
+ ### Added
14
+
15
+ - Bundle profiles: `core` (loop-guard, check-in, guardrails) and `dev` (known-names, mcp, btw, skills,
16
+ source-links) install a set of the shipped bundles, `chi bundle install core`. A bundle you uninstall stays
17
+ out when the profile is installed or upgraded again, and `chi update` installs a bundle a new chi adds to a
18
+ profile you have. `chi bundle uninstall core` removes its bundles, `chi bundle list` and `status` show them.
19
+ - `chi bootstrap` installs the system bundle and the `core` bundles, and on a terminal offers `dev`. Existing
20
+ installs get neither by themselves.
21
+
22
+ ### Changed
23
+
24
+ - Dismissing a question (Esc, an empty answer, or no one to answer in `-p`) now tells the model not to do what it
25
+ asked about, or anything else that changes files or state, and to wait, instead of "go on with your best
26
+ judgement".
27
+ - The system bundle tells chi to stop and ask on anything unexpected while following a skill (a failing check,
28
+ output that differs from a step, a warning the skill doesn't mention) instead of improvising a fix.
29
+ - `chi bundle list` and `chi update`'s footer list the shipped bundles that aren't installed under their
30
+ profile (`core (…)`, `dev (…)`).
31
+ - `chi bundle status` without a name lists a bundle whose `manifest.json` doesn't parse as unreadable instead of
32
+ failing.
33
+ - The web stage view flashes a plugin's nudge ("↪ check-in nudged the model") and a hook's info notice in its
34
+ trail for about 4 s, as it already did a warn notice, so they are seen while the turn runs with the block
35
+ closed. chi's own "asking again" row does not flash (the status line says it).
36
+ - A turn a hook stops (loop-guard, check-in's Stop) tells the model which hook stopped it and why.
37
+ - With custom `context.status_thresholds`, the context guidance follows them (the top bucket reads critical)
38
+ instead of calling every bucket healthy.
39
+
40
+ ### Removed
41
+
42
+ - `POST /api/sessions/:id/answer` no longer takes the unused aliases `question_id`, `selection`, `other` or
43
+ a nested `answer` object: only `id`, `selected` and `freeform`, as the web page sends them.
44
+ - Sessions no longer write plain-text input for a worker from before 2026-09-23 (one that advertises no input
45
+ format) or refuse images to one that predates them (`images_unsupported`), and a worker no longer reads
46
+ `input/*.txt`: every chi since then writes and reads JSON input. A worker that old never exits by itself;
47
+ restart it (`chi sessions stop ID`, then send to it) before upgrading.
48
+ - A worker from before the owner lock (2026-09-22) no longer counts as a session's owner through its pid file:
49
+ only the lock holder does, so a stale pid file whose pid was reused no longer makes a session look busy.
50
+
51
+ ### Fixed
52
+
53
+ - A hook or plugin that fails while watching the stream is logged once a minute instead of on every batch
54
+ (it printed to stderr on every fire).
55
+ - The web stage view's headline skips lines inside a code fence.
56
+ - A question left open when a session's worker died no longer stays pending: the web and the hub stop showing
57
+ it (and the "needs you" badge) at once, and the next worker drops it.
58
+
59
+ ### Security
60
+
61
+ - Session ids from outside (the worker's Bridge, the web routes, `chi send` / `note` / `sessions`,
62
+ `--resume` / `--attach`, the delegate and send_note tools) are checked in one place, so an id such as
63
+ `../x` can no longer reach a path outside the sessions folder. The Bridge answers 400, the web 404.
64
+
65
+ Update with `chi update`. New bundles: `core` 0.1.0 and `dev` 0.1.0 (install with `chi bundle install core`,
66
+ `chi bundle install dev`); existing installs don't get them by themselves.
67
+
68
+ ## [0.7.0] - 2026-09-30
69
+
70
+ ### Added
71
+
72
+ - The desktop helper also sends to agent CLIs (claude, codex, …) in kitty windows: with `kitty.listen_on` copied
73
+ from kitty.conf (and `chi desktop upgrade`), the panel lists them in a "kitty" group next to the sessions; ⏎
74
+ pastes the quote, message and image paths and presses Enter, the new ⌥⏎ only pastes (so several screenshots
75
+ can pile up), and one send can go to chi sessions and kitty windows together. ⌥⏎ no longer inserts a newline
76
+ in the panel (⇧⏎ does). See docs/desktop.md "Agents in kitty".
77
+
78
+ Update with `chi update`. No bundle versions changed.
79
+
11
80
  ## [0.6.0] - 2026-09-30
12
81
 
13
82
  ### Added
@@ -393,7 +462,9 @@ and long-lived sessions.
393
462
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
394
463
  hotkey panel that send selected text or the clipboard to your sessions.
395
464
 
396
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.6.0...HEAD
465
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.8.0...HEAD
466
+ [0.8.0]: https://github.com/dm1try/samagotchi/compare/v0.7.0...v0.8.0
467
+ [0.7.0]: https://github.com/dm1try/samagotchi/compare/v0.6.0...v0.7.0
397
468
  [0.6.0]: https://github.com/dm1try/samagotchi/compare/v0.5.1...v0.6.0
398
469
  [0.5.1]: https://github.com/dm1try/samagotchi/compare/v0.5.0...v0.5.1
399
470
  [0.5.0]: https://github.com/dm1try/samagotchi/compare/v0.4.0...v0.5.0
data/README.md CHANGED
@@ -67,8 +67,11 @@ chi bootstrap # look on this machine's usual ports
67
67
  It finds out whether the server is llama.cpp or OpenAI-compatible, picks the
68
68
  model (or asks, when there are several), sends one test request and writes
69
69
  the config. With a config already there, it adds a `hosts:` entry and keeps
70
- the rest of the file. Edit the file later as in [Configure](#configure);
71
- `chi bootstrap --help` has the options.
70
+ the rest of the file. Then it installs the `core` bundles (loop-guard,
71
+ check-in, guardrails: the safety set for a small local model) and, on a
72
+ terminal, offers `dev` (known-names, mcp, btw, skills, source-links). Edit the
73
+ file later as in [Configure](#configure); `chi bootstrap --help` has the
74
+ options.
72
75
 
73
76
  ## Configure
74
77
 
@@ -154,7 +157,8 @@ Command Line Tools. See [Desktop helper](docs/desktop.md).
154
157
 
155
158
  Every tool call the model makes can be allowed, denied, or put to you first.
156
159
  Rules come from `config.yml` (`guardrails:`), installed bundles and hooks;
157
- `chi bundle install guardrails` adds a default set (asks before `git push`,
160
+ the `guardrails` bundle (in `core`, which `chi bootstrap` installs; else
161
+ `chi bundle install guardrails`) adds a default set (asks before `git push`,
158
162
  history rewrites, wide `rm -rf`, `curl | sh`, writes outside the repo; denies
159
163
  writes to `.git/hooks`). You answer in the REPL, the attached TUI or the web:
160
164
  once, for the session, for the repo, or for the whole rule in the repo.
data/docs/architecture.md CHANGED
@@ -92,7 +92,7 @@ bin/chi ─▶ TerminalUI ─▶ Engine#run_turn ─▶ KernelLoop ──┬─
92
92
  | Tools | `lib/samagotchi/tools/*` | Execute, read, edit, write, memory, task_*, web_fetch, plus runtime/output-guardrails. |
93
93
  | Background | `Samagotchi::SessionManager` | Builds `Engine` directly (no terminal rendering) for workers. |
94
94
  | Web | `Samagotchi::Web::App`, `Samagotchi::Web::Server`, `Samagotchi::Web::SessionHub` | Rack+WEBrick single-port `127.0.0.1:4567` (index.html + `/api/*` + SSE). The hub is chi web's projection of the session list, pushed to every tab over `GET /api/events`. |
95
- | Sessions | `Samagotchi::Session`, `SessionManager` | File `sessions/<uuid>.json` + sidecar `input/`/`output/`/`pid`; retention 14d/500, `updated_at desc`, lazy sweep. |
95
+ | Sessions | `Samagotchi::Session`, `SessionManager` | File `sessions/<uuid>.json` + sidecar `input/`/`output/`; retention 14d/500, `updated_at desc`, lazy sweep. |
96
96
 
97
97
  ## Entry points
98
98
 
@@ -110,12 +110,12 @@ bin/chi ─▶ TerminalUI ─▶ Engine#run_turn ─▶ KernelLoop ──┬─
110
110
  `dismiss_question`). The worker runs in the session's `working_directory`, so `!cmd` and
111
111
  the tools don't depend on where the terminal attached from.
112
112
  - `bin/chi web` → builds `Web::Server` (Rack+WEBrick on `127.0.0.1:4567`, `--port`/`SAMAGOTCHI_WEB_PORT`, `--open`).
113
- - `bin/chi sessions {list,stop,delete,prune,clean}` (`SessionsCommand`; not the REPL's `SessionCommands`) → retention & ordering (`Session.prune`, `updated_at desc`, dry-run, test-only); `stop` is `SessionManager.stop_session(wait:)`, which waits for the worker to release `owner.lock`; `delete` (`SessionDeleteCommand`) is `SessionManager.delete_session(stop:)`, which the TUI's `/exit --delete` and the web's `DELETE /api/sessions/:id` use too.
113
+ - `bin/chi sessions {list,stop,delete,prune,clean}` (`SessionsCommand`; not the REPL's `SessionCommands`) → retention & ordering (`SessionRetention`, `updated_at desc`, dry-run, test-only); `stop` is `SessionManager.stop_session(wait:)`, which waits for the worker to release `owner.lock`; `delete` (`SessionDeleteCommand`) is `SessionManager.delete_session(stop:)`, which the TUI's `/exit --delete` and the web's `DELETE /api/sessions/:id` use too.
114
114
  - `--prompt`, `--non-interactive`, and `SessionManager` workers build `Engine` directly.
115
115
 
116
116
  ## Session retention & ordering
117
117
 
118
- - **Files:** `~/.local/state/samagotchi/sessions/<uuid>.json` + `<uuid>/input|output|pid|owner.lock|bridge.json` (XDG-aware).
118
+ - **Files:** `~/.local/state/samagotchi/sessions/<uuid>.json` + `<uuid>/input|output|owner.lock|bridge.json` (XDG-aware).
119
119
  - **Single owner:** the process running a session's Engine (worker or in-process TUI) holds a flock on `owner.lock` (`OwnerLock`); a second owner backs off, and the web answers 409 for a TUI-owned session.
120
120
  - **Status:** `status` is turn state (`idle`/`running`); liveness is the lock.
121
121
  - **Retention:** 14 days / 500 cap (env `SAMAGOTCHI_SESSION_RETENTION_DAYS`/`MAX_COUNT`, optional `KEEP_STATUS`), live-owner guard, only when `*.json` present; lazy sweep ≤1/24h from the session hub's full-probe tick (and on `GET /api/sessions`, which the page no longer calls) & `Dashboard#render_list`, manual via `bin/chi sessions prune --dry-run`.
@@ -134,7 +134,7 @@ agent logic and can be used without any terminal rendering; the UI is a thin lay
134
134
  | Model loops and adapters | `KernelLoop`, `LLM::ChatLoop`, `Samagotchi::Client`, `LLM::OpenAIChat`, `LLM::HTTP` | The model↔tool loops and the HTTP adapters they talk through (see "Model loops and adapters"). |
135
135
  | Bridge (SSE/HTTP) | `Samagotchi::Bridge`, `SessionManager` | The **single live client transport**: an SSE read stream + HTTP POST turn/cancel/answer surface that attaches to a worker's existing `Engine` via `Engine#subscribe`. Every session worker starts it (bound `127.0.0.1`, no auth, localhost-only). |
136
136
  | Web (Rack) | `Samagotchi::Web::App`, `SessionManager` | Single-port `127.0.0.1:4567` control plane via `rack`+`webrick` (serve `index.html` + `/api/*`; `/stream` proxies each session's Bridge). `bin/chi web` entrypoint. |
137
- | Sessions | `Samagotchi::Session`, `SessionManager` | File-based `~/.local/state/samagotchi/sessions/<uuid>.json` + sidecar `input/`/`output/`/`pid`; retention (14d/500) + ordering (`updated_at desc`). |
137
+ | Sessions | `Samagotchi::Session`, `SessionManager` | File-based `~/.local/state/samagotchi/sessions/<uuid>.json` + sidecar `input/`/`output/`; retention (14d/500) + ordering (`updated_at desc`). |
138
138
 
139
139
  - `bin/chi` in REPL mode (see `LaunchMode` above) builds `TerminalUI`. `TerminalUI#run` is the single
140
140
  dispatch for the REPL, `-p`/`--prompt`, `--non-interactive`, and `--resume`: it
data/docs/cli.md CHANGED
@@ -21,7 +21,7 @@
21
21
  - `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))
22
22
  - `chi self` — print version, source dir (checkout or installed gem), config/memory/session paths, model/host and bundles; `chi self --model` prints only the model a new session starts on (the desktop helper's hint)
23
23
  - `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))
24
- - `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), [the check-in bundle](plugins.md#the-check-in-bundle) and [the skills bundle](plugins.md#the-skills-bundle))
24
+ - `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; `core` and `dev` are profiles that install a set of them (see [Bundle profiles](memory.md#bundle-profiles-core-and-dev), [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), [the check-in bundle](plugins.md#the-check-in-bundle) and [the skills bundle](plugins.md#the-skills-bundle))
25
25
 
26
26
  ### First setup
27
27
 
@@ -58,6 +58,13 @@ chi bootstrap # try localhost 8080, 11434, 1234, 8000
58
58
  none. A server already in the file writes nothing; a file in YAML flow style
59
59
  or with anchors gets the lines printed to paste instead. `--dry-run` shows
60
60
  what it would write.
61
+ - **The bundles.** Unless writing the config failed, it installs the system
62
+ bundle and the `core` profile (loop-guard, check-in, guardrails), one line
63
+ each; on a terminal it then asks `Also install dev (known-names, mcp, btw,
64
+ skills, source-links)? [y/N]`. Run again, it installs nothing new: a bundle
65
+ you uninstalled stays out ([Bundle profiles](memory.md#bundle-profiles-core-and-dev)).
66
+ A bundle that fails to install makes the exit 1. `--dry-run` says what it
67
+ would install.
61
68
 
62
69
  ### Updating
63
70
 
@@ -69,10 +76,11 @@ chi (gem) 0.2.0 0.3.0 updated
69
76
  system bundle 0.2.0 0.3.0 updated (kept your edits in identity.md: chi bundle diff samagotchi-system identity.md)
70
77
  btw 0.1.1 up to date
71
78
  known-names 0.1.0 0.1.1 updated
79
+ core 0.1.0 0.2.0 updated (+ new-bundle)
72
80
  infra_tools 1.0.0 skipped (not from chi)
73
81
  Chi Helper 0.2.0 up to date (launch file refreshed)
74
82
  workers 2 live on 0.2.0: they move to 0.3.0 at idle exit (30 min) or chi sessions stop 2ea8c1f0 91b0d2aa
75
- Also shipped, not installed: check-in, skills, source-links (chi bundle install NAME)
83
+ Also shipped, not installed: dev (mcp, skills, source-links) (chi bundle install NAME)
76
84
  done
77
85
  ```
78
86
 
@@ -97,6 +105,10 @@ done
97
105
  A bundle of the same name from elsewhere (a zip, git) is skipped ("not from
98
106
  chi"), a newer installed one is left, one whose new version needs a newer
99
107
  chi is skipped, and bundles you didn't install stay uninstalled.
108
+ - **Profiles** (`core`, `dev`): an installed one installs the bundles a new
109
+ chi added to it (`updated (+ name)`), even at the same version, and retries
110
+ one that failed before; one you uninstalled stays out. See [Bundle
111
+ profiles](memory.md#bundle-profiles-core-and-dev).
100
112
  - **The desktop helper** (macOS) is rebuilt and restarted only when its Swift
101
113
  sources changed (or the Ruby it runs moved); otherwise only its launch file
102
114
  is refreshed. See [Desktop helper](desktop.md).
@@ -206,7 +218,8 @@ of UIs at once: the Web UI and attached terminals (`chi`, `--resume`,
206
218
  `--attach`). They all see the same turns as they happen, and any of them can send
207
219
  a prompt, also while a turn runs (it merges into that turn as steering). The
208
220
  first answer to an `ask_user_question` wins; the other UIs close their widget.
209
- An empty answer dismisses the question in every UI.
221
+ An empty answer dismisses the question in every UI. The model is then told
222
+ not to go ahead with what it asked about, or change anything else, and to wait.
210
223
 
211
224
  In an attached terminal:
212
225
 
@@ -445,7 +458,9 @@ card, so it stays on screen without scrolling: a status row (what it is
445
458
  doing, the step, the elapsed time), your prompt on one line, the newest
446
459
  narration sentence (else the newest thinking one, in italics; click it for
447
460
  the step's reasoning), the running tool with what it does, and the last
448
- three calls. A card that needs you (a question, an approval, check-in)
461
+ three calls. A plugin's or hook's notice and a plugin's nudge ("check-in
462
+ nudged the model") show ahead of those calls for about 4 s, as their rows
463
+ wait in the closed block. A card that needs you (a question, an approval, check-in)
449
464
  sits in the stage too. The chip under it, `N steps · M tool calls` with one
450
465
  tick per call, opens the turn view's block in place (newest step first
451
466
  while it runs; a tick opens its step). `▾` folds the stage to its status
@@ -687,6 +687,23 @@ hosts:
687
687
  `models:` wins over `hosts:`. On a native host, `vision: true` skips only the
688
688
  modalities check: without a media marker the prompt can't carry an image.
689
689
 
690
+ ## Desktop helper: agents in kitty
691
+
692
+ The desktop helper (`chi desktop`) can also paste into agent CLIs (claude,
693
+ codex, …) running in [kitty](https://sw.kovidgoyal.net/kitty/) windows. It
694
+ lists them only with `kitty.listen_on` set: copy the value of `listen_on` from
695
+ your `kitty.conf` as it is (kitty must also have `allow_remote_control` on).
696
+
697
+ ```yaml
698
+ kitty:
699
+ listen_on: unix:/tmp/kitty.${KITTY_PID} # as in kitty.conf; unset = no kitty targets
700
+ binary: /Applications/kitty.app/Contents/MacOS/kitty # the default
701
+ agents: [claude, codex] # the programs listed; "*" = every window
702
+ ```
703
+
704
+ The helper reads these from its launch file: after editing them run
705
+ `chi desktop upgrade` (or `chi update`). See [Desktop helper](desktop.md#agents-in-kitty).
706
+
690
707
  ## Project specific description
691
708
 
692
709
  If an AGENT.md file is present in the project root, samagotchi injects its
@@ -756,6 +773,9 @@ described in their own sections.
756
773
  | `update.gem` | `true` | | `false`: `chi update` never installs a newer gem (`--no-gem` for one run). See [CLI: Updating](cli.md#updating). |
757
774
  | `update.bundles` | `true` | | `false`: `chi update` leaves the shipped bundles to `chi bundle upgrade` (`--no-bundles`). |
758
775
  | `update.desktop` | `true` | | `false`: `chi update` leaves the desktop helper alone (`--no-desktop`). |
776
+ | `kitty.listen_on` | none | | The desktop helper's kitty socket, copied from `kitty.conf`; unset = no kitty targets. See "Desktop helper: agents in kitty". |
777
+ | `kitty.binary` | `/Applications/kitty.app/Contents/MacOS/kitty` | | The kitty the helper runs `kitty @` with. |
778
+ | `kitty.agents` | `claude\|codex\|gemini\|aider\|opencode\|cursor-agent\|amp\|goose` | | Foreground programs whose kitty windows the helper lists, `\|`-separated (a YAML list works too); `*` lists every window. |
759
779
  | `read.truncate_at_bytes` | `65536` | yes | A `read` result larger than this is cut to a preview. |
760
780
  | `read.preview_bytes` | `12288` | yes | Size of that preview. |
761
781
  | `read.hard_max_bytes` | `2097152` | yes | Largest file `read` opens. |
data/docs/desktop.md CHANGED
@@ -20,6 +20,8 @@ sessions. Keys:
20
20
  - **⏎** sends a message: `chi send -m <the line>` with the text as quoted context above it. With the line empty,
21
21
  the text is the message.
22
22
  - **⌘⏎** sends a note: `chi note`, the line (if any) then the text.
23
+ - **⌥⏎** pastes into an agent in a kitty window without pressing Enter (see [Agents in kitty](#agents-in-kitty));
24
+ it is not a newline.
23
25
  - **⇧⏎** is a newline, in either field. Esc closes.
24
26
 
25
27
  The list shows the sessions a worker runs now, then, under a "recent" divider, up to 3 stopped ones (dimmed, with
@@ -64,6 +66,53 @@ thumbnails sit under the message line, 48 px high, the file name as a tooltip; h
64
66
  the panel closes; files from Finder are sent as they are, never touched. A send with images may take up to 30 s
65
67
  (converting, a worker starting) before the panel gives up.
66
68
 
69
+ ## Agents in kitty
70
+
71
+ The panel can also send to agent CLIs (claude, codex, gemini, aider, …) running in
72
+ [kitty](https://sw.kovidgoyal.net/kitty/) windows, next to chi sessions. kitty only, through its remote control;
73
+ off until you set it up:
74
+
75
+ 1. In `kitty.conf`, remote control on and a socket: `allow_remote_control yes` and a `listen_on unix:…` line.
76
+ 2. Copy that `listen_on` value as it is into chi's config.yml:
77
+
78
+ ```yaml
79
+ kitty:
80
+ listen_on: unix:/tmp/kitty.${KITTY_PID}
81
+ ```
82
+
83
+ `kitty.agents` picks the programs listed (default `claude|codex|gemini|aider|opencode|cursor-agent|amp|goose`, a
84
+ YAML list works too; `"*"` lists every window, shells too), `kitty.binary` the kitty to run (default
85
+ `/Applications/kitty.app/Contents/MacOS/kitty`). See [Configuration](configuration.md#desktop-helper-agents-in-kitty).
86
+ 3. Run `chi desktop upgrade` (or `chi update`): the helper reads these from its launch file, so **after every edit of
87
+ `kitty:`** run it again. No rebuild when only the settings changed.
88
+
89
+ The helper finds the sockets itself: kitty adds `-<pid>` to a `listen_on` path from kitty.conf (and fills in
90
+ `{kitty_pid}`), `~` and variables the helper knows are expanded, others (`${KITTY_PID}`) stay as written, as kitty
91
+ keeps them. Several kitty instances all count. Only `unix:` sockets work.
92
+
93
+ The panel lists the windows whose foreground program is one of the agents under a **kitty** divider, between live
94
+ and recent sessions: "claude · <its title>", the folder below, a terminal icon, and a ⌘ number like the sessions.
95
+ With the settings on but no agent running, one dim line says "no agents in kitty". A window counts when a foreground
96
+ process's program is an agent, also as the script of `node …/codex` (that match is untested for codex and gemini).
97
+
98
+ - **⏎** pastes into every chosen window and presses Enter: the text as a `> ` quote, a blank line, your message, then
99
+ one image path per line. Chosen together with chi sessions, each gets the message and the panel shows one line
100
+ ("Sent. · sent to claude · samagotchi").
101
+ - **⌥⏎** only pastes (no Enter), and only when every chosen target is a kitty window: build up several screenshots,
102
+ each with its own line, then press Enter in kitty. The footer shows **Paste ⌥⏎** instead of Note then.
103
+ - **⌘⏎** with a kitty window chosen beeps: "Notes are for chi sessions". ⌥⏎ with a chi session: "Paste only is
104
+ for kitty windows".
105
+ - **Images:** a screenshot or other image data is copied to `$TMPDIR/chi-helper-sent/<uuid>.png` (or `.tiff`) and
106
+ its path pasted; the agent reads it from there (claude attaches it as `[Image #1]`). These files are deleted a week
107
+ later, at the helper's next start. Finder files are pasted with their own path, `'…'`-quoted when they hold spaces.
108
+ - The paste arrives as one bracketed paste, so the agent takes newlines as text (claude shows long ones as
109
+ `[Pasted text]`). With `agents: "*"` beware of a program that doesn't turn bracketed paste on: the newlines would
110
+ act as Enter.
111
+ - No delivery confirmation: before pasting, the helper checks the window is still there ("window closed" if not);
112
+ it can't know whether the agent read it. The 16 KB cap is chi's; a kitty window takes more.
113
+ - Focus goes back to the app you came from, as for sessions. The last choice (kitty windows too) is preselected
114
+ while listed; ⌘ numbers shift as windows come and go.
115
+
67
116
  ## Install
68
117
 
69
118
  ```sh
data/docs/guardrails.md CHANGED
@@ -146,7 +146,7 @@ without it.
146
146
  ## The guardrails bundle
147
147
 
148
148
  ```sh
149
- chi bundle install guardrails
149
+ chi bundle install guardrails # chi bootstrap installs it, with the core profile
150
150
  ```
151
151
 
152
152
  installs a default rule set plus a short memory telling the model not to
@@ -164,7 +164,7 @@ denies every call until the bundle is reinstalled.
164
164
  ## The known-names bundle
165
165
 
166
166
  ```sh
167
- chi bundle install known-names
167
+ chi bundle install known-names # or chi bundle install dev
168
168
  ```
169
169
 
170
170
  installs one `before_tool_call` hook and a short memory. A local model that
data/docs/hooks.md CHANGED
@@ -416,7 +416,7 @@ Notes:
416
416
  - Ordering: bundle hooks fire by `(priority, bundle_name, hook_name)` (lower priority first), then plain `config.yml` hooks in registration order.
417
417
  - Settings: a hook class with `initialize(settings = {})` gets the bundle's section of `config.yml` `bundles:` (see [Settings](#settings)).
418
418
  - A bundle can also ship a `plugin.rb` whose `chi.on(event)` blocks are bundle hooks too, next to commands and tools; see [Plugins](plugins.md).
419
- - Shipped bundles: `chi bundle install guardrails` (rules, see [Guardrails](guardrails.md#the-guardrails-bundle)) and `chi bundle install known-names` (a hook, see [Guardrails](guardrails.md#the-known-names-bundle)), `chi bundle install source-links` (a hook: announces source refs, see [The source-links bundle](#the-source-links-bundle)), `chi bundle install btw` (a plugin: `/btw`, see [Plugins](plugins.md#the-btw-bundle)), `chi bundle install mcp` (a plugin: tools from MCP servers, see [Plugins](plugins.md#the-mcp-bundle)) `chi bundle install loop-guard` (a plugin: breaks tool-call loops, see [Plugins](plugins.md#the-loop-guard-bundle)), `chi bundle install check-in` (a plugin: checks on a long turn, see [Plugins](plugins.md#the-check-in-bundle)) and `chi bundle install skills` (a plugin: `/skill`, versions of skills, see [Plugins](plugins.md#the-skills-bundle)).
419
+ - Shipped bundles (each in a profile: `core` or `dev`, see [Bundle profiles](memory.md#bundle-profiles-core-and-dev)): `chi bundle install guardrails` (rules, see [Guardrails](guardrails.md#the-guardrails-bundle)) and `chi bundle install known-names` (a hook, see [Guardrails](guardrails.md#the-known-names-bundle)), `chi bundle install source-links` (a hook: announces source refs, see [The source-links bundle](#the-source-links-bundle)), `chi bundle install btw` (a plugin: `/btw`, see [Plugins](plugins.md#the-btw-bundle)), `chi bundle install mcp` (a plugin: tools from MCP servers, see [Plugins](plugins.md#the-mcp-bundle)) `chi bundle install loop-guard` (a plugin: breaks tool-call loops, see [Plugins](plugins.md#the-loop-guard-bundle)), `chi bundle install check-in` (a plugin: checks on a long turn, see [Plugins](plugins.md#the-check-in-bundle)) and `chi bundle install skills` (a plugin: `/skill`, versions of skills, see [Plugins](plugins.md#the-skills-bundle)).
420
420
  - Installing a bundle executes its hook code at `Engine` startup. Only install bundles you trust, as you would a gem. Hooks are **not** executed at install time (copy-only); they are `module_eval`'d at `Engine.new` inside per-bundle `Samagotchi::Bundles::<name>` namespaces (no top-level `require` collisions). Keep hook files side-effect-free at load time; do work in `#call` — top-level side effects (require, IO, `at_exit`, global assignment) run once per `Engine.new` (class redefinition is idempotent).
421
421
 
422
422
  Lifecycle:
@@ -428,7 +428,7 @@ Lifecycle:
428
428
  ## The source-links bundle
429
429
 
430
430
  ```sh
431
- chi bundle install source-links
431
+ chi bundle install source-links # or chi bundle install dev
432
432
  ```
433
433
 
434
434
  installs one `after_turn` hook and a short memory. When the model's answer
@@ -25,7 +25,9 @@ Emission behavior:
25
25
  synthetic char-based estimate (`src=estimate`). The guidance text is dynamic
26
26
  and escalates with the bucket: healthy → proceed normally; moderate → prefer
27
27
  targeted/range reads; elevated → be concise, avoid large re-reads; critical →
28
- summarize aggressively and delegate broad work to subagents.
28
+ summarize aggressively and delegate broad work to subagents. With custom
29
+ `context.status_thresholds` the top bucket reads critical, the one below it
30
+ elevated, the next moderate, and the first bucket (and the rest below) healthy.
29
31
 
30
32
  Configuration:
31
33
 
data/docs/memory.md CHANGED
@@ -47,7 +47,9 @@ chi is told about skills by the system bundle (`identity.md`, every turn, and
47
47
  one ("I like how we did that") it may ask first.
48
48
  - **Following.** The skill's index line (the `description:` of its
49
49
  `memory_write`, "Release a new version of this repo: …") is in every
50
- prompt, so next time chi reads the skill and follows it.
50
+ prompt, so next time chi reads the skill and follows it. When something
51
+ looks unexpected on the way (a failing check, a warning the skill doesn't
52
+ mention), chi stops, says what it saw and asks how to go on.
51
53
  - **Updating.** When a step turned out different (a renamed script, an extra
52
54
  step), chi fixes the skill in the same turn: those steps changed, the rest
53
55
  kept, a dated Changelog line added. No confirmation.
@@ -74,6 +76,51 @@ skills`, [docs/plugins.md](plugins.md#the-skills-bundle)) adds `/skill save`,
74
76
  one-line diff after each update, and nudges a model that skips a failing
75
77
  step instead of fixing the skill.
76
78
 
79
+ ## Bundle profiles: core and dev
80
+
81
+ Every bundle chi ships (the system bundle aside) belongs to one of two
82
+ profiles, meta bundles that install a set of them:
83
+
84
+ | profile | bundles |
85
+ |---|---|
86
+ | `core` | loop-guard, check-in, guardrails: the recommended safety set |
87
+ | `dev` | known-names, mcp, btw, skills, source-links |
88
+
89
+ `chi bootstrap` installs `core` and, on a terminal, offers `dev`. An existing
90
+ install gets neither by itself: `chi bundle install core` (or `dev`).
91
+
92
+ A profile's `manifest.yml` has `includes:` (bundle names) and nothing else to
93
+ install; its dir holds only that file. What installing it does follows one
94
+ rule:
95
+
96
+ installed now = the profile's bundles − the ones it recorded − the ones installed
97
+
98
+ - **First install:** each bundle that isn't installed is installed (from the
99
+ bundles chi ships, never a same-named dir in the cwd); one you installed by
100
+ hand is recorded without reinstalling it.
101
+ - **Again, or `chi bundle upgrade core`:** only a bundle new to the profile
102
+ is installed. One you uninstalled (`chi bundle uninstall check-in`) was
103
+ recorded, so it stays out; re-running `chi bootstrap` changes nothing.
104
+ - **A new chi adds a bundle to a profile:** `chi update` installs just that
105
+ one ("core … updated (+ name)"). A bundle dropped from a profile stays
106
+ installed.
107
+ - **A bundle this chi is too old for** (its `requires_chi`) is skipped with a
108
+ note, and one that fails is reported (exit 1); neither is recorded, so the
109
+ next `chi update` or install tries again.
110
+ - The profile never pins its bundles' versions: each one upgrades on its own,
111
+ as before (`chi update`, `chi bundle upgrade NAME`).
112
+ - Only the profiles chi ships expand: `includes:` in a bundle from anywhere
113
+ else is ignored.
114
+
115
+ `chi bundle uninstall core` uninstalls each bundle it recorded that is still
116
+ installed (one you installed by hand before core too), then core. A bundle
117
+ with an edited memory file is kept and named (`--force` removes it), the
118
+ rest go, and core stays until it is gone (exit 1). `--scope project` is
119
+ refused for a profile. `chi bundle list` shows an installed profile's
120
+ bundles (`includes=`) and the ones you left out (`left out=`), a profile not
121
+ installed with its bundles on its line, and `chi bundle status core` lists
122
+ each one.
123
+
77
124
  ## Bundles that need outside commands
78
125
 
79
126
  A bundle's memory can rely on a command chi doesn't ship, such as a GitHub
data/docs/plugins.md CHANGED
@@ -596,7 +596,7 @@ ctx.sessions.read(id) # => {id:, title:, status:, parent_id:, running:, message
596
596
 
597
597
  ## The btw bundle
598
598
 
599
- `chi bundle install btw` installs the bundle shipped with chi. It is written
599
+ `chi bundle install btw` (or the `dev` profile) installs the bundle shipped with chi. It is written
600
600
  only against this API (`lib/samagotchi/bundles/btw/plugin.rb`).
601
601
 
602
602
  - `/btw <question>` asks the session's model a side question about the
@@ -621,7 +621,7 @@ only against this API (`lib/samagotchi/bundles/btw/plugin.rb`).
621
621
 
622
622
  ## The mcp bundle
623
623
 
624
- `chi bundle install mcp` installs the bundle shipped with chi. It is written
624
+ `chi bundle install mcp` (or the `dev` profile) installs the bundle shipped with chi. It is written
625
625
  only against this API (`lib/samagotchi/bundles/mcp/plugin.rb`), and adds
626
626
  tools from [MCP](https://modelcontextprotocol.io) servers. It has no memory
627
627
  file, so it costs the prompt nothing but its tools. Stdio servers only, for
@@ -733,7 +733,8 @@ bundles:
733
733
 
734
734
  ## The loop-guard bundle
735
735
 
736
- `chi bundle install loop-guard` installs the bundle shipped with chi. It is
736
+ `chi bundle install loop-guard` installs the bundle shipped with chi (`chi
737
+ bootstrap` installs it with the `core` profile). It is
737
738
  written only against this API (`lib/samagotchi/bundles/loop-guard/plugin.rb`),
738
739
  with `chi.on` hooks, and has no memory file. It guards two kinds of loop:
739
740
  repeated tool calls (below) and [thinking that repeats
@@ -853,7 +854,8 @@ Not caught (yet):
853
854
 
854
855
  ## The check-in bundle
855
856
 
856
- `chi bundle install check-in` installs the bundle shipped with chi. It is
857
+ `chi bundle install check-in` installs the bundle shipped with chi (`chi
858
+ bootstrap` installs it with the `core` profile). It is
857
859
  written only against this API (`lib/samagotchi/bundles/check-in/plugin.rb`):
858
860
  `chi.on` hooks, `ctx.card`, `ctx.steer`, `ctx.stop_turn` and one anytime
859
861
  command. It has no memory file.
@@ -911,7 +913,7 @@ with a line and carry on unchanged.
911
913
 
912
914
  ## The skills bundle
913
915
 
914
- `chi bundle install skills` installs the bundle shipped with chi
916
+ `chi bundle install skills` (or the `dev` profile) installs the bundle shipped with chi
915
917
  (`lib/samagotchi/bundles/skills/plugin.rb`): one anytime command, `chi.on`
916
918
  hooks, `ctx.sessions.send`, `ctx.notify` and `event[:steer]`. It has no memory
917
919
  file; skills themselves work without it ([docs/memory.md](memory.md#skills)).
data/docs/releasing.md CHANGED
@@ -13,13 +13,17 @@ can run the whole release; the user approves the notes before the tag and the
13
13
  and `rake release:check` enforce it). The system bundle upgrades itself when
14
14
  chi starts.
15
15
  - **Every other shipped bundle** (btw, check-in, guardrails, known-names,
16
- loop-guard, mcp, skills, source-links) has its own semver in its
16
+ loop-guard, mcp, skills, source-links, and the profiles core and dev) has its own semver in its
17
17
  `manifest.yml` and, when it needs a newer chi, a `requires_chi:` line. They never upgrade by themselves: users run
18
18
  `chi update` (all of them, with the gem) or `chi bundle upgrade NAME`. So:
19
19
  - a change to a bundle's files bumps that bundle's `version:`
20
20
  (`rake bundles:check` fails otherwise, once there's a tag to compare with);
21
21
  - a bundle that uses something new in chi raises its `requires_chi` in the
22
22
  same change; `release:bump` never touches `requires_chi`;
23
+ - a new shipped bundle joins a profile (`includes:` in `core` or `dev`; a
24
+ spec checks every bundle is in exactly one), and that profile's version
25
+ goes up: `chi update` then installs only the new bundle for those who
26
+ have the profile;
23
27
  - the release notes say "run `chi update`" and name the bundles whose
24
28
  version moved (what changed in each), not per-bundle upgrade steps.
25
29
  - Pre-1.0: config and commands may change in a minor version (0.2 → 0.3); a
data/docs/sessions.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Sessions
2
2
 
3
- Sessions are plain files — no DB. Each session is `~/.local/state/samagotchi/sessions/<uuid>.json` (XDG-aware via `XDG_STATE_HOME`) plus a sidecar dir `<uuid>/` with `input/`/`notes/`/`output/`/`pid`/`bridge.json` (`Session.session_dir`). Besides the messages and turn state, the JSON keeps `used_memory_names` (what the session read), `preloaded_memory_names` (`--memory`) and `muted_memory_names` (`--mute`), the last two written before the worker starts so a respawn builds the same prompt, and `parent_id`, the session that delegated this one (see *Delegating*; `null` otherwise); files from before those keys read as empty lists and no parent.
3
+ Sessions are plain files — no DB. Each session is `~/.local/state/samagotchi/sessions/<uuid>.json` (XDG-aware via `XDG_STATE_HOME`) plus a sidecar dir `<uuid>/` with `input/`/`notes/`/`output/`/`owner.lock`/`bridge.json` (`Session.session_dir`). Besides the messages and turn state, the JSON keeps `used_memory_names` (what the session read), `preloaded_memory_names` (`--memory`) and `muted_memory_names` (`--mute`), the last two written before the worker starts so a respawn builds the same prompt, and `parent_id`, the session that delegated this one (see *Delegating*; `null` otherwise); files from before those keys read as empty lists and no parent.
4
4
 
5
5
  **Retention (file-based, opt-out via env):**
6
6
 
@@ -15,7 +15,7 @@ A session is deleted if **expired by age OR overflow by count** (unless `keep_st
15
15
 
16
16
  **Worker idle exit:** a background worker (plain `chi`, Web UI sessions) exits after `SAMAGOTCHI_SESSION_IDLE_EXIT_MINUTES` (`session.idle_exit_minutes`, default `30`, `0`=never) with no turn running or queued, no client on its stream (an open web tab or attached terminal keeps it), no reminder registered and no continue offer waiting for an answer. It removes `bridge.json` and frees `owner.lock`; the next prompt or `--attach` wakes a new worker (`WorkerIdleExit`, `SessionManager.run_session_loop`). A client can also ask the worker to exit right away (Bridge `POST /session/:id/exit`; `/exit` in an attached terminal does, `/detach` doesn't): the same rules apply without the timeout (even with `0`), and the asker's own stream doesn't count. The reply names what keeps it up. The session's status stays as it was, not `stopped`.
17
17
 
18
- **Empty sessions:** a session nothing happened in is deleted when it is left, so `chi` → `/exit` leaves no trace. Empty means: no messages, no turn tried (a failed turn leaves a `last_prompt` and keeps it), no memory used, no pending question, nothing in `input/`, `notes/` or `images/`, nothing in its dir beyond the worker's skeleton, mode `assist`, and the default model. A session on another model (`/model`, `--model`) counts as prepared and stays (`SessionManager.empty_session?`). The worker checks as it idle-exits or leaves on a client's request and deletes it once its lock is free, checking again first. The attached `/exit` then says `Detached; the session was empty, so it is discarded.` The REPL (`--no-shared`) does the same at `/exit` or Ctrl-D. The retention sweep catches those killed first: empty sessions over an hour old with no live owner, whatever `DAYS`/`MAX`, and skeleton-only dirs with no `*.json`. `session.keep_empty: true` (env `SAMAGOTCHI_SESSION_KEEP_EMPTY`) turns all of it off.
18
+ **Empty sessions:** a session nothing happened in is deleted when it is left, so `chi` → `/exit` leaves no trace. Empty means: no messages, no turn tried (a failed turn leaves a `last_prompt` and keeps it), no memory used, no pending question, nothing in `input/`, `notes/` or `images/`, nothing in its dir beyond the worker's skeleton, mode `assist`, and the default model. A session on another model (`/model`, `--model`) counts as prepared and stays (`SessionManager.discardable?`). The worker checks as it idle-exits or leaves on a client's request and deletes it once its lock is free, checking again first. The attached `/exit` then says `Detached; the session was empty, so it is discarded.` The REPL (`--no-shared`) does the same at `/exit` or Ctrl-D. The retention sweep catches those killed first: empty sessions over an hour old with no live owner, whatever `DAYS`/`MAX`, and skeleton-only dirs with no `*.json`. `session.keep_empty: true` (env `SAMAGOTCHI_SESSION_KEEP_EMPTY`) turns all of it off.
19
19
 
20
20
  **Attached by default:** plain `chi` and `chi --resume ID` run their session in a background worker and attach to it (`session.shared`, default `true`; env `SAMAGOTCHI_SESSION_SHARED`). The worker runs in the session's `working_directory`, so `!commands` and tools see the directory the session was started in. `session.shared: false` keeps the in-process REPL, `--no-shared` does for one run; a session the REPL has open can't be attached ("close it there first"). See [CLI: Sharing a session](cli.md#sharing-a-session).
21
21
 
@@ -3,6 +3,7 @@
3
3
  require "json"
4
4
  require "fileutils"
5
5
  require "time"
6
+ require_relative "atomic_file"
6
7
 
7
8
  require_relative "session"
8
9
 
@@ -76,13 +77,12 @@ module Samagotchi
76
77
  end
77
78
 
78
79
  def self.write(session_id, record, state_dir:)
79
- return false unless File.exist?(File.join(state_dir, "#{session_id}#{Session::FILE_EXT}"))
80
+ return false unless File.exist?(Session.session_file(session_id, state_dir: state_dir))
80
81
 
81
82
  dir = Session.session_dir(session_id, state_dir: state_dir)
82
83
  FileUtils.mkdir_p(dir)
83
84
  path = File.join(dir, FILE)
84
- File.write("#{path}.tmp", JSON.generate(record))
85
- File.rename("#{path}.tmp", path)
85
+ AtomicFile.write(path, JSON.generate(record))
86
86
  true
87
87
  end
88
88
  private_class_method :write
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "securerandom"
5
+
6
+ module Samagotchi
7
+ # Replace a file whole: the content goes to a temporary file next to it
8
+ # (<path>.<pid>.<random>.tmp, unique per process and call, so a worker and
9
+ # a CLI writing the same file never share one), which is then renamed over
10
+ # +path+. A reader sees the old file or the new one, never half of one, and
11
+ # two writers racing leave one of the two contents. The temporary name ends
12
+ # in .tmp, so globs for *.json or *.md never list it. On failure it is
13
+ # removed and the error raised; +path+ is left as it was.
14
+ module AtomicFile
15
+ module_function
16
+
17
+ # @param path [String] the file to replace (its folder must exist)
18
+ # @param content [String] written as-is, byte for byte
19
+ # @param perm [Integer, nil] the new file's mode; nil = a fresh file's
20
+ # (0666 less the umask), like File.write
21
+ # @return [String] +path+
22
+ def write(path, content, perm: nil)
23
+ tmp = "#{path}.#{Process.pid}.#{SecureRandom.hex(4)}.tmp"
24
+ File.open(tmp, File::WRONLY | File::CREAT | File::EXCL | File::BINARY, perm || 0o666) do |io|
25
+ io.write(content)
26
+ end
27
+ File.chmod(perm, tmp) if perm
28
+ File.rename(tmp, path)
29
+ path
30
+ ensure
31
+ FileUtils.rm_f(tmp) if tmp && File.exist?(tmp)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../memory_bundle/manifest"
4
+ require_relative "../memory_bundle/profile"
5
+ require_relative "../memory_bundle/source"
6
+ require_relative "../memory_bundle/system_bundle"
7
+
8
+ module Samagotchi
9
+ module Bootstrap
10
+ # chi bootstrap's bundles step: the system bundle and the shipped
11
+ # profiles, into the config dir the process ENV names (Provenance and
12
+ # the Installer resolve their paths from it, not from bootstrap's env:).
13
+ # A seam: in-process specs pass their own.
14
+ class Bundles
15
+ def initialize(shipped_dir: MemoryBundle::SourceNormalizer::SHIPPED_DIR)
16
+ @shipped_dir = shipped_dir
17
+ end
18
+
19
+ # @return [MemoryBundle::SystemBundle::Result]
20
+ def sync_system(dry_run: false) = MemoryBundle::SystemBundle.sync(dry_run: dry_run)
21
+
22
+ # @return [MemoryBundle::Profile::InstallResult]
23
+ def install(profile, dry_run: false)
24
+ MemoryBundle::Profile.install(dir(profile), shipped_dir: @shipped_dir, dry_run: dry_run)
25
+ end
26
+
27
+ # The members installing +profile+ would install now.
28
+ def to_install(profile)
29
+ MemoryBundle::Profile.to_install(MemoryBundle::Manifest.read(dir: dir(profile)))
30
+ end
31
+
32
+ private
33
+
34
+ def dir(profile) = File.join(@shipped_dir, profile)
35
+ end
36
+ end
37
+ end