samagotchi 0.53.0 → 0.55.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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +52 -1
  3. data/README.md +1 -1
  4. data/bin/chi +3 -2
  5. data/docs/architecture.md +94 -54
  6. data/docs/configuration.md +30 -2
  7. data/docs/context.md +104 -9
  8. data/docs/internals/background-tasks.md +1 -1
  9. data/docs/internals/bundles.md +73 -0
  10. data/docs/internals/context-telemetry.md +27 -6
  11. data/docs/internals/engine.md +55 -24
  12. data/docs/internals/tool-guardrails.md +8 -6
  13. data/docs/plugins.md +2 -1
  14. data/docs/sessions.md +3 -3
  15. data/docs/testing.md +23 -3
  16. data/lib/samagotchi/bridge/turn_accumulator.rb +8 -6
  17. data/lib/samagotchi/bundle_command.rb +31 -36
  18. data/lib/samagotchi/bundle_needs.rb +4 -5
  19. data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
  20. data/lib/samagotchi/bundles/skills/plugin.rb +2 -1
  21. data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
  22. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  23. data/lib/samagotchi/child_move.rb +6 -4
  24. data/lib/samagotchi/config.rb +2 -1
  25. data/lib/samagotchi/context_command.rb +1 -1
  26. data/lib/samagotchi/context_providers.rb +11 -14
  27. data/lib/samagotchi/context_status.rb +10 -18
  28. data/lib/samagotchi/engine.rb +117 -134
  29. data/lib/samagotchi/extension_load.rb +102 -0
  30. data/lib/samagotchi/file_ref.rb +59 -0
  31. data/lib/samagotchi/guardrail_wiring.rb +16 -16
  32. data/lib/samagotchi/kernel_loop.rb +5 -3
  33. data/lib/samagotchi/llm/chat_loop.rb +5 -4
  34. data/lib/samagotchi/memory_bundle/asset_installer.rb +302 -0
  35. data/lib/samagotchi/memory_bundle/builder.rb +170 -178
  36. data/lib/samagotchi/memory_bundle/installed_bundle.rb +90 -0
  37. data/lib/samagotchi/memory_bundle/installer.rb +252 -488
  38. data/lib/samagotchi/memory_bundle/listing.rb +6 -6
  39. data/lib/samagotchi/memory_bundle/manifest.rb +4 -2
  40. data/lib/samagotchi/memory_bundle/plugin_ref.rb +27 -0
  41. data/lib/samagotchi/memory_bundle/profile.rb +21 -21
  42. data/lib/samagotchi/memory_bundle/provenance.rb +38 -40
  43. data/lib/samagotchi/memory_bundle/shipped_update.rb +24 -26
  44. data/lib/samagotchi/memory_bundle/status.rb +22 -24
  45. data/lib/samagotchi/memory_bundle/system_bundle.rb +4 -5
  46. data/lib/samagotchi/memory_bundle/uninstaller.rb +8 -10
  47. data/lib/samagotchi/plugin/loader.rb +8 -8
  48. data/lib/samagotchi/self_report.rb +2 -2
  49. data/lib/samagotchi/send_command.rb +1 -1
  50. data/lib/samagotchi/session_continue.rb +275 -0
  51. data/lib/samagotchi/session_manager.rb +9 -253
  52. data/lib/samagotchi/session_metrics.rb +3 -1
  53. data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -1
  54. data/lib/samagotchi/terminal_ui.rb +12 -2
  55. data/lib/samagotchi/tool_activity.rb +84 -14
  56. data/lib/samagotchi/tool_row_fields.rb +46 -0
  57. data/lib/samagotchi/tool_runner.rb +7 -11
  58. data/lib/samagotchi/version.rb +1 -1
  59. data/lib/samagotchi/web/app.rb +26 -3
  60. data/lib/samagotchi/web/capabilities.rb +13 -9
  61. data/lib/samagotchi/web/editor.rb +38 -0
  62. data/lib/samagotchi/web/message_parts.rb +5 -10
  63. data/lib/samagotchi/web/public/activity.js +35 -18
  64. data/lib/samagotchi/web/public/app.js +240 -1136
  65. data/lib/samagotchi/web/public/commands.js +236 -0
  66. data/lib/samagotchi/web/public/composer.js +101 -0
  67. data/lib/samagotchi/web/public/ctx.js +18 -4
  68. data/lib/samagotchi/web/public/diff_view.js +30 -4
  69. data/lib/samagotchi/web/public/index.html +9 -2
  70. data/lib/samagotchi/web/public/info_bar.js +78 -0
  71. data/lib/samagotchi/web/public/lists_view.js +725 -0
  72. data/lib/samagotchi/web/public/llm_context_chip.js +9 -6
  73. data/lib/samagotchi/web/public/question_card.js +9 -1
  74. data/lib/samagotchi/web/public/question_cards.js +7 -4
  75. data/lib/samagotchi/web/public/refs.js +134 -0
  76. data/lib/samagotchi/web/public/restore.js +81 -0
  77. data/lib/samagotchi/web/public/session_state.js +52 -0
  78. data/lib/samagotchi/web/public/stream_handlers.js +324 -0
  79. data/lib/samagotchi/web/public/thinking_chip.js +6 -7
  80. data/lib/samagotchi/web/public/timing.js +8 -2
  81. data/lib/samagotchi/web/public/turn_events.js +17 -258
  82. data/lib/samagotchi/web/public/turn_html.js +15 -7
  83. data/lib/samagotchi/web/server.rb +4 -4
  84. metadata +17 -2
  85. data/lib/samagotchi/web/public/stable_html.js +0 -18
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f1d170176f088bc87560a05b7596d57925d955011cc5818b93f5ae33ae2b1f55
4
- data.tar.gz: 941c3bd879e09b78144ede81d4c6cf4724f9ca3a7ce8fa62f0174524a26f25bf
3
+ metadata.gz: f90692cf867e2bd082f40c2ae576604656d23169163ed5584f9221ccec528ac1
4
+ data.tar.gz: ac4852809e3ccdc332dd0d052e17671096eb167691cdde6f01fc2ef6e43b9870
5
5
  SHA512:
