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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +52 -1
- data/README.md +1 -1
- data/bin/chi +3 -2
- data/docs/architecture.md +94 -54
- data/docs/configuration.md +30 -2
- data/docs/context.md +104 -9
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bundles.md +73 -0
- data/docs/internals/context-telemetry.md +27 -6
- data/docs/internals/engine.md +55 -24
- data/docs/internals/tool-guardrails.md +8 -6
- data/docs/plugins.md +2 -1
- data/docs/sessions.md +3 -3
- data/docs/testing.md +23 -3
- data/lib/samagotchi/bridge/turn_accumulator.rb +8 -6
- data/lib/samagotchi/bundle_command.rb +31 -36
- data/lib/samagotchi/bundle_needs.rb +4 -5
- data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/plugin.rb +2 -1
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/child_move.rb +6 -4
- data/lib/samagotchi/config.rb +2 -1
- data/lib/samagotchi/context_command.rb +1 -1
- data/lib/samagotchi/context_providers.rb +11 -14
- data/lib/samagotchi/context_status.rb +10 -18
- data/lib/samagotchi/engine.rb +117 -134
- data/lib/samagotchi/extension_load.rb +102 -0
- data/lib/samagotchi/file_ref.rb +59 -0
- data/lib/samagotchi/guardrail_wiring.rb +16 -16
- data/lib/samagotchi/kernel_loop.rb +5 -3
- data/lib/samagotchi/llm/chat_loop.rb +5 -4
- data/lib/samagotchi/memory_bundle/asset_installer.rb +302 -0
- data/lib/samagotchi/memory_bundle/builder.rb +170 -178
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +90 -0
- data/lib/samagotchi/memory_bundle/installer.rb +252 -488
- data/lib/samagotchi/memory_bundle/listing.rb +6 -6
- data/lib/samagotchi/memory_bundle/manifest.rb +4 -2
- data/lib/samagotchi/memory_bundle/plugin_ref.rb +27 -0
- data/lib/samagotchi/memory_bundle/profile.rb +21 -21
- data/lib/samagotchi/memory_bundle/provenance.rb +38 -40
- data/lib/samagotchi/memory_bundle/shipped_update.rb +24 -26
- data/lib/samagotchi/memory_bundle/status.rb +22 -24
- data/lib/samagotchi/memory_bundle/system_bundle.rb +4 -5
- data/lib/samagotchi/memory_bundle/uninstaller.rb +8 -10
- data/lib/samagotchi/plugin/loader.rb +8 -8
- data/lib/samagotchi/self_report.rb +2 -2
- data/lib/samagotchi/send_command.rb +1 -1
- data/lib/samagotchi/session_continue.rb +275 -0
- data/lib/samagotchi/session_manager.rb +9 -253
- data/lib/samagotchi/session_metrics.rb +3 -1
- data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -1
- data/lib/samagotchi/terminal_ui.rb +12 -2
- data/lib/samagotchi/tool_activity.rb +84 -14
- data/lib/samagotchi/tool_row_fields.rb +46 -0
- data/lib/samagotchi/tool_runner.rb +7 -11
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +26 -3
- data/lib/samagotchi/web/capabilities.rb +13 -9
- data/lib/samagotchi/web/editor.rb +38 -0
- data/lib/samagotchi/web/message_parts.rb +5 -10
- data/lib/samagotchi/web/public/activity.js +35 -18
- data/lib/samagotchi/web/public/app.js +240 -1136
- data/lib/samagotchi/web/public/commands.js +236 -0
- data/lib/samagotchi/web/public/composer.js +101 -0
- data/lib/samagotchi/web/public/ctx.js +18 -4
- data/lib/samagotchi/web/public/diff_view.js +30 -4
- data/lib/samagotchi/web/public/index.html +9 -2
- data/lib/samagotchi/web/public/info_bar.js +78 -0
- data/lib/samagotchi/web/public/lists_view.js +725 -0
- data/lib/samagotchi/web/public/llm_context_chip.js +9 -6
- data/lib/samagotchi/web/public/question_card.js +9 -1
- data/lib/samagotchi/web/public/question_cards.js +7 -4
- data/lib/samagotchi/web/public/refs.js +134 -0
- data/lib/samagotchi/web/public/restore.js +81 -0
- data/lib/samagotchi/web/public/session_state.js +52 -0
- data/lib/samagotchi/web/public/stream_handlers.js +324 -0
- data/lib/samagotchi/web/public/thinking_chip.js +6 -7
- data/lib/samagotchi/web/public/timing.js +8 -2
- data/lib/samagotchi/web/public/turn_events.js +17 -258
- data/lib/samagotchi/web/public/turn_html.js +15 -7
- data/lib/samagotchi/web/server.rb +4 -4
- metadata +17 -2
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f90692cf867e2bd082f40c2ae576604656d23169163ed5584f9221ccec528ac1
|
|
4
|
+
data.tar.gz: ac4852809e3ccdc332dd0d052e17671096eb167691cdde6f01fc2ef6e43b9870
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
│
|
|
22
|
-
│
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
│
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
│
|
|
43
|
-
│ execute · read · edit · write ·
|
|
44
|
-
│
|
|
45
|
-
│
|
|
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
|
-
|
|
|
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
|
-
-
|
|
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)
|
|
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
|
|
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`
|
|
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
|
-
|
|
173
|
-
- `:
|
|
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
|
|
182
|
-
|
|
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
|
|
322
|
-
|
|
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
|
|
398
|
-
|
|
399
|
-
|
|
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)). |
|
data/docs/configuration.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
42
|
-
|
|
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)
|
|
48
|
-
- **JSON
|
|
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))
|
|
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
|
|
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
|
|
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)
|
|
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).
|