samagotchi 0.49.0 → 0.51.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 +84 -1
- data/bin/chi +2 -2
- data/docs/cli.md +33 -8
- data/docs/configuration.md +11 -9
- data/docs/hooks.md +12 -3
- data/docs/internals/engine.md +4 -2
- data/docs/sessions.md +8 -4
- data/docs/testing.md +2 -1
- data/lib/samagotchi/bridge/turn_accumulator.rb +3 -1
- data/lib/samagotchi/bridge.rb +24 -9
- data/lib/samagotchi/bridge_client.rb +3 -1
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/plugin.rb +13 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/delivery.rb +33 -0
- data/lib/samagotchi/engine.rb +1 -1
- data/lib/samagotchi/hooks/stream_watch.rb +9 -2
- data/lib/samagotchi/host_registry.rb +17 -4
- data/lib/samagotchi/kernel_loop.rb +28 -2
- data/lib/samagotchi/llm/native_tool_normalizer.rb +2 -2
- data/lib/samagotchi/send_command.rb +124 -10
- data/lib/samagotchi/session_commands.rb +10 -2
- data/lib/samagotchi/session_inbox.rb +27 -7
- data/lib/samagotchi/session_manager.rb +13 -3
- data/lib/samagotchi/session_metrics.rb +32 -6
- data/lib/samagotchi/sessions_command.rb +84 -7
- data/lib/samagotchi/steer_cut.rb +12 -8
- data/lib/samagotchi/terminal_ui/attached_loop.rb +50 -3
- data/lib/samagotchi/terminal_ui/event_renderer.rb +4 -2
- data/lib/samagotchi/terminal_ui/formatting.rb +68 -6
- data/lib/samagotchi/terminal_ui.rb +48 -5
- data/lib/samagotchi/tool_activity.rb +3 -0
- data/lib/samagotchi/tool_runner.rb +2 -0
- data/lib/samagotchi/tools/builtin_calls.rb +33 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +6 -2
- data/lib/samagotchi/web/message_parts.rb +3 -0
- data/lib/samagotchi/web/public/activity.js +6 -0
- data/lib/samagotchi/web/public/app.js +71 -29
- data/lib/samagotchi/web/public/index.html +17 -15
- data/lib/samagotchi/web/public/live_timing.js +32 -5
- data/lib/samagotchi/web/public/session_title.js +26 -0
- data/lib/samagotchi/web/public/stage_model.js +15 -0
- data/lib/samagotchi/web/public/stage_view.js +10 -4
- data/lib/samagotchi/web/public/strip.js +4 -6
- data/lib/samagotchi/web/public/timing.js +11 -0
- data/lib/samagotchi/web/public/turn_events.js +14 -2
- data/lib/samagotchi/web/public/turn_html.js +24 -1
- data/lib/samagotchi/web/public/turn_view.js +9 -3
- data/lib/samagotchi/web/session_hub.rb +11 -3
- data/lib/samagotchi/worker.rb +1 -1
- data/lib/samagotchi/worker_inbound.rb +8 -5
- data/lib/samagotchi.rb +1 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f93eddaef9e07210789382afa552868d94a39b0bca798bb7c0c5a8d4e23cc693
|
|
4
|
+
data.tar.gz: 61f1005b2dd4efcdfba5379622193e79c7bfd305a4eac651827830a54399d1c9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d6ea69e71620afeca9b1813fef569c9f39742d1dbf0d28ca9aa2675c0445ef5b397bdd44fd3b6d544dc3ba07a131771136e7d2163669d13925a0d952b69df175
|
|
7
|
+
data.tar.gz: 9abd2290def4fd2bc5b12e9ea0c5fe722c2b4a8ead03044a8944108a43e3105325fc7bf854e5dd5bba3641fb1e0c0c2047835aa4e5ddeef91013ccf115244a0c
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,87 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.51.0] - 2026-10-10
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `/cut TEXT` in a terminal (plain REPL and attached) and `chi send --cut` ask the running turn for a cut: a
|
|
16
|
+
generation that has streamed only thinking for `steer.cut_after` seconds is cut (now, or once its thinking passes
|
|
17
|
+
that) and the step starts again with the message. The terminal's dim line and `chi send`'s line say what happened
|
|
18
|
+
(`cut in now`, `cuts in once the thinking passes 20 s`, `cutting is off; goes in at the next step`); `--format json`
|
|
19
|
+
adds `delivery` and `cut`. The Bridge's 202 for a turn names its `delivery` and, for a cut, the `cut` outcome.
|
|
20
|
+
- `/queue TEXT` in the plain REPL runs TEXT as a turn of its own after the running one.
|
|
21
|
+
- A tool call named `bash` (any case) with `execute`'s arguments runs as `execute`; the logs keep the model's
|
|
22
|
+
own spelling, and the tool row says "(called as bash)" (TUI and web, live and after a reload). `/stats` and
|
|
23
|
+
`chi sessions stats` count such calls by that spelling (`called as: bash=N`, `tool_calls_aliased` in JSON).
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- A message sent during a running turn (typed in a terminal, from the web, `chi send`, a parent agent) goes in at the
|
|
28
|
+
turn's next step and no longer cuts the model's thinking; only a message sent as a cut does. A line typed mid-turn
|
|
29
|
+
says `(goes in at the next step)`, and `chi send` says `sent (goes in at the running turn's next step)`.
|
|
30
|
+
- A call for a tool that doesn't exist no longer gets its own wrong name quoted back (seeing it again made the
|
|
31
|
+
model call it again): shell-like names (`bash`, `shell`, …) get `Error: no such tool. Shell commands run with
|
|
32
|
+
execute (same arguments).` when `execute` is offered this turn, every other name the available-tools list; the
|
|
33
|
+
UI's tool row and the logs still show what the model called.
|
|
34
|
+
- Web: the open session's title shows once: in its strip card, or in the top bar when the strip is hidden or the
|
|
35
|
+
session has no card there (an older session, a folded delegate). The composer footer's first-message line and the
|
|
36
|
+
banner that appeared at 20 % context are gone; the memory names stay in the footer's `mem N` chip and tooltip.
|
|
37
|
+
- Web: the browser tab names the open session (`<first message> · Chi · <project>`), so several chi tabs read apart.
|
|
38
|
+
- Web: the hidden strip's "▾ N sessions" pill counts sessions, the number the "All sessions →" tile shows (it
|
|
39
|
+
counted cards, so a folded family made the two disagree).
|
|
40
|
+
- Web: the composer's "Cancel" reads "Stop turn" and the footer's "stop" reads "stop session", each saying what it
|
|
41
|
+
ends.
|
|
42
|
+
- Web: a toast (the update notice, an archive's Undo) sits above the open session's composer card instead of over
|
|
43
|
+
its footer, so it never covers the footer's controls (a worker badge's Restart); it follows the card as it grows.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- A bare model id, or an alias with a bare target (`splash: incoai/Qwen3.8-27B-Splash`), in a process that hasn't
|
|
48
|
+
listed the hosts yet (`chi --model splash`, `chi send --new --model splash`, a delegate's worker) goes to the host
|
|
49
|
+
whose saved list (`model_lists.json`, at most a week old) has it, instead of always the default host.
|
|
50
|
+
- `chi sessions stats ID --format json` for a session no worker runs lists its newest saved turn (and that turn's
|
|
51
|
+
tool calls) in `turn_records` / `tool_records`, as a live worker's snapshot does, instead of empty lists.
|
|
52
|
+
|
|
53
|
+
## [0.50.0] - 2026-10-09
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
|
|
57
|
+
- `chi sessions stats ID [--format text|json]`: a session's cost, tokens and progress without running a model turn:
|
|
58
|
+
a live worker's `GET stats` (waiting up to 3 s), else the snapshot its saved `analytics.json` rebuilds (never
|
|
59
|
+
starting a worker); text prints the `/stats` report headed by `<id8> <status> <model>`, json one
|
|
60
|
+
`{session_id, status, live, metrics}` object.
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
|
|
64
|
+
- A turn record in `analytics.json` says whether the turn was a step-limit Continue (`continue: true`, else `false`),
|
|
65
|
+
and the model-notes report counts Continues from that mark when the file has it, instead of the older guess (its
|
|
66
|
+
turn records with no prompt of their own), which also counted reminder turns and prompts `!rollback` erased.
|
|
67
|
+
|
|
68
|
+
### Fixed
|
|
69
|
+
|
|
70
|
+
- TUI: a tool row whose output the runner cut says so with a dim `[cut]`, as the web's row already marks it.
|
|
71
|
+
- `/models` is listed as an anytime command (`mid-turn too`) in `/help` and the web's command list, not the
|
|
72
|
+
per-line `depends` label it got while it always runs mid-turn.
|
|
73
|
+
- Web: a host added to `hosts:` (or a `hosts.<name>.models` context window changed) in config.yml while `chi web`
|
|
74
|
+
runs now reaches the session cards' ctx % on their next refresh, not only after a restart; the hub's host registry
|
|
75
|
+
is rebuilt when `hosts:` changes, as the model picker's already was.
|
|
76
|
+
- A host listing saved to the state dir when the registry was built with its own env (the spawn-time model check, the
|
|
77
|
+
self report) writes under that env's state dir, not `ENV`'s.
|
|
78
|
+
- loop-guard: a dropped stream's step asked again (it streams from the start) no longer reads the dropped attempt's
|
|
79
|
+
thinking as part of the new one — the model re-thinking the same opening after a restart could be cut or stopped
|
|
80
|
+
as "thinking repeats itself". The first `:generation_progress` fire after such a restart carries `restarted: true`,
|
|
81
|
+
and the loop-guard thinking watch starts over on it (loop-guard 0.3.7).
|
|
82
|
+
- Web: the live timing line counts up from the first second (tenths of a second while under 10 s, ticking every
|
|
83
|
+
200 ms there) instead of reading "0ms" for the turn's first second.
|
|
84
|
+
- Web: the stage trail's items hover their whole text — a flash (a hook notice) its whole line, a call its full
|
|
85
|
+
command when it has one, else its whole truncated text.
|
|
86
|
+
- Web: a `!cmd`'s output shows once, as a command bubble: the history renders the saved `!(<command>)` message as
|
|
87
|
+
one (not a user bubble), and a live `command_ran` no longer adds a second bubble after the redraw.
|
|
88
|
+
- `chi send` refuses a UI-only command (`/stats`, `/exit`, `/recap`, …) instead of passing it to the worker, which
|
|
89
|
+
doesn't run it, so the line reached the model as a prompt and cost a whole turn; the refusal names
|
|
90
|
+
`chi sessions stats` for `/stats` and sends nothing (exit 1, also with `--new` before a session is created).
|
|
91
|
+
|
|
11
92
|
## [0.49.0] - 2026-10-09
|
|
12
93
|
|
|
13
94
|
### Added
|
|
@@ -2262,7 +2343,9 @@ and long-lived sessions.
|
|
|
2262
2343
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
2263
2344
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
2264
2345
|
|
|
2265
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
2346
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.51.0...HEAD
|
|
2347
|
+
[0.51.0]: https://github.com/dm1try/samagotchi/compare/v0.50.0...v0.51.0
|
|
2348
|
+
[0.50.0]: https://github.com/dm1try/samagotchi/compare/v0.49.0...v0.50.0
|
|
2266
2349
|
[0.49.0]: https://github.com/dm1try/samagotchi/compare/v0.48.0...v0.49.0
|
|
2267
2350
|
[0.48.0]: https://github.com/dm1try/samagotchi/compare/v0.47.0...v0.48.0
|
|
2268
2351
|
[0.47.0]: https://github.com/dm1try/samagotchi/compare/v0.46.1...v0.47.0
|
data/bin/chi
CHANGED
|
@@ -36,7 +36,7 @@ end
|
|
|
36
36
|
|
|
37
37
|
# Subcommands with their own flags, dispatched before OptionParser.
|
|
38
38
|
SUBCOMMANDS = {
|
|
39
|
-
# list, stop, archive/unarchive, delete, prune, clean (--dry-run, --days, …)
|
|
39
|
+
# list, stop, archive/unarchive, delete, prune, clean, stats (--dry-run, --days, …)
|
|
40
40
|
"sessions" => ["samagotchi/sessions_command", "SessionsCommand"],
|
|
41
41
|
# a context note for sessions (-m, --source, --all)
|
|
42
42
|
"note" => ["samagotchi/note_command", "NoteCommand"],
|
|
@@ -95,7 +95,7 @@ parser = OptionParser.new do |opts|
|
|
|
95
95
|
chi bootstrap [HOST[:PORT]|URL] first setup: find the model server, write config.yml
|
|
96
96
|
chi scratch [options] a one-time session in this terminal: nothing is kept
|
|
97
97
|
chi web [--port PORT] [--open] [--scope=all] the web UI
|
|
98
|
-
chi sessions <list|stop|archive|unarchive|delete|prune|clean>
|
|
98
|
+
chi sessions <list|stop|archive|unarchive|delete|prune|clean|stats>
|
|
99
99
|
chi note [--source NAME] [-m TEXT] ID...|--all background context for sessions
|
|
100
100
|
chi broadcast [-m TEXT] [--all] [--dry-run] | log | deliver a note for every session it may concern
|
|
101
101
|
chi context <add|push|ls|show|refresh|rm|mute|unmute> live external text attached to sessions
|
data/docs/cli.md
CHANGED
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
- `chi web --web-host lan` — the Web UI on your home network too, for your phone: a link with an access token and its QR code (see [chi web on your phone](#chi-web-on-your-phone)); `chi web --new-token` replaces the token
|
|
16
16
|
- `chi web --web-markdown` — opt in to sanitized Markdown rendering for completed assistant messages
|
|
17
17
|
- `chi web --web-view turn` — draw turns with the turn view (each turn as one block of steps, the running one at the bottom of the history) instead of the default stage view (the running turn pinned above the composer); `?view=stage|turn` on the page URL overrides it (see [Web views](#web-views))
|
|
18
|
-
- `chi sessions list|stop|restart|archive|unarchive|delete|prune|clean` — manage persisted sessions; `list` shows this git project's, `list --scope=all` every one, a delegated session with `↳ <parent>`, `list --archived` the archived ones too (see [Sessions](sessions.md)). A usage error (an unknown subcommand or flag, a flag missing its value, `stop`, `restart` or `delete` with no ids, a bad `list --format` or `--scope`) exits 2; an unknown or refused session exits 1
|
|
18
|
+
- `chi sessions list|stop|restart|archive|unarchive|delete|prune|clean|stats` — manage persisted sessions; `list` shows this git project's, `list --scope=all` every one, a delegated session with `↳ <parent>`, `list --archived` the archived ones too (see [Sessions](sessions.md)). `stats ID` prints a session's cost, tokens and progress (a live worker's `GET stats`, else its saved `analytics.json`; `--format json` one object) without running a model turn. A usage error (an unknown subcommand or flag, a flag missing its value, `stop`, `restart` or `delete` with no ids, a bad `list --format` or `--scope`) exits 2; an unknown or refused session exits 1
|
|
19
19
|
- `chi note [--source NAME] [-m TEXT] (ID|PREFIX)... | --all` — add a context note (TEXT or stdin) to sessions: background the model sees on its next turn; it starts no turn (see [Sessions: Context notes](sessions.md#context-notes))
|
|
20
20
|
- `chi context <add|push|ls|show|refresh|rm|mute|unmute> …` — attach live external text to sessions (a command chi runs every so often, or text pushed in); the model gets a short note when it changes and reads it with `context_read`; `chi context add <URL>` attaches a GitHub PR with the github-pr bundle (see [Attached context](context.md))
|
|
21
|
-
- `chi send [-m TEXT] [--image PATH]... (ID|PREFIX)...` — send a message to sessions as if typed there: a turn starts (or a running one
|
|
21
|
+
- `chi send [-m TEXT] [--image PATH]... [--cut] (ID|PREFIX)...` — send a message to sessions as if typed there: a turn starts (or a running one takes it at its next step; `--cut` asks it to cut long thinking for it); piped stdin goes above `-m` as quoted context, and `--image` attaches images (see [Sessions: Sending a message](sessions.md#sending-a-message)); `--new` starts a session with it instead (`--new --continues ID|last:ID` the next link of that session's chain, see [Session chains](sessions.md#session-chains)), and `--wait` prints the answer (`--wait ID` with no message waits for the next reply without sending; `--format json` prints one JSON object instead; exit 3 a question waits, its options on stderr; exit 4 `--timeout` passed with the turn still running; see [Starting a session](sessions.md#starting-a-session))
|
|
22
22
|
- `chi answer ID --question QID (--option N|LABEL)... [--text T] [--timeout S] [--format json]` (or `--dismiss`) — answer the question a session waits on (the one `chi send --wait` exited 3 with), then wait and print what comes next as `chi send --wait` does: the reply (0), the next question (3), still running after `--timeout` (4). `--option` is 1-based or the label, repeated on a multi-select question; `--text` is free text, or a Deny's reason; `--dismiss` leaves it unanswered and the model finishes its reply. A question no longer open (the web answered first) isn't answered again: it waits for that turn's reply. An option the question doesn't offer exits 2; a worker that is gone exits 1 (`send the task again: chi send --wait -m "…" ID`). An approval can be denied, not allowed, unless `guardrails.parent_approvals: once` (a convention, not a security boundary; see [Guardrails](guardrails.md#approvals-from-a-parent-agent))
|
|
23
23
|
- `chi desktop install|upgrade|uninstall|status` — the macOS "Send to chi" helper: a Service and a ⌃⌥⌘N hotkey that send text or images to a session (live, stopped or new) as a message (⏎, a turn runs) or a context note (⌘⏎) (see [Desktop helper](desktop.md))
|
|
24
24
|
- `chi models [--format text|json] [--timeout S] [TEXT]` — list the models every configured host offers, as the names `--model` takes (see [Listing the models](#listing-the-models))
|
|
@@ -289,11 +289,13 @@ to it (`session.shared`, default `true`). A worker's session can have any number
|
|
|
289
289
|
of UIs at once: the Web UI and attached terminals (`chi`, `--resume`,
|
|
290
290
|
`--attach`). They all see the same turns as they happen, and any of them can send
|
|
291
291
|
a prompt, also while a turn runs (it merges into that turn as steering: the model
|
|
292
|
-
is told it is a steer and who sent it). Steering waits for the model's next step
|
|
293
|
-
|
|
294
|
-
|
|
292
|
+
is told it is a steer and who sent it). Steering waits for the model's next step
|
|
293
|
+
and cuts nothing. `/cut TEXT` in a terminal (or `chi send --cut`) asks for a cut:
|
|
294
|
+
once the model has streamed only thinking for `steer.cut_after` seconds (default
|
|
295
|
+
20), now or while your message waits, that generation is cut and the step starts again with your message, marked
|
|
295
296
|
`↪ cut in for your message` (see
|
|
296
|
-
[configuration.md](configuration.md#llama-network-retry-behavior)). The
|
|
297
|
+
[configuration.md](configuration.md#llama-network-retry-behavior)). The web
|
|
298
|
+
composer doesn't cut yet. The
|
|
297
299
|
first answer to an `ask_user_question` wins; the other UIs close their widget.
|
|
298
300
|
An empty answer dismisses the question in every UI. The model is then told
|
|
299
301
|
not to go ahead with what it asked about, or change anything else, and to wait.
|
|
@@ -392,6 +394,17 @@ status: 0 when all are gone, 1 when any was refused or unknown, 2 on a usage
|
|
|
392
394
|
error. The Web UI deletes too: `delete` in the info bar, or the ✕ on a card in
|
|
393
395
|
All sessions; it asks first and stops a live worker.
|
|
394
396
|
|
|
397
|
+
`chi sessions stats ID [--format text|json]` prints a session's cost, tokens
|
|
398
|
+
and progress without running a model turn — the same text `/stats` prints in
|
|
399
|
+
a terminal, headed by `<id8> <status> <model>`: a live worker answers its
|
|
400
|
+
Bridge's `GET stats`, else the snapshot is rebuilt from the session's saved
|
|
401
|
+
`analytics.json` (never starting a worker; a worker that does not answer in
|
|
402
|
+
time falls back to the disk and the line says so). `--format json` prints one
|
|
403
|
+
object `{session_id, status, live, metrics}`; a session with no
|
|
404
|
+
`analytics.json` yet reports `metrics: null` (`(no metrics yet)` in text) and
|
|
405
|
+
exits 0. An unknown or ambiguous id exits 1 with `chi sessions stop`'s
|
|
406
|
+
wording. See [Sessions](sessions.md#cli).
|
|
407
|
+
|
|
395
408
|
**The plain REPL.** Some launches run the session in this process instead, with
|
|
396
409
|
no worker:
|
|
397
410
|
|
|
@@ -476,11 +489,23 @@ The prompt stays open while a turn runs, in an attached terminal and in the plai
|
|
|
476
489
|
REPL alike:
|
|
477
490
|
|
|
478
491
|
- A line you submit merges into the running turn at its next step (after the
|
|
479
|
-
current tool call or answer)
|
|
480
|
-
|
|
492
|
+
current tool call or answer): `(goes in at the next step)` at once, then
|
|
493
|
+
`(1 message merged into the running turn)` when it does. It never cuts the
|
|
494
|
+
model's thinking. An answer the model finished just before the merge is printed first.
|
|
481
495
|
A line that comes after the turn's last step runs as the next turn, and so
|
|
482
496
|
does one sent after Ctrl-C: it doesn't merge into the turn being cancelled.
|
|
483
497
|
Reminder turns take merged lines too.
|
|
498
|
+
- `/cut TEXT` asks for a cut: a generation that has streamed only thinking for
|
|
499
|
+
`steer.cut_after` seconds is cut and the step starts again with TEXT. The dim
|
|
500
|
+
line says what happened: `(cut in now)`, `(cuts in once the thinking passes
|
|
501
|
+
20 s)` (it waits for the thinking; the step may end first) or `(cutting is
|
|
502
|
+
off; goes in at the next step)` (`steer.cut_after: 0`, or no generation to
|
|
503
|
+
cut).
|
|
504
|
+
- `/queue TEXT` (plain REPL only) runs TEXT as a turn of its own after this
|
|
505
|
+
one: `(queued: runs after this turn)`. An attached terminal says
|
|
506
|
+
`(/queue isn't available in an attached session yet)` and sends nothing.
|
|
507
|
+
At the open prompt (no turn running) `/cut TEXT` and `/queue TEXT` run TEXT
|
|
508
|
+
as a normal prompt.
|
|
484
509
|
- Commands answer, wait or are refused, by what they do:
|
|
485
510
|
- At once, beside the turn: `/stats`, `/recap`, `/help`, `/context`, a
|
|
486
511
|
plugin's anytime command (`/btw`), and the commands that only show
|
data/docs/configuration.md
CHANGED
|
@@ -594,8 +594,10 @@ to the default host. A host's declared ids (`hosts.<name>.models`, below) count
|
|
|
594
594
|
as listed. Only exact ids count, never a substring: `/model gemma` on
|
|
595
595
|
a box that lists `gemma-4-26b` needs `box:gemma` or the exact id. The lists are
|
|
596
596
|
known only after `/models` ran (nothing is fetched before the first turn), so
|
|
597
|
-
until then an unqualified name
|
|
598
|
-
|
|
597
|
+
until then an unqualified name (or an alias's unqualified target) goes by the
|
|
598
|
+
lists last saved on disk (`model_lists.json`, what `chi models`, `/models` or the
|
|
599
|
+
web last listed, at most a week old) the same way, else to the default host,
|
|
600
|
+
unless a host declares it; use `host:model` to pin one. A **remote** host (an `https` url or a public address; see "Remote or local") keeps its model
|
|
599
601
|
list for 10 minutes (60s for local hosts).
|
|
600
602
|
|
|
601
603
|
**The context window** (what the context status and `ctx=` count against) comes from,
|
|
@@ -763,12 +765,12 @@ no made-up answer is saved.
|
|
|
763
765
|
A generation a plugin cuts while it streams (loop-guard's thinking watch, or any `stop_generation`,
|
|
764
766
|
[hooks.md](hooks.md#watching-the-stream)) uses the same budget: `↻ cut by loop-guard, asking again (1/1)`, and with
|
|
765
767
|
none left the turn ends as cancelled (hook).
|
|
766
|
-
A message for a running turn
|
|
767
|
-
`
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
(`↪ cut in for your message`).
|
|
771
|
-
|
|
768
|
+
A message for a running turn goes in at its next step and cuts nothing. A message sent as a cut (`/cut TEXT` in a
|
|
769
|
+
terminal, `chi send --cut`) cuts the model's generation once it has streamed only thinking for `steer.cut_after`
|
|
770
|
+
seconds (default `20`, env `SAMAGOTCHI_STEER_CUT_AFTER`, `0` = never; counted from its first thinking token, so the
|
|
771
|
+
wait for a busy server doesn't count) and still does, and the model starts the step again with the message
|
|
772
|
+
(`↪ cut in for your message`). One that comes earlier cuts once the thinking passes `steer.cut_after`, unless the
|
|
773
|
+
step ends first and the message goes in there. The web composer and a parent agent never ask for a cut. Such a cut spends no `retry.empty_answer` attempt and sends no
|
|
772
774
|
hidden note; only the cut thinking is lost. A generation that has streamed visible text or a tool call is never cut,
|
|
773
775
|
and a plugin's steer (`ctx.steer`, `ctx.sessions.send`) never cuts.
|
|
774
776
|
|
|
@@ -1144,7 +1146,7 @@ described in their own sections.
|
|
|
1144
1146
|
| `retry.base_delay` | `0.5` | yes | |
|
|
1145
1147
|
| `retry.max_delay` | `8.0` | yes | |
|
|
1146
1148
|
| `retry.empty_answer` | `1` | | Times a turn asks again after an empty answer (at most 3, `0` = off). See "Llama Network Retry Behavior". |
|
|
1147
|
-
| `steer.cut_after` | `20` | | Seconds a generation must have streamed only thinking before a message
|
|
1149
|
+
| `steer.cut_after` | `20` | | Seconds a generation must have streamed only thinking before a message sent as a cut (`/cut TEXT`, `chi send --cut`) cuts it; other messages go in at the next step; `0` = never. See "Llama Network Retry Behavior". |
|
|
1148
1150
|
| `turn.max_iterations` | `100` | | A turn's step limit: model ↔ tool rounds before it stops and asks to continue (an integer ≥ 1). `--no-interrupt` turns get the larger of 1000 and this. See [CLI: Iteration Limit Behavior](cli.md#iteration-limit-behavior). |
|
|
1149
1151
|
| `turn.parent_continue` | `true` | | Whether a parent agent (`chi answer`) may answer Continue to a session's step-limit question; `false`: Stop only. config.yml only: no environment variable. See [Sub-agent](sub-agent.md) and [Guardrails](guardrails.md#approvals-from-a-parent-agent). |
|
|
1150
1152
|
| `update.gem` | `true` | | `false`: `chi update` never installs a newer gem (`--no-gem` for one run). See [CLI: Updating](cli.md#updating). |
|
data/docs/hooks.md
CHANGED
|
@@ -54,7 +54,7 @@ The plugin class must respond to `#call(event)` — duck-typed, no base class re
|
|
|
54
54
|
| `:after_turn` | After a turn completed or was stopped once the model was asked (not after one that failed, was cut short by an interrupt signal (SIGINT), or was stopped before its `:before_turn` hooks) | `{ type: :after_turn, status: "completed" \| "canceled", present: (see [Presenting the answer](#presenting-the-answer-display-only)), messages: [...] (the conversation the turn stored; a cancelled or empty turn ends it with a `kind: turn_note` system message, and a context line is `kind: context`, see [sessions.md](sessions.md#notes-a-turn-leaves-for-the-model)) }` |
|
|
55
55
|
| `:before_generation` | Before each LLM API call (both loops) | `{ type: :before_generation, iteration: N }` |
|
|
56
56
|
| `:after_generation` | After LLM returns (both loops); not after a generation a hook cut (`stop_generation`) | `{ type: :after_generation, iteration: N, response: "...", messages: [...] (the conversation as sent) }` |
|
|
57
|
-
| `:generation_progress` | While the response streams, in batches (see [Watching the stream](#watching-the-stream)) | `{ type: :generation_progress, iteration: N, thinking: "..." (new since the last fire), text: "..." (new visible text), thinking_chars: N, text_chars: N (this generation so far), elapsed_ms: N }` |
|
|
57
|
+
| `:generation_progress` | While the response streams, in batches (see [Watching the stream](#watching-the-stream)) | `{ type: :generation_progress, iteration: N, thinking: "..." (new since the last fire), text: "..." (new visible text), thinking_chars: N, text_chars: N (this generation so far), elapsed_ms: N, restarted: true (only on the first fire after a dropped stream was asked again) }` |
|
|
58
58
|
| `:before_tool_call` | Before tool dispatch (and before `tool_call_started`) | `{ type: :before_tool_call, iteration: N, call: {...}, params: "...", guardrail: Verdict, context: {...}, targets: {...}, blocked: false, block_reason: nil }` |
|
|
59
59
|
| `:after_tool_call` | After tool execution | `{ type: :after_tool_call, iteration: N, tool: "read", output: "..." (the text the model gets: capped at max_tool_output_chars, a cut one ending in a [cut: N of M chars …] line), status: "ok" \| "error" \| "blocked" \| "stopped" }`; `stopped` is a `task_wait` the turn's Stop ended (the task runs on) or whose task was stopped |
|
|
60
60
|
| `:session_end` | After each turn `:after_turn` fires for, after it (turn-level lifecycle) | `{ type: :session_end, session_id: "..." }` |
|
|
@@ -173,6 +173,14 @@ thinking arrives as `text` instead: a chat provider's that puts `<think>`
|
|
|
173
173
|
inside the answer's content. Tool-call bodies are left out of `text`. With streaming off
|
|
174
174
|
(`stream: false` on a chat host) there are no chunks, so no fires.
|
|
175
175
|
|
|
176
|
+
When a stream drops mid-generation, the step is asked again and streams
|
|
177
|
+
from the start (a `:generation_retrying` event with `restarted: true`);
|
|
178
|
+
what the dropped attempt streamed is void. The first `:generation_progress`
|
|
179
|
+
fire after that carries `restarted: true`; later fires, and fires after a
|
|
180
|
+
normal start, carry no `restarted` key. A watcher of the stream starts
|
|
181
|
+
over on it: the thinking a dropped attempt streamed is no part of this
|
|
182
|
+
generation's, and counting it would read a re-thought opening as a repeat.
|
|
183
|
+
|
|
176
184
|
**Keep it fast.** The hook runs on the turn's thread, inside the HTTP read:
|
|
177
185
|
while it runs, tokens wait in the socket (nothing is lost). A hook over
|
|
178
186
|
100 ms logs `stream_hook_slow` (warn, with its label and the ms) once per
|
|
@@ -212,8 +220,9 @@ What a cut is, in detail:
|
|
|
212
220
|
chunk). A client that joins the running turn still sees its thinking in
|
|
213
221
|
the turn so far until the turn ends.
|
|
214
222
|
- A Stop from the user right after a cut is a plain cancel.
|
|
215
|
-
- A cut for a message (a
|
|
216
|
-
a generation that has streamed only thinking for `steer.cut_after`
|
|
223
|
+
- A cut for a message (one sent as a cut, `/cut TEXT` or `chi send --cut`,
|
|
224
|
+
into a generation that has streamed only thinking for `steer.cut_after`
|
|
225
|
+
seconds; other messages go in at the next step and never cut)
|
|
217
226
|
is not a plugin's cut: it sends no hidden note and spends no retry; the
|
|
218
227
|
message goes in and the UIs print `↪ cut in for your message`. The
|
|
219
228
|
stream's `generation_completed` says `stopped_by: "steer"`, and
|
data/docs/internals/engine.md
CHANGED
|
@@ -203,7 +203,7 @@ One iteration with tool calls:
|
|
|
203
203
|
title, view}.
|
|
204
204
|
- Settles an ask verdict after `:tool_call_started`, so a UI shows the tool line and then the approval.
|
|
205
205
|
- Dispatches through `KernelLoop#dispatch_tool_call`, or writes the denial text. Unknown tools are answered in
|
|
206
|
-
`KernelLoop#dispatch` ("Error:
|
|
206
|
+
`KernelLoop#dispatch` ("Error: no such tool ...", never repeating the wrong name).
|
|
207
207
|
- For edit and write: diffs the file before and after, and refreshes a memory's index line
|
|
208
208
|
(`MemoryBundle::IndexSync`).
|
|
209
209
|
- Attaches returned images, at most `MAX_IMAGES_PER_RESULT` (4) per result.
|
|
@@ -262,7 +262,9 @@ end
|
|
|
262
262
|
|
|
263
263
|
### GenerationPhase
|
|
264
264
|
|
|
265
|
-
What the running generation streams, fed by the Engine's stream handler: whether a
|
|
265
|
+
What the running generation streams, fed by the Engine's stream handler: whether a steer sent as a cut (delivery
|
|
266
|
+
`cut`: `/cut TEXT`, `chi send --cut`) may cut it. `SteerCut#cut_for_steer` answers `:now`, `:waits` or `:off`; the
|
|
267
|
+
Bridge acks it as `cut`.
|
|
266
268
|
A generation is cuttable while it has streamed only thinking, is still thinking, and has thought for at least
|
|
267
269
|
`steer.cut_after` seconds (`cuttable?(min_age)`). It has `started!`, `chunk!`, `retrying!` and `finished!`.
|
|
268
270
|
|
data/docs/sessions.md
CHANGED
|
@@ -40,8 +40,11 @@ chi sessions list --archived # archived sessions too, m
|
|
|
40
40
|
chi sessions delete [--force] ID... # for good; --force stops a live worker first
|
|
41
41
|
chi sessions prune [--dry-run] [--days N] [--keep N] [--keep-status running,...] [--test-only]
|
|
42
42
|
chi sessions clean [--dry-run] [--days N] # test sessions: all, or older than N days
|
|
43
|
+
chi sessions stats ID [--format text|json] # cost, tokens and progress, without a model turn
|
|
43
44
|
```
|
|
44
45
|
|
|
46
|
+
`chi sessions stats ID` prints what `/stats` prints in a terminal, headed by `<id8> <status> <model>`: a live worker answers its Bridge's `GET stats` (no model call, and the command never starts a worker); without one, or when the worker does not answer in time, the same snapshot is rebuilt from the session's saved `analytics.json` (and the line says so when a worker was live). `--format json` prints one object `{session_id, status, live, metrics}` with the snapshot as `metrics` (its `turn_records` / `tool_records` are the recent ones only: a live worker's newest finished turn and any not yet saved, the disk snapshot's newest saved turn; `analytics.json` holds them all); a session with no `analytics.json` yet reports `metrics: null` (`(no metrics yet)` in text) and still exits 0. An unknown or ambiguous id exits 1 with the same wording `chi sessions stop` uses.
|
|
47
|
+
|
|
45
48
|
A session whose worker waits for an answer (a question, a hook's question, a guardrail approval, or the step-limit question of a turn that ran out of steps, which waits between turns) reads `waiting` in the status column, and json has `waiting: "question"|"approval"|"hook"|"continue"` and `waiting_id`, the question's id for `chi answer --question` (both null otherwise). A delegate's approval its parent relayed to its own card (see *Delegating*) reads `waiting (in parent ab12)`, json `relayed_to: "ab12…"` (the parent's short id, else null). Only while a worker runs it: a question a dead worker left in the file shows the saved status. Answer it with `chi answer`, the web or `chi --attach ID`; `chi sessions stop ID` frees the worker instead.
|
|
46
49
|
|
|
47
50
|
Examples:
|
|
@@ -146,20 +149,21 @@ cancel during its thinking, it said it had never been asked.
|
|
|
146
149
|
`chi send` is the other half of `chi note`: the text goes in as your message, the same as typing it in the attached terminal or the web composer, so a turn runs.
|
|
147
150
|
|
|
148
151
|
```sh
|
|
149
|
-
chi send [-m TEXT] [--image PATH]... (ID|PREFIX)...
|
|
152
|
+
chi send [-m TEXT] [--image PATH]... [--cut] (ID|PREFIX)...
|
|
150
153
|
chi send -m "is this the same bug?" 3fa2 # a message
|
|
154
|
+
chi send --cut -m "stop, wrong file" 3fa2 # cut the running turn's long thinking
|
|
151
155
|
chi send --image shot.png -m "why is this red?" 3fa2 # with an image
|
|
152
156
|
pbpaste | chi send -m "is this the same bug?" 3fa2 # the clipboard quoted above the message
|
|
153
157
|
pbpaste | chi send 3fa2 # the clipboard is the message
|
|
154
158
|
```
|
|
155
159
|
|
|
156
160
|
- With both stdin and `-m`, stdin is context: each line becomes a `> ` quote (as web annotations quote a selection), then a blank line, then the message. With only one, it goes in as is. A terminal on stdin is ignored. Both empty is a usage error; over 16 KiB is refused, never cut.
|
|
157
|
-
- 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.
|
|
161
|
+
- 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, and cuts nothing. `--cut` asks for a cut, as `/cut TEXT` in a terminal does: a generation that has streamed only thinking for `steer.cut_after` seconds is cut (now, or once its thinking passes that), and the step starts again with the message. To an idle session `--cut` sends normally. `--cut --wait` into a running turn is refused like any `--wait` there (`busy: a turn is running`).
|
|
158
162
|
- 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.
|
|
159
163
|
- 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`).
|
|
160
164
|
- `--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 stops the send (exit 1; with `--wait --format json`, an `error` line) and nothing is sent; more than 20 is a usage error (exit 2). 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.
|
|
161
|
-
- A session command runs as the command, as typed in a terminal: `chi send -m "/model qwen" 3fa2` switches the model (`sent as a session command`), and the model never sees the line. What counts is what the session's worker knows (the built-ins, `!cmd`, its bundles' commands); an unknown `/word` (`/usr/bin/env is missing`) goes in as a message. With `--new` a line starting with `/` or `!` starts the session idle and goes to its worker once it is up, which decides. With `--wait` a command has no reply to wait for: exit 0 at once, `<id> sent as a session command: no reply to wait for …` on stderr (`{"status":"command",…,"detail":…}` with `--format json`); its output shows where the session is open.
|
|
162
|
-
- One line per session: `sent`, `sent as a session command`, `sent with 2 images`, `sent (the running turn
|
|
165
|
+
- A session command runs as the command, as typed in a terminal: `chi send -m "/model qwen" 3fa2` switches the model (`sent as a session command`), and the model never sees the line. What counts is what the session's worker knows (the built-ins, `!cmd`, its bundles' commands); an unknown `/word` (`/usr/bin/env is missing`) goes in as a message. A UI-only command (`/stats`, `/exit`, `/recap`, …) is refused instead: the worker doesn't run it, so the line would reach the model as a prompt (`<id> refused: /stats is a terminal and web command; use chi sessions stats <id8>`; exit 1, and nothing is sent). With `--new` a line starting with `/` or `!` starts the session idle and goes to its worker once it is up, which decides. With `--wait` a command has no reply to wait for: exit 0 at once, `<id> sent as a session command: no reply to wait for …` on stderr (`{"status":"command",…,"detail":…}` with `--format json`); its output shows where the session is open.
|
|
166
|
+
- One line per session: `sent`, `sent as a session command`, `sent with 2 images`, `sent (goes in at the running turn's next step)`, with `--cut` `sent (cut in now)`, `sent (cuts in once the thinking passes 20 s)` or `sent (cutting is off; goes in at the next step)`, `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`.
|
|
163
167
|
|
|
164
168
|
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)`.
|
|
165
169
|
|
data/docs/testing.md
CHANGED
|
@@ -80,7 +80,8 @@ sessions by model and by the model notes their prompt carried (the session file'
|
|
|
80
80
|
prints the numbers a model note is meant to move: the place of the first edit (an `edit` or `write` call) and of the
|
|
81
81
|
first commit (an `execute` call that runs `git [options] commit`) among the tool calls, commits per 100 steps (model
|
|
82
82
|
requests), `execute` calls with a bare `&`, the longest run of calls with no edit, and what the session needed from
|
|
83
|
-
outside: Continues (the turns in its `analytics.json`
|
|
83
|
+
outside: Continues (the turns in its `analytics.json` marked `continue: true` — the mark a file written after
|
|
84
|
+
2026-10-09 carries; an older file has none, and its turns with no prompt of their own are counted instead), steers (lines that reached a
|
|
84
85
|
running turn from the user, `chi send` or a parent, and a Continue's text), follow-up prompts and plugin nudges. A
|
|
85
86
|
group has the medians of the places and runs, commits per 100 steps pooled and the counts summed. `--help` defines
|
|
86
87
|
each column; `--model GLOB`, `--since DATE` and `--min-steps N` filter, `--json` gives JSON. It reads only and prints
|
|
@@ -158,7 +158,8 @@ module Samagotchi
|
|
|
158
158
|
def self.tool_replay_events(part)
|
|
159
159
|
call = { iteration: part[:iteration], call_index: part[:call_index], tool: part[:tool] }
|
|
160
160
|
call[:label] = part[:label] if part[:label]
|
|
161
|
-
|
|
161
|
+
# Its title and, for an alias's call, the name the model used.
|
|
162
|
+
title = part.slice(:title, :called_as).compact
|
|
162
163
|
view = part[:view] ? { view: part[:view] } : {}
|
|
163
164
|
events = [{ type: :tool_call_started, **call, params: part[:params], **title, **view }]
|
|
164
165
|
if part[:status] == "running"
|
|
@@ -274,6 +275,7 @@ module Samagotchi
|
|
|
274
275
|
tool: event[:tool], params: event[:params], status: "running" }
|
|
275
276
|
part[:label] = event[:label] if event[:label]
|
|
276
277
|
part[:title] = event[:title] if event[:title]
|
|
278
|
+
part[:called_as] = event[:called_as] if event[:called_as]
|
|
277
279
|
part[:view] = event[:view] if event[:view]
|
|
278
280
|
parts << part
|
|
279
281
|
@tool_started_at[[event[:iteration], event[:call_index]]] = @clock.call
|
data/lib/samagotchi/bridge.rb
CHANGED
|
@@ -7,6 +7,7 @@ require "fileutils"
|
|
|
7
7
|
require "securerandom"
|
|
8
8
|
require "time"
|
|
9
9
|
require_relative "atomic_file"
|
|
10
|
+
require_relative "delivery"
|
|
10
11
|
require_relative "session_inbox"
|
|
11
12
|
|
|
12
13
|
require_relative "bridge/bounded_queue"
|
|
@@ -61,6 +62,10 @@ module Samagotchi
|
|
|
61
62
|
MAX_BODY_BYTES = 1_000_000
|
|
62
63
|
# A request's deadline (see #handle_post_turn) that isn't epoch seconds.
|
|
63
64
|
BAD_DEADLINE = [{ "Allow" => "POST" }, 400, { error: "bad_deadline", detail: "deadline must be epoch seconds" }].freeze
|
|
65
|
+
# A request's delivery (see #handle_post_turn) that isn't a string. An
|
|
66
|
+
# unknown string is not an error: it is the default, as a missing one is.
|
|
67
|
+
BAD_DELIVERY = [{ "Allow" => "POST" }, 400,
|
|
68
|
+
{ error: "bad_delivery", detail: "delivery must be a string" }].freeze
|
|
64
69
|
|
|
65
70
|
# @param engine [Samagotchi::Engine] the owning engine (must already live
|
|
66
71
|
# in this process)
|
|
@@ -826,6 +831,11 @@ module Samagotchi
|
|
|
826
831
|
deadline = fetched(parsed, "deadline")
|
|
827
832
|
return BAD_DEADLINE unless deadline_valid?(deadline)
|
|
828
833
|
|
|
834
|
+
raw_delivery = fetched(parsed, "delivery")
|
|
835
|
+
return BAD_DELIVERY unless raw_delivery.nil? || raw_delivery.is_a?(String)
|
|
836
|
+
|
|
837
|
+
delivery = Delivery.parse(raw_delivery)
|
|
838
|
+
|
|
829
839
|
# A session command sent as a message (chi send -m "/model x", chi -p,
|
|
830
840
|
# a UI whose command list lags) runs as the command, as typed in a
|
|
831
841
|
# TUI; an unknown /word stays a prompt. Not with images: those are
|
|
@@ -844,7 +854,7 @@ module Samagotchi
|
|
|
844
854
|
next :expired if expired?("turn_expired", deadline, sid: sid, client_id: client_id)
|
|
845
855
|
|
|
846
856
|
enqueue_turn(prompt: prompt, client_id: client_id, enqueued_id: enqueued_id,
|
|
847
|
-
no_interrupt: no_interrupt, images: images).tap do |ok|
|
|
857
|
+
no_interrupt: no_interrupt, images: images, delivery: delivery).tap do |ok|
|
|
848
858
|
next unless ok
|
|
849
859
|
|
|
850
860
|
enqueued_event = { type: :turn_enqueued, enqueued_id: enqueued_id, client_id: client_id, prompt: prompt.to_s }
|
|
@@ -856,12 +866,17 @@ module Samagotchi
|
|
|
856
866
|
return deadline_passed("turn") if enqueued == :expired
|
|
857
867
|
return [{}, 500, { error: "enqueue_failed", detail: "could not write turn input" }] unless enqueued
|
|
858
868
|
|
|
859
|
-
# Queued (outside the event lock): a
|
|
860
|
-
#
|
|
861
|
-
# chi doesn't know sent it. An image input never
|
|
862
|
-
# it never cuts.
|
|
863
|
-
|
|
864
|
-
|
|
869
|
+
# Queued (outside the event lock): a message sent as +cut+ cuts a
|
|
870
|
+
# running generation that has only been thinking for long, unless a
|
|
871
|
+
# plugin or a client chi doesn't know sent it. An image input never
|
|
872
|
+
# merges mid-turn, so it never cuts. Nothing else cuts: a message
|
|
873
|
+
# that says nothing (an older client, the web composer) goes in at
|
|
874
|
+
# the running turn's next step.
|
|
875
|
+
cut = nil
|
|
876
|
+
cut = @engine.cut_for_steer(Steer.source_for_client(client_id), epoch: epoch) if images.empty? && Delivery.cut?(delivery)
|
|
877
|
+
ack = { status: "accepted", enqueued_id: enqueued_id, session_id: @session_id, delivery: delivery }
|
|
878
|
+
ack[:cut] = cut.to_s if cut
|
|
879
|
+
[{}, 202, ack]
|
|
865
880
|
end
|
|
866
881
|
|
|
867
882
|
# Read-only snapshot surface. AC #4: too-old reconnects re-derive state
|
|
@@ -933,9 +948,9 @@ module Samagotchi
|
|
|
933
948
|
|
|
934
949
|
# Write a turn into this session's input dir, reusing the file IPC the
|
|
935
950
|
# worker polls. Never calls run_turn across the boundary.
|
|
936
|
-
def enqueue_turn(prompt:, client_id: nil, enqueued_id: nil, no_interrupt: false, images: [])
|
|
951
|
+
def enqueue_turn(prompt:, client_id: nil, enqueued_id: nil, no_interrupt: false, images: [], delivery: nil)
|
|
937
952
|
SessionInbox.write_input(session_dir, prompt: prompt, client_id: client_id, enqueued_id: enqueued_id,
|
|
938
|
-
no_interrupt: no_interrupt, images: images)
|
|
953
|
+
no_interrupt: no_interrupt, images: images, delivery: delivery)
|
|
939
954
|
end
|
|
940
955
|
|
|
941
956
|
# ── HTTP plumbing ────────────────────────────────────────────────────────
|
|
@@ -4,6 +4,7 @@ require "json"
|
|
|
4
4
|
require "socket"
|
|
5
5
|
|
|
6
6
|
require_relative "worker_sidecar"
|
|
7
|
+
require_relative "delivery"
|
|
7
8
|
require_relative "bridge_client/sse_parser"
|
|
8
9
|
require_relative "bridge_client/event_stream"
|
|
9
10
|
|
|
@@ -139,10 +140,11 @@ module Samagotchi
|
|
|
139
140
|
# @param no_interrupt [Boolean] run the turn with the raised iteration limit
|
|
140
141
|
# @param images [Array<Hash>] refs ({file:, name:}) to images already in
|
|
141
142
|
# the session's images/
|
|
142
|
-
def post_turn(prompt:, client_id: nil, no_interrupt: false, images: nil)
|
|
143
|
+
def post_turn(prompt:, client_id: nil, no_interrupt: false, images: nil, delivery: nil)
|
|
143
144
|
body = { session_id: @session_id, prompt: prompt, client_id: client_id }
|
|
144
145
|
body[:no_interrupt] = true if no_interrupt
|
|
145
146
|
body[:images] = images if images && !images.empty?
|
|
147
|
+
body[:delivery] = delivery unless delivery.nil? || Delivery.next_step?(delivery)
|
|
146
148
|
body[:deadline] = deadline
|
|
147
149
|
post("turn", body, read_body: true)
|
|
148
150
|
end
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: loop-guard
|
|
3
|
-
version: 0.3.
|
|
3
|
+
version: 0.3.7
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Breaks loops: the same tool call with the same result again and again in one turn is denied with advice (after a few denies the turn stops with a card), and thinking that repeats itself while it streams is cut and the model asked again"
|
|
6
6
|
trust_level: reviewed
|
|
7
7
|
plugin:
|
|
8
8
|
file: plugin.rb
|
|
9
|
-
sha256: sha256:
|
|
9
|
+
sha256: sha256:e8c31f608f7cef5d605285efcc40ca32efd7fd60c862a39480b9edf61df56acc
|
|
10
10
|
requires_chi: ">= 0.6.0"
|
|
@@ -313,8 +313,20 @@ class Plugin
|
|
|
313
313
|
# A stretch of streamed thinking. The first loop in a turn is cut and the
|
|
314
314
|
# model asked again (action retry); a second one, or action stop, stops
|
|
315
315
|
# the turn with a card; action notify only warns, once per generation.
|
|
316
|
+
#
|
|
317
|
+
# A restarted: true fire is a dropped stream's step asked again: the
|
|
318
|
+
# model streams from the start, and what the dropped attempt's watch
|
|
319
|
+
# holds is void — a re-thought opening would repeat it and look like a
|
|
320
|
+
# loop. The watch starts over (a new one, before this fire's thinking);
|
|
321
|
+
# the restart is no good step, and a loop cut before it still counts.
|
|
316
322
|
def progress(event, ctx)
|
|
317
|
-
return if @watch.nil?
|
|
323
|
+
return if @watch.nil?
|
|
324
|
+
|
|
325
|
+
if event[:restarted]
|
|
326
|
+
@watch = ThinkingWatch.new(@thinking)
|
|
327
|
+
@watch_done = false
|
|
328
|
+
end
|
|
329
|
+
return if @watch_done
|
|
318
330
|
|
|
319
331
|
found = @watch.feed(event[:thinking])
|
|
320
332
|
return unless found
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
# What a message sent to a running turn does when it arrives: wait for the
|
|
5
|
+
# turn's next step boundary and go in there (the default), cut the running
|
|
6
|
+
# generation short to go in now, or wait for the turn to end and run as a
|
|
7
|
+
# turn of its own (see docs/configuration.md). The field travels as the
|
|
8
|
+
# wire value name; a sender that names nothing gets the default.
|
|
9
|
+
module Delivery
|
|
10
|
+
NEXT_STEP = "next_step"
|
|
11
|
+
CUT = "cut"
|
|
12
|
+
QUEUE = "queue"
|
|
13
|
+
VALUES = [NEXT_STEP, CUT, QUEUE].freeze
|
|
14
|
+
|
|
15
|
+
# A body's delivery field as one of VALUES. Missing, empty or unknown:
|
|
16
|
+
# NEXT_STEP (an older client, a hand-written request).
|
|
17
|
+
# @param raw [Object] the wire value
|
|
18
|
+
# @return [String] one of VALUES
|
|
19
|
+
def self.parse(raw)
|
|
20
|
+
raw.is_a?(String) && VALUES.include?(raw) ? raw : NEXT_STEP
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Whether it is the one every sender defaulting to a step boundary uses.
|
|
24
|
+
def self.next_step?(value) = parse(value) == NEXT_STEP
|
|
25
|
+
|
|
26
|
+
# Whether the running turn should be cut for it (only when the message
|
|
27
|
+
# asks: nothing else cuts).
|
|
28
|
+
def self.cut?(value) = parse(value) == CUT
|
|
29
|
+
|
|
30
|
+
# Whether it waits for the turn to end and runs as a turn of its own.
|
|
31
|
+
def self.queue?(value) = parse(value) == QUEUE
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -2570,7 +2570,7 @@ module Samagotchi
|
|
|
2570
2570
|
# A dropped stream's step asked again: it streams from the start.
|
|
2571
2571
|
if event[:restarted]
|
|
2572
2572
|
@generation_thought = nil
|
|
2573
|
-
watch&.started(event[:iteration])
|
|
2573
|
+
watch&.started(event[:iteration], restarted: true)
|
|
2574
2574
|
end
|
|
2575
2575
|
when :generation_chunk then note_generation_thought(progress[:thinking])
|
|
2576
2576
|
end
|