6
- metadata.gz: ddc3c2d02b18137887e5e90f636b23dd3ea44461f33a723e4033d58f4cfcd9c7be3267db9cc3349aba1d3af6b73a398415696e3af2c8fdad6662704944dad189
7
- data.tar.gz: bd8bded906f5e3855c8f63ab081582b601d4b5332bb10e9ed5a7dff36e185851583c95d28e8b3528ece4d8b34463cc6e65fd16bb95bf2c493de502d9efa79775
6
+ metadata.gz: 541c55286cdad591988db3b43af6c60480061f931b418c83ff44afe3fe62e677df638021f082ce42241bd1f0145ca3a88173f9046341e9d282351cdc92814a1c
7
+ data.tar.gz: ae24243b3978c3f8f87a655c50d9f446128618b63d6dc5b10923f09eef90d11fcd4bee714692d287157340029f83097476c7d057f27c8ddff98c86f30678758b
data/CHANGELOG.md CHANGED
@@ -8,6 +8,55 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.55.0] - 2026-10-10
12
+
13
+ ### Added
14
+
15
+ - Web: refs. A file tool's row (read, edit, write) and the lines of its diff open the file in your editor: they look
16
+ like text until hovered (a dotted underline and `↗`, a diff line's `↗ N`), and a click opens the current file at the
17
+ line read or edited, the diff line's line or the hunk's start. Only a viewer on this machine gets them (a loopback
18
+ request, or this machine on its LAN address); the phone sees plain text. `web.editor` picks the editor: `vscode`
19
+ (the default), `vscode-insiders`, `cursor`, `zed`, `none`, or a URL template with `{path}` and `{line}`. The
20
+ browser and VS Code each ask once to allow it.
21
+ - For development: `script/affected-specs`, the spec files to run for a change (changed specs, name matches,
22
+ docs-reading specs, specs naming the changed class), ranked and capped. It isn't part of the gem. See
23
+ [docs/testing.md](docs/testing.md).
24
+
25
+ ### Fixed
26
+
27
+ - Web: a page that joins mid-turn (or reloads during one) keeps a grep that found nothing as a muted "no match" row,
28
+ as it showed live; it used to lose the mark.
29
+
30
+ ## [0.54.0] - 2026-10-10
31
+
32
+ ### Changed
33
+
34
+ - Web: the ctx chip is always shown and updates on every request (it used to appear only once the context crossed
35
+ 20/40/60/80%): `ctx ?` before a session's first request, with a tooltip saying why, on session cards too. The
36
+ attached TUI's status line gets the same fresh number each request; the model's own `[CONTEXT: …]` line still
37
+ comes only on a rise into a guidance bucket.
38
+ - Web: a denied approval's summary line reads `<tool>: <command> → Denied`; the typed reason shows in full in the
39
+ card body (expand the card) instead of being cut off in the summary.
40
+ - A grep-family search that found nothing (`grep`/`rg`/`ag`/`pgrep` exiting 1 with no stderr) is no longer a
41
+ failed tool call: it doesn't count in the turn's "(N failed)", the session's tool errors or the skills bundle's
42
+ "fix the skill" nudge, and the web row shows a done mark with a muted "no match". The model still sees
43
+ `exit: 1 (no output)`. Turns saved before keep their "error". (skills bundle 0.1.7; run `chi update`.)
44
+
45
+ ### Removed
46
+
47
+ - The `context.status_cadence` setting (config key, `--status-cadence` flag, `SAMAGOTCHI_CONTEXT_STATUS_CADENCE`):
48
+ the context status now streams on every request, so nothing reads it. A config.yml that still lists it gets a
49
+ startup warning (`config: unknown key 'context.status_cadence'`) and otherwise loads.
50
+
51
+ ### Fixed
52
+
53
+ - `chi -p "/cut TEXT" --non-interactive` (and `/queue TEXT`) runs `TEXT` as the turn's prompt, the same as the idle
54
+ REPL does, instead of feeding the model the literal `/cut` / `/queue` words; a bare `/cut` / `/queue` is still
55
+ refused, as before.
56
+ - `chi bundle install` / `upgrade` no longer warns "Checksum mismatch for hook <file>: expected ..., got …" for a
57
+ hook whose manifest entry declares no `sha256` (or a `hooks/*.rb` the manifest doesn't list); a declared sha256 that
58
+ differs still warns. (Since 0.53.0.)
59
+
11
60
  ## [0.53.0] - 2026-10-10
12
61
 
13
62
  ### Changed
@@ -2412,7 +2461,9 @@ and long-lived sessions.
2412
2461
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
2413
2462
  hotkey panel that send selected text or the clipboard to your sessions.
2414
2463
 
2415
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.53.0...HEAD
2464
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.55.0...HEAD
2465
+ [0.55.0]: https://github.com/dm1try/samagotchi/compare/v0.54.0...v0.55.0
2466
+ [0.54.0]: https://github.com/dm1try/samagotchi/compare/v0.53.0...v0.54.0
2416
2467
  [0.53.0]: https://github.com/dm1try/samagotchi/compare/v0.52.0...v0.53.0
2417
2468
  [0.52.0]: https://github.com/dm1try/samagotchi/compare/v0.51.0...v0.52.0
2418
2469
  [0.51.0]: https://github.com/dm1try/samagotchi/compare/v0.50.0...v0.51.0
data/README.md CHANGED
@@ -202,7 +202,7 @@ once, for the session, for the repo, or for the whole rule in the repo.
202
202
  - [Configuration](docs/configuration.md): `config.yml`, hosts, model server transports, timeouts, retries, logs
203
203
  - [Memory](docs/memory.md): scopes, model-specific overlays and model notes
204
204
  - [Sessions](docs/sessions.md): storage, retention, `chi sessions`
205
- - [Attached context](docs/context.md): `chi context`, sources, scopes, waking, the github-pr bundle, safety
205
+ - [Attached context](docs/context.md): `chi context`, sources, scopes, recipes (a file, a note, a stream), waking, the github-pr bundle, safety
206
206
  - [Broadcast](docs/broadcast.md): `chi broadcast`, who a note reaches, tags, triage, what a session gets, the triage log
207
207
  - [chi as a sub-agent](docs/sub-agent.md): `chi send --wait --format json`, `chi answer`, instructions for a parent agent
208
208
  - [Desktop helper](docs/desktop.md): `chi desktop`, the macOS "Send to chi" Service and hotkey
data/bin/chi CHANGED
@@ -137,7 +137,7 @@ parser = OptionParser.new do |opts|
137
137
  "every session outside a repo) or all") { |v| options[:scope] = v }
138
138
  # Accepted values of string entries that the registry doesn't type as enums.
139
139
  value_hints = { "status.line" => "on|off", "web.host" => "127.0.0.1|::1|localhost|lan|IP", "recap.sentences" => "N|N-M",
140
- "web.annotate_presets" => "text|text" }
140
+ "web.annotate_presets" => "text|text", "web.editor" => "vscode|vscode-insiders|cursor|zed|none|URL" }
141
141
  # Universal config flags (implicit convention: ENV SAMAGOTCHI_* ↔ YAML dotted ↔ CLI --kebab)
