samagotchi 0.4.0 → 0.5.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 +80 -1
- data/README.md +13 -2
- data/bin/chi +29 -39
- data/docs/cli.md +135 -73
- data/docs/configuration.md +15 -20
- data/docs/hooks.md +1 -1
- data/docs/memory.md +40 -0
- data/docs/plugins.md +50 -0
- data/docs/releasing.md +9 -6
- data/docs/sessions.md +3 -3
- data/lib/samagotchi/bootstrap/config_writer.rb +1 -2
- data/lib/samagotchi/bridge/sse_writer.rb +0 -3
- data/lib/samagotchi/bridge/turn_accumulator.rb +1 -0
- data/lib/samagotchi/bridge.rb +16 -11
- data/lib/samagotchi/bundles/skills/manifest.yml +10 -0
- data/lib/samagotchi/bundles/skills/plugin.rb +419 -0
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +7 -8
- data/lib/samagotchi/bundles/system/identity.md +5 -0
- data/lib/samagotchi/bundles/system/manifest.yml +5 -5
- data/lib/samagotchi/bundles/system/memory_guide.md +26 -0
- data/lib/samagotchi/bundles/system/self_map.md +2 -1
- data/lib/samagotchi/client.rb +16 -20
- data/lib/samagotchi/config.rb +40 -100
- data/lib/samagotchi/engine.rb +50 -354
- data/lib/samagotchi/kernel_loop.rb +33 -44
- data/lib/samagotchi/live_versions.rb +7 -1
- data/lib/samagotchi/llm/errors.rb +17 -0
- data/lib/samagotchi/llm/http.rb +4 -18
- data/lib/samagotchi/llm/openai_chat.rb +17 -0
- data/lib/samagotchi/model_profile.rb +4 -10
- data/lib/samagotchi/note_command.rb +2 -1
- data/lib/samagotchi/reply_wait.rb +48 -4
- data/lib/samagotchi/self_report.rb +20 -2
- data/lib/samagotchi/send_command.rb +84 -6
- data/lib/samagotchi/session.rb +4 -2
- data/lib/samagotchi/session_manager.rb +18 -37
- data/lib/samagotchi/system_prompt.rb +403 -0
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +5 -3
- data/lib/samagotchi/terminal_ui/attached_loop.rb +120 -83
- data/lib/samagotchi/terminal_ui/attached_view.rb +27 -12
- data/lib/samagotchi/terminal_ui/event_renderer.rb +23 -7
- data/lib/samagotchi/terminal_ui/formatting.rb +32 -22
- data/lib/samagotchi/terminal_ui/input_support.rb +3 -4
- data/lib/samagotchi/terminal_ui/plain_surface.rb +13 -7
- data/lib/samagotchi/terminal_ui/status_row.rb +81 -0
- data/lib/samagotchi/terminal_ui/surface.rb +1 -1
- data/lib/samagotchi/terminal_ui.rb +95 -690
- data/lib/samagotchi/thinking.rb +11 -0
- data/lib/samagotchi/tool_activity.rb +52 -2
- data/lib/samagotchi/tool_runner.rb +3 -0
- data/lib/samagotchi/tools/execute.rb +3 -3
- data/lib/samagotchi/tools/output_guardrails.rb +8 -7
- data/lib/samagotchi/tools/read.rb +4 -4
- data/lib/samagotchi/update_command.rb +2 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +170 -35
- data/lib/samagotchi/web/lan.rb +99 -0
- data/lib/samagotchi/web/message_parts.rb +15 -11
- data/lib/samagotchi/web/public/activity.js +7 -0
- data/lib/samagotchi/web/public/app.js +99 -54
- data/lib/samagotchi/web/public/chat_view.js +5 -1
- data/lib/samagotchi/web/public/index.html +163 -17
- data/lib/samagotchi/web/public/model_pick.js +136 -0
- data/lib/samagotchi/web/public/model_picker.js +224 -0
- data/lib/samagotchi/web/public/notify.js +10 -0
- data/lib/samagotchi/web/public/stage_model.js +110 -0
- data/lib/samagotchi/web/public/stage_view.js +580 -0
- data/lib/samagotchi/web/public/timing.js +6 -2
- data/lib/samagotchi/web/public/turn_events.js +9 -5
- data/lib/samagotchi/web/public/turn_model.js +11 -3
- data/lib/samagotchi/web/public/turn_view.js +74 -19
- data/lib/samagotchi/web/qr.rb +40 -0
- data/lib/samagotchi/web/server.rb +101 -11
- data/lib/samagotchi/web/token.rb +97 -0
- metadata +27 -3
- data/lib/samagotchi/terminal_ui/legacy_surface.rb +0 -111
data/docs/configuration.md
CHANGED
|
@@ -28,7 +28,6 @@ server: # the model server when there is no hosts: map below
|
|
|
28
28
|
host: 192.0.2.10
|
|
29
29
|
port: 8081
|
|
30
30
|
thinking:
|
|
31
|
-
ui: spinner
|
|
32
31
|
level: default # off | low | medium | high | default; see "Thinking"
|
|
33
32
|
|
|
34
33
|
# Multi-host (optional): aggregated /models and per-model routing.
|
|
@@ -88,6 +87,11 @@ Behavior:
|
|
|
88
87
|
`config: unknown key 'default.modle' (did you mean 'default.model'?)`.
|
|
89
88
|
Names you choose under the maps below (host names, model ids) don't warn.
|
|
90
89
|
- Environment variables and CLI flags win over config-file values.
|
|
90
|
+
- An edit to the file needs no restart of `chi web`: a new session's worker
|
|
91
|
+
reads the file when it starts, and a running chi takes a changed value the
|
|
92
|
+
next time it reads that setting (a worker keeps `hosts:` and what it set up
|
|
93
|
+
at its start until it is stopped). A worker gets the CLI flags of the chi
|
|
94
|
+
that started it through its environment.
|
|
91
95
|
- Workers inherit hosts via `SAMAGOTCHI_HOSTS_JSON` propagated through `SessionManager.spawn_options`.
|
|
92
96
|
|
|
93
97
|
This lets you run `chi` without repeating common defaults such as model
|
|
@@ -115,15 +119,9 @@ one shell (`SAMAGOTCHI_LOG_LEVEL=debug chi`); keep lasting choices in the file.
|
|
|
115
119
|
Most settings also have a CLI flag: the dotted name in kebab case
|
|
116
120
|
(`--server-read-timeout 900`); `chi --help` lists them.
|
|
117
121
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
(`SAMAGOTCHI_DEFAULT_MODEL: my-model`). They are still read, but every run
|
|
122
|
-
warns (`config key 'SAMAGOTCHI_DEFAULT_MODEL' is legacy UPPER — use
|
|
123
|
-
'default.model'`). When a file has both, the nested key wins and the warning
|
|
124
|
-
names both (`both 'SAMAGOTCHI_DEFAULT_MODEL' and 'default.model' are set;
|
|
125
|
-
using 'default.model', remove the flat key`). Move each to its nested form (`default: {model: my-model}`) and delete
|
|
126
|
-
the flat line. `/model --default` already writes the nested form.
|
|
122
|
+
An environment name used as a top-level key (`SAMAGOTCHI_DEFAULT_MODEL: my-model`,
|
|
123
|
+
the old flat form) is not read: it warns as an unknown key with the nested one
|
|
124
|
+
to use (`did you mean 'default.model'?`).
|
|
127
125
|
|
|
128
126
|
## Model Server Transport
|
|
129
127
|
|
|
@@ -409,7 +407,9 @@ What each backend gets:
|
|
|
409
407
|
| native, `gemma4` | no `<\|think\|>` token at the start of the system prompt | no knob, one notice |
|
|
410
408
|
| chat host (`api: openai`) | `chat_template_kwargs: {enable_thinking: false}` and `reasoning_effort: "none"` | `reasoning_effort: <level>` |
|
|
411
409
|
|
|
412
|
-
On chat hosts: llama.cpp honours both off switches but ignores the effort
|
|
410
|
+
On chat hosts: llama.cpp honours both off switches but ignores the effort (when its `/props` says
|
|
411
|
+
`chat_template_caps.supports_reasoning_effort: false`, chi says so once per session and host, from the `/props`
|
|
412
|
+
answer the turn already fetched for the window); Splash takes `reasoning_effort` (off only
|
|
413
413
|
through `none`) and scales with it; OpenRouter translates `reasoning_effort` per model (some can't turn thinking off:
|
|
414
414
|
Qwen3-30B-A3B thinks anyway, gpt-oss refuses).
|
|
415
415
|
|
|
@@ -727,7 +727,7 @@ described in their own sections.
|
|
|
727
727
|
| `session.max_children` | `4` | | Running delegated sessions one session may have. |
|
|
728
728
|
| `session.retention_days` | `14` | yes | Delete sessions not updated for N days; `0` = forever. See [Sessions](sessions.md). |
|
|
729
729
|
| `session.max_count` | `500` | yes | Keep the newest N; `0` = uncapped. |
|
|
730
|
-
| `session.keep_status` |
|
|
730
|
+
| `session.keep_status` | none | yes | Comma list of statuses never pruned (a session a worker or `chi` has open is never pruned anyway). |
|
|
731
731
|
| `session.sweep_interval_hours` | `24` | yes | How often the retention sweep runs. |
|
|
732
732
|
| `image.max_side` | `1568` | | See "Images". |
|
|
733
733
|
| `image.max_bytes` | `3750000` | | See "Images". |
|
|
@@ -736,17 +736,12 @@ described in their own sections.
|
|
|
736
736
|
| `log.file` | state dir | yes | See "Debug Log File". |
|
|
737
737
|
| `log.disable` | `false` | yes | No file logging. |
|
|
738
738
|
| `log.level` | `info` | yes | `debug`, `info`, `warn`, `error`. |
|
|
739
|
-
| `status.line` | `on` | yes | The REPL
|
|
740
|
-
| `status.width_mode` | `terminal_cap` | yes | `terminal_cap` (terminal width up to `max_width`) or `fixed`. |
|
|
741
|
-
| `status.max_width` | `160` | yes | Cap for `terminal_cap`. |
|
|
742
|
-
| `status.fixed_width` | `120` | yes | Width for `fixed`. |
|
|
739
|
+
| `status.line` | `on` | yes | The status row under the prompt (the REPL's and attached mode's), `on` or `off`. |
|
|
743
740
|
| `context.status` | `true` | yes | Context-usage telemetry for the model. See [context telemetry](internals/context-telemetry.md). |
|
|
744
741
|
| `context.window_tokens` | server's, else 256000 | yes | Context window when the server doesn't report one. |
|
|
745
742
|
| `context.chars_per_token` | `4.0` | yes | Estimate ratio when the server reports no usage. |
|
|
746
743
|
| `context.status_thresholds` | `20,40,60,80` | yes | Percentages that trigger a status. |
|
|
747
744
|
| `context.status_cadence` | `0` | yes | Also every N rounds; `0` = thresholds only. |
|
|
748
|
-
| `thinking.ui` | `spinner` | yes | `spinner` or `off`. |
|
|
749
|
-
| `thinking.render_interval` | `0.08` | yes | Seconds between thinking redraws. |
|
|
750
745
|
| `thinking.turn_preamble` | `true` | yes | Ask a `qwen36` model to open its thinking with a short `TURN:` line (the step label). |
|
|
751
746
|
| `thinking.level` | `default` | `--thinking` | `off`, `low`, `medium`, `high` or `default` for every model; the flag and env outrank the `models:`/`hosts:` entries, the file's value doesn't. See "Thinking". |
|
|
752
747
|
| `models.<key>.thinking`, `hosts.<name>.thinking` | none | | A model's or host's level. See "Thinking". |
|
|
@@ -766,9 +761,9 @@ described in their own sections.
|
|
|
766
761
|
| `execute.preview_bytes` | `12288` | yes | |
|
|
767
762
|
| `execute.telemetry_threshold_pct` | `80` | yes | |
|
|
768
763
|
| `web.port` | `4567` | `--port` | `chi web`'s port. See [CLI](cli.md). |
|
|
769
|
-
| `web.host` | `127.0.0.1` | yes | `127.0.0.1`, `::1` or `localhost
|
|
764
|
+
| `web.host` | `127.0.0.1` | yes | `127.0.0.1`, `::1` or `localhost`; `lan` (this machine's private IPv4 address) or one of its IPv4 addresses also opens `chi web` to the network, with an access token (`chi web --new-token` replaces it). Anything else binds `127.0.0.1` with a warning. See [CLI: chi web on your phone](cli.md#chi-web-on-your-phone). |
|
|
770
765
|
| `web.markdown` | `false` | yes | Render answers as Markdown in `chi web`. |
|
|
771
|
-
| `web.
|
|
766
|
+
| `web.view` | `turn` | yes | How `chi web` draws a turn: `turn` (one block per turn), `stage` (the running turn pinned above the composer) or `chat` (the row of bubbles). See [CLI](cli.md#web-views). |
|
|
772
767
|
| `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). |
|
|
773
768
|
| `history.file` | state dir | | Prompt history path. |
|
|
774
769
|
| `no_interrupt` | `false` | `--no-interrupt` | Raise the tool-call limit of a turn to 1000; a top-level key. |
|
data/docs/hooks.md
CHANGED
|
@@ -351,7 +351,7 @@ Notes:
|
|
|
351
351
|
- Ordering: bundle hooks fire by `(priority, bundle_name, hook_name)` (lower priority first), then plain `config.yml` hooks in registration order.
|
|
352
352
|
- Settings: a hook class with `initialize(settings = {})` gets the bundle's section of `config.yml` `bundles:` (see [Settings](#settings)).
|
|
353
353
|
- A bundle can also ship a `plugin.rb` whose `chi.on(event)` blocks are bundle hooks too, next to commands and tools; see [Plugins](plugins.md).
|
|
354
|
-
- Shipped bundles: `chi bundle install guardrails` (rules, see [Guardrails](guardrails.md#the-guardrails-bundle)) and `chi bundle install known-names` (a hook, see [Guardrails](guardrails.md#the-known-names-bundle)), `chi bundle install source-links` (a hook: announces source refs, see [The source-links bundle](#the-source-links-bundle)), `chi bundle install btw` (a plugin: `/btw`, see [Plugins](plugins.md#the-btw-bundle)), `chi bundle install mcp` (a plugin: tools from MCP servers, see [Plugins](plugins.md#the-mcp-bundle)) `chi bundle install loop-guard` (a plugin: breaks tool-call loops, see [Plugins](plugins.md#the-loop-guard-bundle))
|
|
354
|
+
- Shipped bundles: `chi bundle install guardrails` (rules, see [Guardrails](guardrails.md#the-guardrails-bundle)) and `chi bundle install known-names` (a hook, see [Guardrails](guardrails.md#the-known-names-bundle)), `chi bundle install source-links` (a hook: announces source refs, see [The source-links bundle](#the-source-links-bundle)), `chi bundle install btw` (a plugin: `/btw`, see [Plugins](plugins.md#the-btw-bundle)), `chi bundle install mcp` (a plugin: tools from MCP servers, see [Plugins](plugins.md#the-mcp-bundle)) `chi bundle install loop-guard` (a plugin: breaks tool-call loops, see [Plugins](plugins.md#the-loop-guard-bundle)), `chi bundle install check-in` (a plugin: checks on a long turn, see [Plugins](plugins.md#the-check-in-bundle)) and `chi bundle install skills` (a plugin: `/skill`, versions of skills, see [Plugins](plugins.md#the-skills-bundle)).
|
|
355
355
|
- Installing a bundle executes its hook code at `Engine` startup. Only install bundles you trust, as you would a gem. Hooks are **not** executed at install time (copy-only); they are `module_eval`'d at `Engine.new` inside per-bundle `Samagotchi::Bundles::<name>` namespaces (no top-level `require` collisions). Keep hook files side-effect-free at load time; do work in `#call` — top-level side effects (require, IO, `at_exit`, global assignment) run once per `Engine.new` (class redefinition is idempotent).
|
|
356
356
|
|
|
357
357
|
Lifecycle:
|
data/docs/memory.md
CHANGED
|
@@ -34,6 +34,46 @@ names of a comma list are read). Its file and index line are untouched.
|
|
|
34
34
|
These startup index reads are harness-injected context assembly and are not
|
|
35
35
|
rendered as `tool>` activity lines.
|
|
36
36
|
|
|
37
|
+
## Skills
|
|
38
|
+
|
|
39
|
+
A **skill** is a memory named `skill_<name>` that holds the steps of a
|
|
40
|
+
repeatable task you and chi did together: a release, a deploy, a data fix.
|
|
41
|
+
chi is told about skills by the system bundle (`identity.md`, every turn, and
|
|
42
|
+
`memory_guide.md`), so this works without installing anything:
|
|
43
|
+
|
|
44
|
+
- **Saving.** Say "let's memorize this" or "save this as a skill" after the
|
|
45
|
+
task, and chi writes the skill with `memory_write` (project scope; system
|
|
46
|
+
when you ask, or when it isn't about this project) and shows it. On a vague
|
|
47
|
+
one ("I like how we did that") it may ask first.
|
|
48
|
+
- **Following.** The skill's index line (the `description:` of its
|
|
49
|
+
`memory_write`, "Release a new version of this repo: …") is in every
|
|
50
|
+
prompt, so next time chi reads the skill and follows it.
|
|
51
|
+
- **Updating.** When a step turned out different (a renamed script, an extra
|
|
52
|
+
step), chi fixes the skill in the same turn: those steps changed, the rest
|
|
53
|
+
kept, a dated Changelog line added. No confirmation.
|
|
54
|
+
|
|
55
|
+
A skill is plain Markdown, no frontmatter:
|
|
56
|
+
|
|
57
|
+
```markdown
|
|
58
|
+
# Skill: release
|
|
59
|
+
|
|
60
|
+
## Steps
|
|
61
|
+
1. Run `scripts/verify.sh`; stop if it fails.
|
|
62
|
+
2. …
|
|
63
|
+
## Gotchas
|
|
64
|
+
- …
|
|
65
|
+
## Changelog
|
|
66
|
+
- 2026-09-29 created
|
|
67
|
+
- 2026-09-30 step 1: check.sh was renamed to verify.sh
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
It is a memory like any other: `memory_read`, `chi --mute skill_release`,
|
|
71
|
+
`chi bundle build` share it. The optional `skills` bundle (`chi bundle install
|
|
72
|
+
skills`, [docs/plugins.md](plugins.md#the-skills-bundle)) adds `/skill save`,
|
|
73
|
+
`/skill list`, `/skill show`, `/skill diff`, keeps older versions with a
|
|
74
|
+
one-line diff after each update, and nudges a model that skips a failing
|
|
75
|
+
step instead of fixing the skill.
|
|
76
|
+
|
|
37
77
|
## Bundles that need outside commands
|
|
38
78
|
|
|
39
79
|
A bundle's memory can rely on a command chi doesn't ship, such as a GitHub
|
data/docs/plugins.md
CHANGED
|
@@ -814,6 +814,56 @@ with a line and carry on unchanged.
|
|
|
814
814
|
| `/checkin mode ask` / `nudge` / `notify` | the mode |
|
|
815
815
|
| `/checkin nudge` / `later` / `stop` | the card's actions, also by hand |
|
|
816
816
|
|
|
817
|
+
## The skills bundle
|
|
818
|
+
|
|
819
|
+
`chi bundle install skills` installs the bundle shipped with chi
|
|
820
|
+
(`lib/samagotchi/bundles/skills/plugin.rb`): one anytime command, `chi.on`
|
|
821
|
+
hooks, `ctx.sessions.send`, `ctx.notify` and `event[:steer]`. It has no memory
|
|
822
|
+
file; skills themselves work without it ([docs/memory.md](memory.md#skills)).
|
|
823
|
+
|
|
824
|
+
| | |
|
|
825
|
+
|---|---|
|
|
826
|
+
| `/skill save [name] [--system]` | sends this session a request to save what was just done as `skill_<name>` (chi picks a name when none is given), project scope unless `--system`. The request holds the skill's shape, so the result is the same with a model that never read the memory guide. It runs as a turn; sent while a turn runs, it joins that turn at its next step (the request says to finish the task first). An existing skill is updated. In a `--no-shared` REPL, which takes no messages, the command shows the request to send yourself |
|
|
827
|
+
| `/skill list` | the `skill_*` memories of both scopes, with the date and description from the index |
|
|
828
|
+
| `/skill show <name>` | one skill as saved (project first, as `memory_read` looks) |
|
|
829
|
+
| `/skill diff <name> [N]` | the skill now against its N-th newest older version (default 1: before the last change), unified |
|
|
830
|
+
|
|
831
|
+
**History.** Before `memory_write`, `write` or `edit` changes a
|
|
832
|
+
`skill_<name>.md` in a memories folder, the file as it was is kept under
|
|
833
|
+
`$XDG_STATE_HOME/samagotchi/plugins/skills/history/<scope>/<name>/` (`system`,
|
|
834
|
+
or `project-<project folder>`), the newest `history_keep`. It is state, not a
|
|
835
|
+
memory: `chi bundle build` and a synced `~/.config` never see it. After the
|
|
836
|
+
call a line says what happened:
|
|
837
|
+
|
|
838
|
+
```
|
|
839
|
+
skills> skill release saved (project, 14 lines)
|
|
840
|
+
skills> skill release updated (+2 −1): 1. Run `scripts/verify.sh`; stop if it fails. · /skill diff release
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
The line is the file on disk changing, whatever the tool answered; a denied
|
|
844
|
+
write shows nothing.
|
|
845
|
+
|
|
846
|
+
**The nudge** (`nudge: true`). Some models, finding a skill's step broken,
|
|
847
|
+
skip it and go on without fixing the skill. In a turn that read a skill
|
|
848
|
+
(`memory_read` of a `skill_*` name, or `read` of its file), the first failing
|
|
849
|
+
tool call after it (an `execute` that exited non-zero, a tool error) steers
|
|
850
|
+
the model once: *"A step of skill release failed. Find out why before skipping
|
|
851
|
+
it; if the skill is out of date, fix it now: memory_write the whole skill,
|
|
852
|
+
its title and every section as they were, that step fixed, a Changelog line
|
|
853
|
+
added."* If the turn ends with a failed step and the
|
|
854
|
+
skill not rewritten, one line says so: `skill release was followed, a step
|
|
855
|
+
failed, the skill wasn't updated`. A failure unrelated to the skill (a test
|
|
856
|
+
meant to fail) can set it off too: once per turn, and only after a skill was
|
|
857
|
+
read.
|
|
858
|
+
|
|
859
|
+
```yaml
|
|
860
|
+
# config.yml
|
|
861
|
+
bundles:
|
|
862
|
+
skills:
|
|
863
|
+
history_keep: 20 # older versions kept per skill
|
|
864
|
+
nudge: true # steer once when a followed skill's step fails
|
|
865
|
+
```
|
|
866
|
+
|
|
817
867
|
## Shutdown
|
|
818
868
|
|
|
819
869
|
When the REPL exits, or a session's worker exits (an idle exit, `/exit`, a
|
data/docs/releasing.md
CHANGED
|
@@ -12,9 +12,9 @@ can run the whole release; the user approves the notes before the tag and the
|
|
|
12
12
|
and `lib/samagotchi/bundles/system/manifest.yml` are bumped together (a spec
|
|
13
13
|
and `rake release:check` enforce it). The system bundle upgrades itself when
|
|
14
14
|
chi starts.
|
|
15
|
-
- **Every other shipped bundle** (btw, guardrails, known-names,
|
|
16
|
-
mcp) has its own semver in its
|
|
17
|
-
a `requires_chi:` line. They never upgrade by themselves: users run
|
|
15
|
+
- **Every other shipped bundle** (btw, check-in, guardrails, known-names,
|
|
16
|
+
loop-guard, mcp, skills, source-links) has its own semver in its
|
|
17
|
+
`manifest.yml` and, when it needs a newer chi, a `requires_chi:` line. They never upgrade by themselves: users run
|
|
18
18
|
`chi update` (all of them, with the gem) or `chi bundle upgrade NAME`. So:
|
|
19
19
|
- a change to a bundle's files bumps that bundle's `version:`
|
|
20
20
|
(`rake bundles:check` fails otherwise, once there's a tag to compare with);
|
|
@@ -130,9 +130,12 @@ next patch version.
|
|
|
130
130
|
The suite runs the same on Linux CI as on macOS, with no CI-only skips. A few
|
|
131
131
|
things differ there, and a new spec that trips on them fails only on CI:
|
|
132
132
|
|
|
133
|
-
- `CI` set marks every new session a test run, and
|
|
134
|
-
|
|
135
|
-
`test_run: false`.
|
|
133
|
+
- `CI` set marks every new session a test run, and `list_sessions` and
|
|
134
|
+
friends leave test runs out. A spec that lists sessions makes them with
|
|
135
|
+
`test_run: false`. `chi sessions list --live`/`--cwd`/`--format` and
|
|
136
|
+
`chi note --all` show test runs when they run as one (`CI` set counts): a
|
|
137
|
+
spec checking that they are hidden unsets `CI` or stubs
|
|
138
|
+
`Session.test_session_env?`.
|
|
136
139
|
- The gems live under `vendor/bundle`: a child `ruby` started with a bare env
|
|
137
140
|
(`unsetenv_others: true`) needs `GEM_HOME`/`GEM_PATH` to find nokogiri.
|
|
138
141
|
- Ruby 3.3's zlib raises `Zlib::BufError` when a thread interrupt lands in a
|
data/docs/sessions.md
CHANGED
|
@@ -56,12 +56,12 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
|
|
|
56
56
|
|
|
57
57
|
**Projects:** a session belongs to the git project it was started in: the repository, whichever worktree or subfolder of it (the same project root memories use). `chi sessions list` (with `--live` and `--format` too) and `chi web` show the current folder's project; `--scope=all` shows every session, and so does a folder in no repo (`~`). `--cwd PATH` is a folder filter instead of the project. The project is stored with the session (`project_root` in its JSON, `project` in `--format json`), so a session keeps it after its worktree is deleted; sessions older than that are placed by their folder, and one whose folder is gone shows in `--scope=all` only. Retention, `--resume`/`--attach ID`, `chi send`/`chi note ID` and `chi note --all` (every live session) are not scoped. The agent's `list_sessions` lists its own project's sessions; `cwd: "/"` lists every one.
|
|
58
58
|
|
|
59
|
-
`--live`, `--cwd` and `--format` make `list` a picker for scripts (`SessionManager.session_summaries`): `--live` keeps the sessions a worker runs now (the owner lock, not the saved status; a session open in a plain REPL is left out), `--cwd PATH` those in PATH or below, and test runs are left out. `--live` shows 10 unless `--limit` says otherwise; filters apply before the limit. `--format json` prints `[{id, short_id, desc, cwd, project, updated_at, live, busy, owner, recap, parent_id, archived, scratch, ctx_pct}]` (`owner`: `"worker"`, `"tui"` for a plain REPL, which takes no notes or messages, or null; `recap`: the first sentence of the session's recap, or null), `--format tsv` one `id<TAB>desc` line per session, where `desc` is `<folder> · <last prompt>` cut to 60 characters (the text form of `--live`/`--cwd` shows `<folder> · <recap>` when there is one; tsv and json keep `desc`).
|
|
59
|
+
`--live`, `--cwd` and `--format` make `list` a picker for scripts (`SessionManager.session_summaries`): `--live` keeps the sessions a worker runs now (the owner lock, not the saved status; a session open in a plain REPL is left out), `--cwd PATH` those in PATH or below, and test runs are left out unless the list is itself run as one (`SAMAGOTCHI_ENV=test`, `RACK_ENV=test` or `CI` set: then they show, marked `[test]`; `chi note --all` likewise). `--live` shows 10 unless `--limit` says otherwise; filters apply before the limit. `--format json` prints `[{id, short_id, desc, cwd, project, updated_at, live, busy, owner, recap, parent_id, archived, scratch, ctx_pct}]` (`owner`: `"worker"`, `"tui"` for a plain REPL, which takes no notes or messages, or null; `recap`: the first sentence of the session's recap, or null), `--format tsv` one `id<TAB>desc` line per session, where `desc` is `<folder> · <last prompt>` cut to 60 characters (the text form of `--live`/`--cwd` shows `<folder> · <recap>` when there is one; tsv and json keep `desc`).
|
|
60
60
|
|
|
61
61
|
**Ordering:**
|
|
62
62
|
|
|
63
63
|
- `Session.list` / `SessionManager.list_sessions` / `GET /api/sessions?sort=&order=&limit=&offset=` default to `updated_at desc` (newest activity first). Also supports `created_at`, `asc`. `X-Total-Count` header when paginated.
|
|
64
|
-
- Web UI (`chi web`): the page's scope is in its URL. Started in a git repo, `chi web` opens `/?dir=<that folder>`: that project's sessions, and new chats start in that folder; the header chip says `<project> · all`, and `all` opens the same place without `?dir` (every session; a new chat there starts in the server's own folder, shown on the start page, and cards name their folder; `← <project>` goes back to the project view it came from, or to the server's own project). One server serves every project: a second `chi web` (from another repo) finds it through `GET /api/info` and prints (with `--open`, opens) its page for its own folder instead of starting another. The 3 latest sessions sit above the chat; "All sessions" (or `/`) opens every session at `#/sessions`, with a search over preview, id and status (Esc or Back returns). The open session is in the URL (`#/s/<id>`), so a reload or a copied link opens it again; the chi logo top left goes back to the empty start for a new chat. The message box grows with its text; drag its top edge to keep it taller (double-click resets). The info bar copies `chi --attach <id>` for a terminal. The start page's model picker (bottom left of the composer,
|
|
64
|
+
- Web UI (`chi web`): the page's scope is in its URL. Started in a git repo, `chi web` opens `/?dir=<that folder>`: that project's sessions, and new chats start in that folder; the header chip says `<project> · all`, and `all` opens the same place without `?dir` (every session; a new chat there starts in the server's own folder, shown on the start page, and cards name their folder; `← <project>` goes back to the project view it came from, or to the server's own project). One server serves every project: a second `chi web` (from another repo) finds it through `GET /api/info` and prints (with `--open`, opens) its page for its own folder instead of starting another. The 3 latest sessions sit above the chat; "All sessions" (or `/`) opens every session at `#/sessions`, with a search over preview, id and status (Esc or Back returns). The open session is in the URL (`#/s/<id>`), so a reload or a copied link opens it again; the chi logo top left goes back to the empty start for a new chat. The message box grows with its text; drag its top edge to keep it taller (double-click resets). The info bar copies `chi --attach <id>` for a terminal. The start page's model picker (bottom left of the composer, the chosen name `▾`) chooses the model a new chat starts on; it opens a search list over the conversation (↓ or a click; downward when the start page leaves no room above): an empty search shows the last 5 picks (Recent) and then each host's model ids A–Z under the host, the default host first; typed words must each match the host or the id (a substring, else for 3+ characters the letters in order, so `deepseek4.1 fla` finds `openrouter · deepseek/deepseek-v4.1-flash`), ⏎ picks, Esc/Tab close, a pick puts the focus in the message box: `GET /api/models` lists the hosts' models as chi spells them (`{default, models: [{name, host, id}], warning?}`; bare for the default host, `host:model` for the others, from the same cached lists as `/models`, a bounded wait, a host that is down noted in `warning`), and `POST /api/sessions` takes `model` (blank means the default). The start page's first message goes as later ones do: `POST /api/sessions` with `idle: true` and `preview` (the message, which names the session until its turn is saved), then `POST /api/sessions/:id/turn`, so a failed first turn puts its prompt back in the composer too; a first command line (`/model …`) still starts the session as its `prompt`. The browser remembers the last choice; a running session's model is in the info bar and changes only with `/model`.
|
|
65
65
|
- Web notifications: when a session needs you while the tab is hidden or behind another window (a question or a guardrail approval, a plugin card with buttons during a turn such as check-in's, a failed turn, a turn done after 10 s or more; a delegated session only for its questions, a canceled turn never), the page title counts it, `(N) Chi`, until you come back to the tab. The bell at the right of the top bar also turns on OS notifications ("needs an answer", "needs approval", "needs you", "turn failed", "done in 42 s", under the session's first message; a click opens the session): its first click asks the browser for the permission, and the choice is kept per browser. Any session of the page's scope counts, not only the open one. What was already so when the page loaded never counts, and several chi tabs show one notification per event. A chi tab in front tells the others (a `BroadcastChannel`), so while you look at one, the ones behind neither notify nor count what it shows; coming back to a tab clears those events in the other tabs' titles too. The hub's `session` frames carry what this needs: `pending_question` (`{id, kind}`, kind `question` or `approval`), `pending_card` (`{id, bundle}`: the running turn's card with actions, from `pending_card.json` that the worker keeps in the session's folder while one is open; a card without actions, such as loop-guard's, never counts) and `last_turn` (`{outcome, ended_at, seconds, origin}`, saved in the session's JSON as the engine ends each turn).
|
|
66
66
|
- The web frontend is a zero-build ES-module stack in `lib/samagotchi/web/public/`: `data.js` (retrieval, typed SSE `openStream` for a session and `openEvents` for the session list), `sessions_list.js` (the list as a pure reducer over `GET /api/events`: a snapshot replaces it, an upsert keeps a known card in place, a removal drops it), `notify.js` (which of those events needs the user, for the notifications), `app.js` (presentation/state; a session with no live stream gets one when its `session` event says `bridge_up`, after one re-read: `event_seq` starts over in each worker, so a dropped stream is never resumed with its old cursor), `format.js` (pure formatters such as `previewOf`). Unit-tested via `npm test` (`node --test spec/web/public/*.test.js`).
|
|
67
67
|
- Selecting a session in the web UI is read-only: `GET /api/sessions/:id` never spawns a worker (it reads a live worker's snapshot when one runs, else the session file). A prompt (`POST /turn`) or a command (`POST /command`) wakes the worker, and `/stream` briefly waits for a freshly-spawned bridge before answering. A caught-up SSE reconnect holds the stream open; `reset` markers are only sent for reconnects behind the ring window, or with a cursor from another worker (event ids are `<event_seq>-<epoch>`, one epoch per worker).
|
|
@@ -146,7 +146,7 @@ pbpaste | chi send 3fa2 # the clipboard is the messa
|
|
|
146
146
|
- It goes through the same path as the web composer (`SessionManager.deliver_turn`): the worker's Bridge when it is up, so every attached UI shows it as your message (client id `cli:send`, shown like any user message); the input file when the worker is on its way out. During a running turn it is merged into that turn at the next step, like a message typed then.
|
|
147
147
|
- A session with no worker gets one started (like `--attach` or the web composer). A session open in a plain REPL (`--no-shared`) refuses it. Only sessions on this machine: Bridges listen on 127.0.0.1.
|
|
148
148
|
- Fire and forget: it returns once the message is queued and never prints the answer; that shows in whatever is attached. A guardrail "ask" waits for a UI to answer it, so with nothing attached the turn stalls there until one attaches (`chi --attach ID`).
|
|
149
|
-
- `--image PATH` (repeatable, up to 20) sends images with the message, as the web composer's chips do. Each file is read once before anything is sent (bmp, tiff and heic converted to png, large ones downscaled, as for `@path`); a missing file or one that isn't an image is a usage error (exit 2) and nothing is sent. Each session gets its own copy in `<session>/images/`. Text is still required (`-m` or stdin; context alone counts), also with `--wait`. The session's model must see images:
|
|
149
|
+
- `--image PATH` (repeatable, up to 20) sends images with the message, as the web composer's chips do. Each file is read once before anything is sent (bmp, tiff and heic converted to png, large ones downscaled, as for `@path`); a missing file or one that isn't an image is a usage error (exit 2) and nothing is sent. Each session gets its own copy in `<session>/images/`. Text is still required (`-m` or stdin; context alone counts), also with `--wait`. The session's model must see images: one known not to (the host's model list says text-only, `vision: false` in `models:` or `hosts:`, a native llama.cpp without a vision model) is refused before anything is sent, with the reason (`<id> refused: gemma can't take images (…); send text only or switch the model (/model)`); the other sessions of the same send still get it, and the exit is 1. With `--new` nothing is started. When chi can't tell (a native host whose `/props` doesn't answer, a list that doesn't say), the message is sent as before and a text-only model fails the turn in the session. A running turn doesn't take images mid-turn: the message runs as the next turn, and the line says `(runs after the current turn)`. With `--new` the session starts idle with the message as its preview, then the message goes in as its first turn once its worker is up (`<id> started with 1 image`); if the worker doesn't come up in 5 s the session is kept, with its id on the `failed:` line.
|
|
150
150
|
- One line per session: `sent`, `sent with 2 images`, `sent (the running turn picks it up)`, `sent (started its worker)`, `refused: …` or `failed: …`. Exit 0 when all were sent, 1 when any was refused, failed or not found, 2 for a usage error. There is no `--all`.
|
|
151
151
|
|
|
152
152
|
The Automator action above works for messages too: swap its last line for `pbpaste | "$chi" send -m "what do you make of this?" $(print -r -- "$picked" | cut -f1)`.
|
|
@@ -243,14 +243,13 @@ module Samagotchi
|
|
|
243
243
|
|
|
244
244
|
section = data["default"]
|
|
245
245
|
value = section.is_a?(Hash) ? section["model"] : nil
|
|
246
|
-
value ||= data[ConfigFile::DEFAULT_MODEL_KEY]
|
|
247
246
|
value.to_s.strip.empty? ? nil : value.to_s.strip
|
|
248
247
|
end
|
|
249
248
|
|
|
250
249
|
# Whether bare model names go somewhere today: a default.model or a
|
|
251
250
|
# server: section. Then a new hosts: block keeps that route as `default`.
|
|
252
251
|
def routed?
|
|
253
|
-
configured_default_model || data.key?("server")
|
|
252
|
+
configured_default_model || data.key?("server")
|
|
254
253
|
end
|
|
255
254
|
|
|
256
255
|
# The `default` hosts entry chi derives from server.* when the file has
|
|
@@ -187,9 +187,6 @@ module Samagotchi
|
|
|
187
187
|
"Cache-Control: no-cache\r\n" \
|
|
188
188
|
"Connection: keep-alive\r\n" \
|
|
189
189
|
"X-Accel-Buffering: no\r\n" \
|
|
190
|
-
"Access-Control-Allow-Origin: *\r\n" \
|
|
191
|
-
"Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n" \
|
|
192
|
-
"Access-Control-Allow-Headers: Content-Type, Last-Event-ID\r\n" \
|
|
193
190
|
"\r\n"
|
|
194
191
|
)
|
|
195
192
|
io.flush
|
|
@@ -146,6 +146,7 @@ module Samagotchi
|
|
|
146
146
|
part = { kind: "tool", iteration: event[:iteration], call_index: event[:call_index],
|
|
147
147
|
tool: event[:tool], params: event[:params], status: "running" }
|
|
148
148
|
part[:label] = event[:label] if event[:label]
|
|
149
|
+
part[:title] = event[:title] if event[:title]
|
|
149
150
|
parts << part
|
|
150
151
|
when :tool_call_completed
|
|
151
152
|
tool = parts.reverse_each.find do |part|
|
data/lib/samagotchi/bridge.rb
CHANGED
|
@@ -305,8 +305,8 @@ module Samagotchi
|
|
|
305
305
|
if request[:too_large]
|
|
306
306
|
write_json(io, 413, { "Connection" => "close" }, { error: "too_large", detail: "request body over #{MAX_BODY_BYTES} bytes" })
|
|
307
307
|
break
|
|
308
|
-
elsif
|
|
309
|
-
write_json(io,
|
|
308
|
+
elsif browser_request?(headers)
|
|
309
|
+
write_json(io, 403, nil, { error: "cross_origin", detail: "the bridge answers chi's own clients only" })
|
|
310
310
|
elsif (m = stream_match(request[:path])) && method == "GET"
|
|
311
311
|
cursor = reconnect_cursor(headers, request[:query])
|
|
312
312
|
Log.debug(:bridge, "stream", method: method, path: request[:path], client_id: stream_client_id(request[:query]))
|
|
@@ -344,7 +344,7 @@ module Samagotchi
|
|
|
344
344
|
payload, status, body = handle_snapshot(m[1])
|
|
345
345
|
write_json(io, status, payload, body)
|
|
346
346
|
else
|
|
347
|
-
write_json(io, 404, { "Allow" => "GET, POST
|
|
347
|
+
write_json(io, 404, { "Allow" => "GET, POST" },
|
|
348
348
|
{ error: "not_found", path: request[:path] })
|
|
349
349
|
end
|
|
350
350
|
|
|
@@ -414,12 +414,18 @@ module Samagotchi
|
|
|
414
414
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
415
415
|
end
|
|
416
416
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
417
|
+
LOOPBACK_NAMES = %w[127.0.0.1 [::1] localhost].freeze
|
|
418
|
+
|
|
419
|
+
# Only chi's Ruby clients talk to the Bridge, and they send no Origin and
|
|
420
|
+
# no Sec-Fetch-Site. A browser page always sends one of them (another
|
|
421
|
+
# website's text/plain POST needs no preflight), and a DNS-rebound page
|
|
422
|
+
# also has a foreign Host. Headers are the raw ones, lowercased.
|
|
423
|
+
def browser_request?(headers)
|
|
424
|
+
return true if headers.key?("origin")
|
|
425
|
+
return true if headers.key?("sec-fetch-site") && headers["sec-fetch-site"].downcase != "none"
|
|
426
|
+
|
|
427
|
+
host = headers["host"].to_s
|
|
428
|
+
!host.empty? && !LOOPBACK_NAMES.include?(host.downcase.sub(/:\d*\z/, ""))
|
|
423
429
|
end
|
|
424
430
|
|
|
425
431
|
def stream_match(path)
|
|
@@ -893,8 +899,7 @@ module Samagotchi
|
|
|
893
899
|
"Content-Type" => "application/json",
|
|
894
900
|
"Content-Length" => data.bytesize.to_s,
|
|
895
901
|
"Connection" => "close",
|
|
896
|
-
"Cache-Control" => "no-store"
|
|
897
|
-
"Access-Control-Allow-Origin" => "*"
|
|
902
|
+
"Cache-Control" => "no-store"
|
|
898
903
|
}.merge(extra_headers)
|
|
899
904
|
|
|
900
905
|
io.write("HTTP/1.1 #{status} #{reason}\r\n")
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: skills
|
|
3
|
+
version: 0.1.0
|
|
4
|
+
scope: system
|
|
5
|
+
description: "Skills (skill_<name> memories, the steps of a task done together): /skill save, list, show and diff; older versions kept with a short diff line on every update; a nudge when a followed skill's step fails"
|
|
6
|
+
trust_level: reviewed
|
|
7
|
+
plugin:
|
|
8
|
+
file: plugin.rb
|
|
9
|
+
sha256: sha256:76758d41e75db107e0a6f7ce35b9eac4944052746af16aeba30f38210cc436dc
|
|
10
|
+
requires_chi: ">= 0.4.0"
|