142
142
  # Generated from Samagotchi::Config registry — Option A: snake leaf in YAML, kebab in CLI.
143
143
  Samagotchi::Config.cli_entries.each do |entry|
@@ -235,7 +235,8 @@ if options[:web]
235
235
  view = Samagotchi::Config.get("web.view")
236
236
  exit Samagotchi::Web::Server.launch(port: port, scope: options[:scope] || "project", open_browser: options[:web_open],
237
237
  markdown: markdown, view: view, new_token: options[:new_token],
238
- annotate_presets: Samagotchi::Config.get("web.annotate_presets"))
238
+ annotate_presets: Samagotchi::Config.get("web.annotate_presets"),
239
+ editor: Samagotchi::Config.get("web.editor"))
239
240
  end
240
241
 
241
242
  # --resume and --attach take a unique id prefix too, like git.
data/docs/architecture.md CHANGED
@@ -6,48 +6,51 @@ A compact visual overview of the current architecture, then the core API in pros
6
6
  ## System map
7
7
 
8
8
  ```
9
- ┌─────────────────────────────────────────┐
10
- │ bin/chi │
11
- │ (CLI entry point) │
12
- └───────────────────┬─────────────────────┘
13
- │
14
- ┌──────────────────────────┴──────────────┐
15
- │ TerminalUI │ ← REPL (Reline),
16
- │ render · status line · REPL commands │ rendering, commands
17
- └──────────────────────────┬──────────────┘
18
- │ delegates
19
- ┌───────────────────────────┴───────────────────────┐
20
- │ Engine (core logic) │
21
- │ system prompt · memory injection │
22
- │ tool declarations · session lifecycle │
23
- │ model↔tool loop — `run_turn` │
24
- └───────────────────────────┬───────────────────────┘
25
- │ delegates
26
- ┌────────────────────────────┴──────────────┐
27
- │ Web::App (Rack) │ ← browser UI
28
- │ session hub · /api/events · /api/* │ via SessionManager file IPC
29
- └───────────────────────────┬───────────────┘
30
- │
31
- ┌───────────────────────────────┬──────────┴───────────────┬──────────────────┐
32
- │ │ │ │
33
- ┌─────┴─────┐ ┌───────┴───────────┐ ┌────────┴───────┐ ┌───────┴────────┐
34
- │ KernelLoop │◀── drives────────▶│ Model API │ │ Session │ │ SessionManager │
35
- │ (the loop)│ │ (LLM HTTP call) │ │ (state, │ │ (bg workers) │
36
- └─────┬──────┘ └─────────────────────┘ │ history) │ └────────────────┘
37
- │ └──────────────────┘ └────────────────┘
38
- │ run_turn events (turn_started, turn_completed,
39
- │ turn_canceled, raw KernelLoop events)
40
- ▼
9
+ bin/chi (CLI entry point) bin/chi web
10
+ │ │
11
+ ┌────────────────────┴────────────────────┐ ┌───────────────┴────────────────┐
12
+ │ TerminalUI │ │ Web::App (Rack) │ ← browser UI
13
+ │ attached (default): AttachLauncher + │ │ session hub · /api/events │
14
+ │ AttachedLoop, no Engine │ │ /api/* · reads session files │
15
+ │ REPL (--no-shared …): its own Engine │ └───────────────┬────────────────┘
16
+ └────────────────────┬────────────────────┘ │ App#relay, bridge_get,
17
+ │ BridgeClient (SSE + HTTP) │ /stream proxy
18
+ └────────────────────┬────────────────────┘
19
+ ▼
20
+ ┌─────────────────────┴─────────────────────┐
21
+ │ Worker (one process per session) + Bridge │ ← forked/spawned by
22
+ │ input queue · idle exit · owner.lock │ SessionManager
23
+ └─────────────────────┬─────────────────────┘
24
+ │ builds, runs turns
25
+ ┌─────────────────────┴─────────────────────┐
26
+ │ Engine (core logic) │
27
+ │ system prompt · memory injection │
28
+ │ tool declarations · session lifecycle │
29
+ │ model↔tool loop — `run_turn` │
30
+ └─────────────────────┬─────────────────────┘
31
+ │
32
+ ┌───────────────────────────────────┴──────────────────────────┐
33
+ │ │ │
34
+ ┌─────┴──────┐ ┌─────────┴──────────┐ ┌────────┴─────────┐
35
+ │ KernelLoop │◀──── drives ────▶│ Model API │ │ Session │
36
+ │ / ChatLoop │ │ (LLM HTTP call) │ │ (state, history) │
37
+ └─────┬──────┘ └────────────────────┘ └──────────────────┘
38
+ │
39
+ │ run_turn events (turn_started, turn_completed,
40
+ │ turn_canceled, raw KernelLoop events)
41
+ ▼
41
42
  ┌──────────────────────────────────────────────────────────────────────────────────┐
42
- │ Tools │
43
- │ execute · read · edit · write · memory · web_fetch · task_create/get/list/ │
44
- │ task_stop/wait │
45
- │ declared via tool_declarations.rb │
43
+ │ Tools │
44
+ │ execute · read · edit · write · memory_read/write · web_fetch · │
45
+ │ task_create/get/list/stop/wait · delegate · delegate_result · ask_user_question │
46
+ │ register_reminder/cancel_reminder/list_reminders · send_note · list_sessions · │
47
+ │ context_read · forget_outputs (+ bundle plugin tools) │
48
+ │ declared via tool_declarations.rb │
46
49
  └──────────────────────────────────────────────────────────────────────────────────┘
47
50
  │
48
51
  ▼
49
52
  ┌──────────────────────────────────────────────────────────────────────────────────┐
50
- │ Persistence │
53
+ │ Persistence │
51
54
  │ Project: ~/.config/samagotchi/memories/projects/<repo>_<hash>/ (per git repo) │
52
55
  │ System: ~/.config/samagotchi/memories/ │
53
56
  └──────────────────────────────────────────────────────────────────────────────────┘
@@ -81,6 +84,9 @@ bin/chi ─▶ TerminalUI ─▶ Engine#run_turn ─▶ KernelLoop ──┬─
81
84
  (turn started / completed / canceled)
82
85
  ```
83
86
 
87
+ That is the REPL; attached (the default) the session's `Worker` calls `Engine#run_turn` and the
88
+ terminal renders the events it gets over the Bridge.
89
+
84
90
  ## Layers at a glance
85
91
 
86
92
  | Layer | Class(es) | Responsibility |
@@ -89,8 +95,9 @@ bin/chi ─▶ TerminalUI ─▶ Engine#run_turn ─▶ KernelLoop ──┬─
89
95
  | UI | `Samagotchi::TerminalUI` | REPL (Reline), rendering, REPL commands. Delegates all core work to an `Engine`. |
90
96
  | Model loops | `KernelLoop` (via `LLM::NativeBackend`), `LLM::ChatLoop` | The model↔tool loop: raw prompt or OpenAI chat API, chosen per host (see below). |
91
97
  | Adapters | `Samagotchi::Client`, `LLM::OpenAIChat`, `LLM::HTTP` | Raw-prompt servers, the OpenAI chat API, and the HTTP both share. |
92
- | Tools | `lib/samagotchi/tools/*` | Execute, read, edit, write, memory, task_*, web_fetch, plus runtime/output-guardrails. |
93
- | Background | `Samagotchi::SessionManager` | Builds `Engine` directly (no terminal rendering) for workers. |
98
+ | Tools | `lib/samagotchi/tools/*` | Execute, read, edit, write, memory, task_*, web_fetch, delegate/delegate_result, ask_user_question, reminders, send_note, list_sessions, context_read, forget_outputs, plus runtime/output-guardrails. |
99
+ | Bundles | `lib/samagotchi/memory_bundle/*`, `BundleCommand` | `chi bundle`: install/upgrade (3-way merge), build, uninstall; the install record under `.bundles/<name>/` that the hook, guardrail and plugin loaders read ([internals/bundles.md](internals/bundles.md)). |
100
+ | Background | `Samagotchi::Worker`, `Samagotchi::SessionManager` | `SessionManager` forks/spawns one worker process per session; its `Worker` builds the `Engine` and `Bridge` (no terminal rendering) and runs queued turns. |
94
101
  | 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
102
  | Sessions | `Samagotchi::Session`, `SessionManager` | File `sessions/<uuid>.json` + sidecar `input/`/`output/`; retention 14d/500, `updated_at desc`, lazy sweep. |
96
103
 
@@ -110,15 +117,16 @@ bin/chi ─▶ TerminalUI ─▶ Engine#run_turn ─▶ KernelLoop ──┬─
110
117
  `dismiss_question`). The worker runs in the session's `working_directory`, so `!cmd` and
111
118
  the tools don't depend on where the terminal attached from.
112
119
  - `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 (`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
- - `--prompt`, `--non-interactive`, and `SessionManager` workers build `Engine` directly.
120
+ - `bin/chi sessions {list,stop,restart,archive,unarchive,delete,prune,clean,stats}` (`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.
121
+ - In REPL mode `TerminalUI` builds the `Engine` in process (`-p` and `--non-interactive` too);
122
+ otherwise the session's `Worker` (`lib/samagotchi/worker.rb`, spawned by `SessionManager`) builds it.
115
123
 
116
124
  ## Session retention & ordering
117
125
 
118
126
  - **Files:** `~/.local/state/samagotchi/sessions/<uuid>.json` + `<uuid>/input|output|owner.lock|bridge.json` (XDG-aware), and the `<uuid>/stopped` and `<uuid>/archived` marker files.
119
127
  - **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
128
  - **Status:** `status` is turn state (`idle`/`running`); liveness is the lock. A stop is the `<uuid>/stopped` marker (`Session.mark_stopped`; a resume removes it), which `Session.load` and `.summary_from_file` read as status `stopped`, so no other process rewrites a worker's session file.
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`.
129
+ - **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), manual via `bin/chi sessions prune --dry-run`.
122
130
  - **Ordering:** `Session.list(sort:,order:,limit:,offset:)` and `GET /api/sessions?sort=&order=&limit=&offset=` default `updated_at desc`; Web UI sort/filter/pagination.
123
131
  - **Test hygiene:** `test_run` flag when `SAMAGOTCHI_ENV=test`/`RACK_ENV=test`/`CI`, targetable via `prune --test-only` / `clean`.
124
132
 
@@ -133,7 +141,7 @@ agent logic and can be used without any terminal rendering; the UI is a thin lay
133
141
  | UI | `Samagotchi::TerminalUI` | Interactive REPL (Reline), rendering (ANSI, spinner, status line), REPL commands. Delegates all core work to an `Engine`. |
134
142
  | 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
143
  | 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
- | 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. |
144
+ | Web (Rack) | `Samagotchi::Web::App`, `SessionManager` | Single-port `127.0.0.1:4567` control plane via `rack`+`webrick` (serve `index.html` + `/api/*`). It talks to each worker's Bridge (`App#relay`, `bridge_get`; `/stream` proxies it) and reads the session files; no `Engine` of its own. `bin/chi web` entrypoint. |
137
145
  | Sessions | `Samagotchi::Session`, `SessionManager` | File-based `~/.local/state/samagotchi/sessions/<uuid>.json` + sidecar `input/`/`output/`; retention (14d/500) + ordering (`updated_at desc`). |
138
146
 
139
147
  - `bin/chi` in REPL mode (see `LaunchMode` above) builds `TerminalUI`. `TerminalUI#run` is the single
@@ -141,7 +149,7 @@ agent logic and can be used without any terminal rendering; the UI is a thin lay
141
149
  builds the working session once, runs a single prompt turn when `-p` is given,
142
150
  then either exits (`--non-interactive`) or drops into the REPL carrying the
143
151
  post-turn conversation.
144
- - `SessionManager` background workers build `Engine` directly (no terminal rendering).
152
+ - A session's `Worker` (spawned by `SessionManager`) builds its `Engine` (no terminal rendering).
145
153
  - `bin/chi` in attached mode (the default, `--attach`, `--shared`) builds no `Engine`: `TerminalUI::AttachLauncher` finds
146
154
  or starts the worker, and `TerminalUI::AttachedLoop` is a client of its Bridge
147
155
  (`BridgeClient#follow` for events, `post_turn`/`cancel`/`answer` for input). It
@@ -168,18 +176,23 @@ engine.run_turn(session, "hello", on_event: nil) # => LLM::ModelResult (`.outp
168
176
  forwards the raw `KernelLoop` events unchanged (the low-level contract) and adds a few
169
177
  higher-level events so UIs get clean turn boundaries without inferring them:
170
178
 
171
- - `:turn_started` — `{ session_id:, prompt: }`
172
- - `:turn_completed` — `{ result: }` (the final `LLM::ModelResult`)
173
- - `:turn_canceled` — `{ cancellation_reason:, cancelled_by: }` (`cancelled_by`: the bundle or hook label that
179
+ - `:turn_started` — `{ session_id:, prompt:, turn_id: }` (plus `continue: true` for a continue
180
+ turn, `images:` with the prompt's image refs)
181
+ - `:turn_completed` — `{ result:, turn_summary:, display_pending: }` (`result`: the final
182
+ `LLM::ModelResult`; `display_pending`: `after_turn` hooks may still present the answer)
183
+ - `:turn_canceled` — `{ cancellation_reason:, cancelled_by:, duration_ms: }` (`cancelled_by`: the bundle or hook label that
174
184
  stopped it, for `cancellation_reason: :hook`; else nil)
175
185
 
186
+ A turn started with `origin:` carries it on each of these (and `:turn_failed`).
187
+
176
188
  Every event is a `Hash` with a `:type` symbol key; the sink must not raise (the Engine
177
189
  rescues sink errors). A new UI (web, API) supplies its own `on_event` and
178
190
  renders whatever it needs from the stream + final `Result`. The public Engine API:
179
191
 
180
192
  ```ruby
181
- engine.run_turn(session, prompt, on_event: nil, max_iterations: nil, cancel_controller: nil) # nil: turn.max_iterations
182
- engine.run(session: nil, prompt: "...", on_event: nil) # create/resume session + run
193
+ engine.run_turn(session, prompt, on_event: nil, max_iterations: nil, cancel_controller: nil, # nil: turn.max_iterations
194
+ max_tool_output_chars: nil, pending_input: nil, continue: false, origin: nil,
195
+ images: [], id: nil)
183
196
  engine.system_prompt # fully built system prompt string
184
197
  engine.session # current session (Engine owns create/resume)
185
198
  ```
@@ -240,6 +253,9 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
240
253
  `501 not_supported` with the restart hint; a Bridge `404 unknown_session`, no bridge or a
241
254
  refused connection is `503 not_live`.
242
255
  - `POST /session/:id/cancel` — cancel the running turn; `202`, or `409` when none runs.
256
+ - `POST /session/:id/recap` — `/recap` in an attached TUI: `200 {enabled, saved, request,
257
+ min_user_turns}` (`{enabled: false}` without recaps); a new recap is asked for at once and
258
+ arrives as `:recap_ready`. Answers mid-turn too.
243
259
  - `POST /session/:id/answer` — answer the pending question; `200`, `409` when another client
244
260
  answered first or it is gone, `400` for an invalid selection. With `client_id: "cli:answer"`
245
261
  (`chi answer`, a parent agent) an allow on an approval beyond the worker's
@@ -318,8 +334,8 @@ Every route goes through `Bridge#dispatch`: an id other than the bridge's own se
318
334
  The per-session port is OS-assigned (bound to `0`) and published to a `bridge.json` sidecar
319
335
  for client discovery. `chi web`'s `GET /api/sessions/:id/stream` proxies this bridge
320
336
  (503 `not_live` when the worker is not running; full history of any session is served by
321
- `GET /api/sessions/:id/output`). Resume/ring-buffer state is **in-memory** (v1) — durable
322
- cross-process resume is a staged next step, not part of v1.
337
+ `GET /api/sessions/:id/output`). Resume/ring-buffer state is **in-memory**, per worker: a cursor from
338
+ an earlier worker gets the `reset` snapshot, not a replay.
323
339
 
324
340
  **Session hub.** The cross-session layer (which sessions exist, who owns them, what changed)
325
341
  never pulls from the page: `chi web` runs one `Samagotchi::Web::SessionHub` (a thread inside
@@ -394,9 +410,10 @@ its own). An empty answer with a `length` stop and the context 90 % full or
394
410
  more is not asked again (a full window, not a thinking loop); with no finish
395
411
  reason it is. Both also keep one `ContextStatus` per turn: before each request
396
412
  it estimates how full the window is (the server's last count plus what the turn
397
- appended since), emits `:context_status` on a bucket change, and on a rise past
398
- the second threshold puts the model's own `[CONTEXT: …]` line on the tail as a
399
- system message (docs/internals/context-telemetry.md).
413
+ appended since), emits `:context_status` on every request (so the web and the
414
+ attached TUI get a fresh number each time), and on a rise past the second
415
+ threshold puts the model's own `[CONTEXT: …]` line on the tail as a system
416
+ message (docs/internals/context-telemetry.md).
400
417
 
401
418
  **Adapters.** `Client` (raw-prompt servers) and `LLM::OpenAIChat` (one per host,
402
419
  `HostRegistry#adapter_for`) share `LLM::HTTP`: timeouts, TLS for https, a line
@@ -421,3 +438,26 @@ server (`/props`), then the host's model list, then config.
421
438
  **Keys.** A host's API key comes only from the environment variable its
422
439
  `api_key_env:` names; it never reaches config.yml, `HOSTS_JSON`, `chi self`,
423
440
  logs or events.
441
+
442
+ ## Other components
443
+
444
+ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/`):
445
+
446
+ | Component | Main file(s) | What it is |
447
+ |---|---|---|
448
+ | Worker | `worker.rb` (`worker_idle_exit.rb`, `worker_inbound.rb`) | A session's background process: builds the `Engine` and `Bridge`, runs queued turns, exits when idle. |
449
+ | Config | `config.rb` | Every setting as an `Entry`: env, config.yml and CLI layers ([configuration.md](configuration.md)). |
450
+ | MemoryBundle | `memory_bundle.rb`, `memory_bundle/{installer,builder,installed_bundle,plugin_ref,bundle_hook}.rb` (`Installer`, `Builder`, `InstalledBundle`, `PluginRef`, `BundleHook`) | Installing, building and reading bundles of memories, hooks and plugins. |
451
+ | Plugin API | `plugin/api.rb` (`Plugin::Api`), `plugin/loader.rb` | What a bundle's plugin registers: tools, commands, hooks, services ([plugins.md](plugins.md)). |
452
+ | Shipped bundles | `bundles/` (`mcp`, `github-pr`, `loop-guard`, `skills`, `source-links`, …) | Bundles that ship with chi. |
453
+ | Hooks | `hooks/registry.rb` (`Hooks::Registry`), `hooks/loader.rb` | Hook registration and dispatch ([hooks.md](hooks.md)). |
454
+ | Guardrails | `guardrails.rb`, `guardrails/shell_lex.rb`, `guardrails/rules.rb`, `guardrail_wiring.rb` | The gate before every tool call: shell lexing, YAML rules, approvals ([guardrails.md](guardrails.md)). |
455
+ | Delegation | `tools/delegate.rb`, `tools/delegate_result.rb`, `tools/delegate_wait.rb`, `child_reports.rb`, `relay_desk.rb` | Child sessions a session starts and waits on, and their approval relay ([sessions.md](sessions.md#delegating)). |
456
+ | send / broadcast | `send_command.rb` (`SendCommand`), `broadcast_command.rb` (`BroadcastCommand`), `broadcast/` | `chi send` and `chi broadcast` ([broadcast.md](broadcast.md), [sub-agent.md](sub-agent.md)). |
457
+ | Idle work | `idle_scheduler.rb` (`IdleScheduler`), `idle_recap.rb` (`IdleRecap`), `idle_reminders.rb` | One poller for an idle session's recap and reminders. |
458
+ | Attached context | `context_sources.rb` (`ContextSources`), `context_poller.rb`, `context_absorber.rb` | External sources a session keeps fresh ([context.md](context.md)). |
459
+ | LLM context strategies | `llm_context_strategy.rb`, `llm_context_view.rb`, `llm_context_stale.rb`, `llm_context_forget.rb` | What a request sends of the history (none, stale, forget) ([internals/llm-context-forget.md](internals/llm-context-forget.md)). |
460
+ | Session chains | `session_continue.rb` (`SessionContinue`), `session_chain.rb` (`SessionChain`) | A session that continues an earlier one. |
461
+ | Analytics | `analytics_file.rb` (`AnalyticsFile`) | A session's `analytics.json` (`SessionMetrics#persist`). |
462
+ | Images / vision | `image_store.rb` (`ImageStore`), `vision_support.rb`, `vision_context.rb` | Stored prompt images and whether a model takes them. |
463
+ | Desktop | `desktop.rb`, `desktop/macos.rb`, `desktop_command.rb` | `chi desktop`, the macOS "Send to chi" helper ([desktop.md](desktop.md)). |
@@ -1094,6 +1094,34 @@ its own). The first target is a long session such as a day-long coordinator, whe
1094
1094
  turn that runs a layer or whose session set its strategy) and the web's chip. A woken worker's status line counts
1095
1095
  its saved context against the session's budget.
1096
1096
 
1097
+ ## Opening files from chi web
1098
+
1099
+ In `chi web`, a file tool's row (read, edit, write) and the lines of its diff are refs to that file. They look like
1100
+ text until hovered (the title gets a dotted underline and `↗`, a diff line an accent bar and `↗ N`); a click opens the
1101
+ file in your editor: a row's title at the lines it read or edited (an edit without a range at its first changed
1102
+ line, a write at line 1), a diff line at its line, a hunk header at the hunk's start. A drag that selects text isn't
1103
+ a click. `web.editor` picks what opens:
1104
+
1105
+ ```yaml
1106
+ web:
1107
+ editor: vscode # or vscode-insiders, cursor, zed, none
1108
+ # editor: "idea://open?file={path}&line={line}" # a template of your own
1109
+ ```
1110
+
1111
+ - A preset, or a URL template with `{path}` (the absolute path, each segment URL-encoded, the slashes kept) and an
1112
+ optional `{line}` (1 when there is none). A value that is neither a preset nor has `{path}` logs a warning and
1113
+ counts as `none`. A change needs a `chi web` restart.
1114
+ - A template that puts `{path}` in a query (`?file={path}`) gets the path's slashes unencoded; editors accept that.
1115
+ - The first click asks twice, once each: the browser asks to open the editor's links (choose "always allow"; Safari
1116
+ and Chrome ask per origin, so another `chi web` port asks again), and VS Code asks to allow opening local files.
1117
+ - It opens the file as it is now, not the agent's copy: after later edits a diff line's number can point elsewhere,
1118
+ and a file in a worktree that was removed won't open. The row's diff and output are what the agent saw.
1119
+ - Only a viewer on this machine gets refs: a loopback request, or this machine on its own LAN address
1120
+ (`web.host: lan`, the LAN URL or its QR code opened here). A phone sees plain text. An SSH tunnel or a local
1121
+ reverse proxy (`tailscale serve`) looks like loopback, so a remote viewer behind one gets refs that can't open.
1122
+ - `GET /api/sessions/:id` (and its `?tail=1`) carries `capabilities.editor` (true for such a viewer) and
1123
+ `editor_url`, the template (null otherwise).
1124
+
1097
1125
  ## All settings
1098
1126
 
1099
1127
  Every setting below takes the three forms described in
@@ -1167,8 +1195,7 @@ described in their own sections.
1167
1195
  | `llm_context.policy` | the subtask-boundaries line | | The sentence `forget_outputs`' description carries (the `forget` layer): "Tidy at subtask boundaries: once a subtask is done, forget its tool outputs and note what it established; keep anything you'll still edit against." Blank: none. Read when a session starts. |
1168
1196
  | `context.chars_per_token` | `4.0` | yes | Estimate ratio when the server reports no usage. |
1169
1197
  | `memory.index_warn_tokens` | `2500` | yes | The tokens one scope's memory index (project or system; sent with every prompt) may hold before a `memory_write`, `write` or `edit` that takes it over gets a note asking the model to tighten long index descriptions; `0`: no note. Estimated as characters / `context.chars_per_token`. `memory:` is its own section, not the `memories:` preload list. See [Memory](memory.md). |
1170
- | `context.status_thresholds` | `20,40,60,80` | yes | Percentages that trigger a status. |
1171
- | `context.status_cadence` | `0` | yes | Also every N rounds; `0` = thresholds only. |
1198
+ | `context.status_thresholds` | `20,40,60,80` | yes | Percentages that put the estimate into a bucket; the rise into a guidance bucket (from the second up) leaves the model its own `[CONTEXT: …]` line, and the bucket decides the guidance. |
1172
1199
  | `context.every_seconds` | `300` | | How often an attached context source's command runs when it has no `--every` (seconds, at least 30). See [Attached context](context.md). |
1173
1200
  | `context.wake` | `true` | yes | A source whose update says `wake: true` may start a turn in a live, idle session (one per source per 10 minutes, within `session.max_wakes`); `false`: updates wait as notes for the next turn. See [Waking](context.md#waking). |
1174
1201
  | `thinking.turn_preamble` | `true` | yes | Ask a `qwen36` model to open its thinking with a short `TURN:` line (the step label). |
@@ -1209,6 +1236,7 @@ described in their own sections.
1209
1236
  | `web.markdown` | `false` | yes | Render answers as Markdown in `chi web`. |
1210
1237
  | `web.view` | `stage` | yes | How `chi web` draws a turn: `stage` (the running turn pinned above the composer) or `turn` (one block per turn in the history). See [CLI](cli.md#web-views). |
1211
1238
  | `web.annotate_presets` | `Agreed\|Could you please elaborate?` | yes | Quick replies next to Annotate in `chi web`, `\|`-separated (a YAML list works too); `""` in the file or on the CLI leaves only Annotate (an empty env value means the default). See [CLI](cli.md#web-annotate-presets). |
1239
+ | `web.editor` | `vscode` | yes | What a file ref in `chi web` (a file tool row's title, a diff line) opens, for a viewer on this machine: a preset (`vscode`, `vscode-insiders`, `cursor`, `zed`) or a URL template with `{path}` and an optional `{line}`; `none` turns refs off. See [Opening files from chi web](#opening-files-from-chi-web). |
1212
1240
  | `history.file` | state dir | | Prompt history path, shared by the TUI and the web composer. |
1213
1241
  | `no_interrupt` | `false` | `--no-interrupt` | Raise the step limit of a turn to 1000 (or `turn.max_iterations`, when larger); a top-level key. |
1214
1242
  | `no_default_input` | `false` | `--no-default-input` | Don't pre-fill `default.input`; a top-level key. |
data/docs/context.md CHANGED
@@ -25,7 +25,8 @@ update.
25
25
  - **A command** (`--cmd CMD`): chi runs it every `--every` seconds (at least
26
26
  30; default `context.every_seconds`, 300) while the session's worker is up.
27
27
  - **Pushed** (`--push`): nothing runs; `chi context push NAME` (stdin, or
28
- `-m TEXT`) sets its text. Only a pushed source takes a push: a command
28
+ `-m TEXT`) sets its text, as plain text or the same JSON a command prints
29
+ ([below](#the-command-contract)). Only a pushed source takes a push: a command
29
30
  source's text is its command's.
30
31
  - **A URL** (`chi context add URL`): an installed bundle's provider turns it
31
32
  into a command source (name, command, hint, interval). The `github-pr`
@@ -38,19 +39,24 @@ for up to 60 s; stdout is the text (up to 1 MiB), stderr goes to the debug
38
39
  log. It gets two variables:
39
40
 
40
41
  - `SAMAGOTCHI_CONTEXT_NAME`: the source's name;
41
- - `SAMAGOTCHI_CONTEXT_PREVIOUS`: the path of the last snapshot (JSON with
42
- `text`), absent the first time, so the script can say what changed.
42
+ - `SAMAGOTCHI_CONTEXT_PREVIOUS`: the path of the last snapshot, absent the
43
+ first time, so the script can say what changed. It is JSON: `text` (missing
44
+ while every run so far failed), `summary`, `fetched_at`, `error` and a few
45
+ more. Only the text is kept from your output: an extra key of yours (a
46
+ cursor) isn't there next time.
43
47
 
44
48
  Its output is either:
45
49
 
46
50
  - **plain text**: all of it is the text; chi's summary is
47
- `content changed (+12/−3 lines)`. It never wakes the session.
48
- - **JSON**, an object with a string `text`:
51
+ `content changed (+12/−3 lines)` (`12 lines of text` the first time). It never wakes the session.
52
+ - **JSON**: output that starts with `{` and parses as an object with a string
53
+ `text` (anything else is plain text):
49
54
  ```json
50
55
  {"text": "...the full text...", "summary": "2 new comments; checks failing", "wake": true, "hint": "optional new hint"}
51
56
  ```
52
57
  `summary` (one line, up to 200 characters) goes into the update note;
53
- `wake: true` asks for a turn ([Waking](#waking)). Unknown keys are ignored.
58
+ `wake: true` asks for a turn ([Waking](#waking)); `hint` replaces the
59
+ source's hint until a later output gives another. Unknown keys are ignored.
54
60
 
55
61
  A non-zero exit, a timeout, more than 1 MiB or empty output is a failed
56
62
  fetch: the last good text stays, and the agent hears once, at the first
@@ -84,7 +90,7 @@ web) without touching the other sessions.
84
90
 
85
91
  ```
86
92
  chi context add NAME (--cmd CMD | --push) [--every SECONDS] [--why TEXT] [--hint TEXT] TARGET
87
- chi context add URL [--why TEXT] TARGET a URL an installed bundle's provider knows
93
+ chi context add URL [--why TEXT] [--hint TEXT] TARGET a URL an installed bundle's provider knows
88
94
  chi context push NAME [-m TEXT] [TARGET] new text for a --push source (stdin without -m)
89
95
  chi context ls [TARGET] [--format json]
90
96
  chi context show NAME [--json] [TARGET]
@@ -95,11 +101,100 @@ chi context mute|unmute NAME ID... a project source, for one sessi
95
101
 
96
102
  `TARGET` is session ids (or unique prefixes) or `--project`. Inside a chi
97
103
  session (its `execute`) the default is that session. `NAME` is `a-z`, `0-9`
98
- and `-`, up to 40 characters.
104
+ and `-`, up to 40 characters, starting with a letter or digit
105
+ (`subscriptions` is taken).
99
106
 
100
107
  In a session, `/context` lists what is attached (as `context_read` does
101
108
  with no name).
102
109
 
110
+ ## Recipes
111
+
112
+ Anything that prints text is a source.
113
+
114
+ ```sh
115
+ # a file you keep editing
116
+ chi context add todo --cmd 'cat ~/notes/todo.md' --every 60 --why "my running todo" --project
117
+
118
+ # an Apple Note (macOS asks once to let chi control Notes)
119
+ chi context add plan --cmd "osascript -e 'tell application \"Notes\" to get plaintext of note \"Plan\"'" --project
120
+
121
+ # pushed by something else: a Shortcut, a cron job, the clipboard
122
+ chi context add clip --push --project
123
+ pbpaste | chi context push clip --project
124
+ ```
125
+
126
+ ### A stream: chat, a log, a feed
127
+
128
+ A source's text is a snapshot, not a log: each run replaces it. For
129
+ something that only grows (a chat channel, CI events, a log), let the script
130
+ keep a window of the latest items and fetch only what's new. chi keeps no
131
+ state for the script apart from the last text, so the cursor lives in the
132
+ text: each item is a line starting with its id, and the script reads the
133
+ newest id from `SAMAGOTCHI_CONTEXT_PREVIOUS`.
134
+
135
+ ```ruby
136
+ #!/usr/bin/env ruby
137
+ # chat-feed: the last KEEP items of a stream, fetching only the new ones.
138
+ require "json"
139
+
140
+ KEEP = 50
141
+ LINE = /\A#(?<id>\S+) / # each item is one line: "#<id> 14:02 @ann: text"
142
+
143
+ # Swap this for the real thing (Slack's conversations.history with oldest:,
144
+ # a log file, an RSS feed…). Here: items from a JSONL file, one per line.
145
+ def fetch_since(since_id)
146
+ File.readlines(ARGV.fetch(0), chomp: true).map { JSON.parse(_1) }
147
+ .select { since_id.nil? || _1["id"].to_s > since_id }
148
+ end
149
+
150
+ prev_path = ENV["SAMAGOTCHI_CONTEXT_PREVIOUS"]
151
+ prev = prev_path && JSON.parse(File.read(prev_path))["text"] # absent or no text: first run
152
+ old_lines = prev.to_s.lines(chomp: true).grep(LINE)
153
+ since_id = old_lines.last&.match(LINE)&.[](:id)
154
+
155
+ new_items = fetch_since(since_id)
156
+ new_lines = new_items.map { "##{_1["id"]} #{_1["ts"]} @#{_1["author"]}: #{_1["text"].tr("\n", " ")}" }
157
+ lines = (old_lines + new_lines).last(KEEP)
158
+
159
+ summary =
160
+ if new_items.empty? then "no new messages"
161
+ else "#{new_items.size} new from #{new_items.map { "@#{_1["author"]}" }.uniq.join(", ")}"
162
+ end
163
+ wake_word = ENV["FEED_WAKE_ON"]
164
+ wake = !wake_word.nil? && new_items.any? { _1["text"].include?(wake_word) }
165
+
166
+ puts JSON.generate(text: lines.empty? ? "(no messages yet)" : lines.join("\n"), summary:, wake:)
167
+ ```
168
+
169
+ ```sh
170
+ chi context add chat --cmd "FEED_WAKE_ON=@me ~/bin/chat-feed ~/chat.jsonl" --why "team chat" --project
171
+ ```
172
+
173
+ What makes it work:
174
+
175
+ - **Nothing new, same text.** The script prints the old window again, the
176
+ hash doesn't change, and the agent hears nothing.
177
+ - **The window keeps it small.** The text has to stay under 1 MiB, and a
178
+ small model's context window is smaller still.
179
+ - **The summary gives counts and authors, never the words** ("3 new from
180
+ @ann, @bob"), like the github-pr bundle's: other people's words reach the
181
+ agent only when it reads the text, framed as third-party.
182
+ - **Wake rarely.** A busy channel that wakes on every message keeps a
183
+ session (and its bill) running; wake on what is meant for you.
184
+ - **A failed run keeps the last text**, so the next run picks up from the
185
+ last good cursor.
186
+ - **Edits and deletions aren't seen.** An id already in the window isn't
187
+ fetched again.
188
+
189
+ The cursor lives in the text because chi keeps nothing else from the output.
190
+ A script that wants it out of the text keeps its own file (say under
191
+ `$XDG_STATE_HOME/<your tool>/`, by `$SAMAGOTCHI_CONTEXT_NAME`). Two sessions
192
+ with a session source of the same name would share that file.
193
+
194
+ An attached source feeds the sessions you attach it to. To drop an item and
195
+ let chi find the sessions it concerns, pipe it to
196
+ [`chi broadcast`](broadcast.md) instead.
197
+
103
198
  ## What the agent sees
104
199
 
105
200
  Notes join the conversation **between turns** (a change during a turn waits
@@ -226,7 +321,7 @@ project root, working directory, branch, PR URL, main checkout or worktree,
226
321
  model), `attached` and `declined` (with the seconds since the offer), and
227
322
  `first_prompt` (the first 160 characters of every session's first message,
228
323
  offered or not; a fork's first after the conversation it started from, once;
229
- not scratch sessions or delegate children). Over 1 MB it is
324
+ not scratch sessions or delegate children). Over 1 MiB it is
230
325
  renamed to `offers.ndjson.1` (one old file is kept). Only you can read it
231
326
  (0600), and nothing leaves your machine.
232
327
 
@@ -24,6 +24,6 @@ Behavior:
24
24
  - `task_get` returns metadata and `output_path`; use `read` for output contents.
25
25
  - `task_wait` accepts `timeout`, `tail_lines` (maximum 100), and `done_pattern` (a regular expression string).
26
26
  - `task_create` accepts `env` as a JSON object string for deterministic overrides such as `PATH`; use an absolute interpreter path when that is simpler. Ruby/Bundler isolation variables remain protected.
27
- - `execute` and `task_create` export chi's own markers into the command, set over the model's `env` (which can't set them: `Error: env key is reserved`): `SAMAGOTCHI_PARENT_SESSION` (the session's id, `chi` without one; a chi started there answers as a parent agent) and `SAMAGOTCHI_SESSION_MODEL` (the resolved ref the session runs on, read at each call so it follows `/model`; unset when unknown). `chi self` reads the latter for its `model` row. A session's worker never inherits them from whoever started it (a `chi send` run by another session's `execute`): its other commands (a user's `!cmd`, an MCP server, a context command) have none.
27
+ - `execute` and `task_create` export chi's own markers into the command, set over the model's `env` (which can't set them: `Error: env key is reserved`): `SAMAGOTCHI_PARENT_SESSION` (the session's id, `chi` without one; a chi started there answers as a parent agent), `SAMAGOTCHI_SESSION_MODEL` (the resolved ref the session runs on, read at each call so it follows `/model`; unset when unknown) and `SAMAGOTCHI_SESSION_THINKING` (the session's own thinking level; unset when it has none). `chi self` reads the last two for its model and thinking level. The three are `TaskRuntime::MARKER_ENV_KEYS` (and `SessionManager::SESSION_MARKER_ENV`, which a spawned worker unsets). A session's worker never inherits them from whoever started it (a `chi send` run by another session's `execute`): its other commands (a user's `!cmd`, an MCP server, a context command) have none.
28
28
 
29
29
  Work that needs a model, not a shell command, goes to a child chi session instead: `delegate` / `delegate_result` have the same shape (start, then wait) and return only the child's final reply; see [Sessions: Delegating](../sessions.md#delegating).