samagotchi 0.48.0 → 0.50.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 +73 -1
- data/bin/chi +2 -2
- data/docs/cli.md +12 -1
- data/docs/configuration.md +1 -1
- data/docs/context.md +9 -2
- data/docs/hooks.md +9 -1
- data/docs/internals/background-tasks.md +1 -1
- data/docs/plugins.md +4 -2
- data/docs/sessions.md +5 -2
- data/docs/testing.md +2 -1
- data/lib/samagotchi/bundles/github-pr/manifest.yml +2 -2
- data/lib/samagotchi/bundles/github-pr/plugin.rb +39 -6
- 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/child_move.rb +22 -8
- data/lib/samagotchi/engine.rb +3 -2
- data/lib/samagotchi/hooks/stream_watch.rb +9 -2
- data/lib/samagotchi/host_registry.rb +1 -1
- data/lib/samagotchi/model_profile.rb +11 -4
- data/lib/samagotchi/plugin/context.rb +13 -1
- data/lib/samagotchi/prompt.rb +26 -11
- data/lib/samagotchi/prompt_literal_guard.rb +4 -1
- data/lib/samagotchi/send_command.rb +61 -0
- data/lib/samagotchi/session_commands.rb +1 -2
- data/lib/samagotchi/session_metrics.rb +7 -0
- data/lib/samagotchi/sessions_command.rb +84 -7
- data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -2
- data/lib/samagotchi/terminal_ui/event_renderer.rb +4 -2
- data/lib/samagotchi/terminal_ui/formatting.rb +17 -2
- data/lib/samagotchi/terminal_ui/question_prompt.rb +7 -0
- data/lib/samagotchi/terminal_ui.rb +1 -1
- data/lib/samagotchi/tools/task_runtime.rb +4 -3
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +3 -3
- data/lib/samagotchi/web/public/app.js +12 -3
- data/lib/samagotchi/web/public/live_timing.js +32 -5
- 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/timing.js +11 -0
- data/lib/samagotchi/web/public/turn_events.js +11 -0
- data/lib/samagotchi/web/public/turn_html.js +21 -0
- data/lib/samagotchi/web/public/turn_view.js +9 -3
- data/lib/samagotchi/web/session_hub.rb +11 -3
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a34348a50eeb261495c985e51195fa87b9f04121e56852083b014229bf05323a
|
|
4
|
+
data.tar.gz: 4bc36375cf4eb7bfba24212c080b86759026c29e17bae2adae653f0d2f7e88f2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 68eea3684c38f7ca72ab13898f461673e1ddb817dce4a6f9dc250ba41230e9e526601ed8c632b58cdba2dce5b5f6fbdb5559da4a6ffa5971680096d36e6973b5
|
|
7
|
+
data.tar.gz: ecc35d61cb8b8560e688fcda868c91135c2f1a1662f649f5a21105c912614a2211d240ea39d3f15bb56500d52b98743decd2c0d5a13eab4f43ba68962076bd1b
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,76 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.50.0] - 2026-10-09
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `chi sessions stats ID [--format text|json]`: a session's cost, tokens and progress without running a model turn:
|
|
16
|
+
a live worker's `GET stats` (waiting up to 3 s), else the snapshot its saved `analytics.json` rebuilds (never
|
|
17
|
+
starting a worker); text prints the `/stats` report headed by `<id8> <status> <model>`, json one
|
|
18
|
+
`{session_id, status, live, metrics}` object.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- A turn record in `analytics.json` says whether the turn was a step-limit Continue (`continue: true`, else `false`),
|
|
23
|
+
and the model-notes report counts Continues from that mark when the file has it, instead of the older guess (its
|
|
24
|
+
turn records with no prompt of their own), which also counted reminder turns and prompts `!rollback` erased.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- TUI: a tool row whose output the runner cut says so with a dim `[cut]`, as the web's row already marks it.
|
|
29
|
+
- `/models` is listed as an anytime command (`mid-turn too`) in `/help` and the web's command list, not the
|
|
30
|
+
per-line `depends` label it got while it always runs mid-turn.
|
|
31
|
+
- Web: a host added to `hosts:` (or a `hosts.<name>.models` context window changed) in config.yml while `chi web`
|
|
32
|
+
runs now reaches the session cards' ctx % on their next refresh, not only after a restart; the hub's host registry
|
|
33
|
+
is rebuilt when `hosts:` changes, as the model picker's already was.
|
|
34
|
+
- A host listing saved to the state dir when the registry was built with its own env (the spawn-time model check, the
|
|
35
|
+
self report) writes under that env's state dir, not `ENV`'s.
|
|
36
|
+
- loop-guard: a dropped stream's step asked again (it streams from the start) no longer reads the dropped attempt's
|
|
37
|
+
thinking as part of the new one — the model re-thinking the same opening after a restart could be cut or stopped
|
|
38
|
+
as "thinking repeats itself". The first `:generation_progress` fire after such a restart carries `restarted: true`,
|
|
39
|
+
and the loop-guard thinking watch starts over on it (loop-guard 0.3.7).
|
|
40
|
+
- Web: the live timing line counts up from the first second (tenths of a second while under 10 s, ticking every
|
|
41
|
+
200 ms there) instead of reading "0ms" for the turn's first second.
|
|
42
|
+
- Web: the stage trail's items hover their whole text — a flash (a hook notice) its whole line, a call its full
|
|
43
|
+
command when it has one, else its whole truncated text.
|
|
44
|
+
- Web: a `!cmd`'s output shows once, as a command bubble: the history renders the saved `!(<command>)` message as
|
|
45
|
+
one (not a user bubble), and a live `command_ran` no longer adds a second bubble after the redraw.
|
|
46
|
+
- `chi send` refuses a UI-only command (`/stats`, `/exit`, `/recap`, …) instead of passing it to the worker, which
|
|
47
|
+
doesn't run it, so the line reached the model as a prompt and cost a whole turn; the refusal names
|
|
48
|
+
`chi sessions stats` for `/stats` and sends nothing (exit 1, also with `--new` before a session is created).
|
|
49
|
+
|
|
50
|
+
## [0.49.0] - 2026-10-09
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
|
|
54
|
+
- Native Gemma 4 on llama.cpp (with `--mmproj`) sees images: `@path`, `read` on an image and the web's pasted images
|
|
55
|
+
reach it, as they do Qwen 3.6. Before, chi refused with "profile gemma4 has no image template yet".
|
|
56
|
+
- Plugins: `ctx.frontend` says what runs the session: `:repl` (the REPL, `-p` without `--non-interactive`),
|
|
57
|
+
`:one_shot` (`-p --non-interactive`) or `:worker` (a session worker: the web, an attached TUI).
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
|
|
61
|
+
- github-pr 0.4.0: line links read the PR's files (`gh api`) only in a session worker (the web, an attached chi), not
|
|
62
|
+
in the REPL, `chi -p` or a scratch session, whose answers no web page shows while they run. `bundles: github-pr:
|
|
63
|
+
line_links: always` reads in the REPL and `-p` too; `false` still turns it off. The PR lookup and attach are
|
|
64
|
+
unchanged. See [Line links](docs/context.md#line-links).
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
|
|
68
|
+
- Web: a command queued mid-turn (`/model X`) shows its bubble after the turn's answer once it runs, not among the
|
|
69
|
+
turn's rows above the answer.
|
|
70
|
+
- A background task whose process exited and whose pid another process reused since no longer reads as running in
|
|
71
|
+
`task_list`, `task_get` and `task_wait` until a `task_stop`: each read checks the process's start time.
|
|
72
|
+
- A continue that died after noting its moved delegates no longer notes them again when the new link's worker
|
|
73
|
+
finishes the move: `move.json` records who was told.
|
|
74
|
+
- An attached chi watching a `-p --non-interactive` run says a question there was dismissed because no one could
|
|
75
|
+
answer it (`(dismissed: no one to answer in a non-interactive run)`), as the run itself does, not
|
|
76
|
+
`(question cancelled)`.
|
|
77
|
+
|
|
78
|
+
Update with `chi update`: it updates github-pr (0.4.0: line links only where the web can show them). If you set
|
|
79
|
+
`line_links: true` to get links in the REPL, set `line_links: always` instead.
|
|
80
|
+
|
|
11
81
|
## [0.48.0] - 2026-10-09
|
|
12
82
|
|
|
13
83
|
### Changed
|
|
@@ -2231,7 +2301,9 @@ and long-lived sessions.
|
|
|
2231
2301
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
2232
2302
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
2233
2303
|
|
|
2234
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
2304
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.50.0...HEAD
|
|
2305
|
+
[0.50.0]: https://github.com/dm1try/samagotchi/compare/v0.49.0...v0.50.0
|
|
2306
|
+
[0.49.0]: https://github.com/dm1try/samagotchi/compare/v0.48.0...v0.49.0
|
|
2235
2307
|
[0.48.0]: https://github.com/dm1try/samagotchi/compare/v0.47.0...v0.48.0
|
|
2236
2308
|
[0.47.0]: https://github.com/dm1try/samagotchi/compare/v0.46.1...v0.47.0
|
|
2237
2309
|
[0.46.1]: https://github.com/dm1try/samagotchi/compare/v0.46.0...v0.46.1
|
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,7 +15,7 @@
|
|
|
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
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 picks it up); 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))
|
|
@@ -392,6 +392,17 @@ status: 0 when all are gone, 1 when any was refused or unknown, 2 on a usage
|
|
|
392
392
|
error. The Web UI deletes too: `delete` in the info bar, or the ✕ on a card in
|
|
393
393
|
All sessions; it asks first and stops a live worker.
|
|
394
394
|
|
|
395
|
+
`chi sessions stats ID [--format text|json]` prints a session's cost, tokens
|
|
396
|
+
and progress without running a model turn — the same text `/stats` prints in
|
|
397
|
+
a terminal, headed by `<id8> <status> <model>`: a live worker answers its
|
|
398
|
+
Bridge's `GET stats`, else the snapshot is rebuilt from the session's saved
|
|
399
|
+
`analytics.json` (never starting a worker; a worker that does not answer in
|
|
400
|
+
time falls back to the disk and the line says so). `--format json` prints one
|
|
401
|
+
object `{session_id, status, live, metrics}`; a session with no
|
|
402
|
+
`analytics.json` yet reports `metrics: null` (`(no metrics yet)` in text) and
|
|
403
|
+
exits 0. An unknown or ambiguous id exits 1 with `chi sessions stop`'s
|
|
404
|
+
wording. See [Sessions](sessions.md#cli).
|
|
405
|
+
|
|
395
406
|
**The plain REPL.** Some launches run the session in this process instead, with
|
|
396
407
|
no worker:
|
|
397
408
|
|
data/docs/configuration.md
CHANGED
|
@@ -904,7 +904,7 @@ Whether a model can see images is found out before a turn with images is sent:
|
|
|
904
904
|
|
|
905
905
|
- a native llama.cpp host: `/props` must report `modalities.vision` (the server
|
|
906
906
|
runs with `--mmproj`) and a media marker, and the prompt profile must know the
|
|
907
|
-
chat template's image wrapping (qwen36
|
|
907
|
+
chat template's image wrapping (qwen36 and gemma4 do);
|
|
908
908
|
- mlx and oMLX hosts: no (not verified with recent chi versions);
|
|
909
909
|
- an OpenAI-API host: a local llama.cpp's `/props`, else the host's model list
|
|
910
910
|
(OpenRouter's `architecture.input_modalities`); when it doesn't say, the image
|
data/docs/context.md
CHANGED
|
@@ -258,13 +258,20 @@ show the answer as it is.
|
|
|
258
258
|
So the first answer of a very short turn may come without links, and
|
|
259
259
|
after a push links may be a few lines off until the next read. Without
|
|
260
260
|
`gh` or its login, nothing is linked.
|
|
261
|
+
- **Where it reads**: only in a session worker (the web, an attached chi),
|
|
262
|
+
whose answers the web shows while the session runs. The REPL and
|
|
263
|
+
`chi -p` read nothing (no `gh api` calls), so their answers have no links,
|
|
264
|
+
in the web too; a scratch session never reads. The PR itself is still
|
|
265
|
+
attached and its context still reaches the model everywhere.
|
|
261
266
|
|
|
262
|
-
|
|
267
|
+
`line_links:` takes `false` (off), `true` (the default: a worker only) or
|
|
268
|
+
`always` (the REPL and `chi -p` read too, so their answers are linked when
|
|
269
|
+
you open the session in the web later):
|
|
263
270
|
|
|
264
271
|
```yaml
|
|
265
272
|
bundles:
|
|
266
273
|
github-pr:
|
|
267
|
-
line_links: false
|
|
274
|
+
line_links: false # or always
|
|
268
275
|
```
|
|
269
276
|
|
|
270
277
|
## Safety
|
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
|
|
@@ -19,7 +19,7 @@ Behavior:
|
|
|
19
19
|
|
|
20
20
|
- `execute`, `task_create` and the `!cmd` shell bang run the command in a non-login shell: `/bin/sh -c` on Linux; on macOS, when `/bin/zsh` is there, `/bin/zsh --emulate sh +o ignore_braces +o sh_glob -o bash_rematch +o bsd_echo -c` (macOS's `/bin/sh` is bash 3.2, which can't parse an apostrophe in a heredoc inside `"$( )"`, the usual commit message shape). That mode reads no rc files and keeps POSIX word splitting, unmatched globs left as is, `{1..3}`, `[[ … =~ (…) ]]` with `BASH_REMATCH`, arrays, `$'…'`, `&>` and echo expanding `\t`. Bash-only builtins and variables (`shopt`, `BASH_VERSION`, `${!prefix*}`, `read -p`) are not there.
|
|
21
21
|
- Task metadata and output are persisted under `tmp/tasks/`.
|
|
22
|
-
- A task record keeps its process's pid and start time (`pid_started`). Before `task_stop` signals, the pid must still lead its own process group and must not have started at another time; otherwise the pid was reused by another process, nothing is signalled and the task ends as failed. A start time that can't be read at that moment (a failed `ps`) falls back to the group check, as does a record without `pid_started` (from an older chi). `task_get`, `task_list
|
|
22
|
+
- A task record keeps its process's pid and start time (`pid_started`). Before `task_stop` signals, the pid must still lead its own process group and must not have started at another time; otherwise the pid was reused by another process, nothing is signalled and the task ends as failed. A start time that can't be read at that moment (a failed `ps`) falls back to the group check, as does a record without `pid_started` (from an older chi). `task_get`, `task_list`, `task_wait`'s polls and the running-task lists (a cancelled turn's note, a worker's idle exit) make the same check, so a task whose pid was reused reads as exited (failed) at once, not as running until a `task_stop`; on macOS that is one `ps` per running task per check (~5 ms).
|
|
23
23
|
- Task listing is workspace-scoped (current project only).
|
|
24
24
|
- `task_get` returns metadata and `output_path`; use `read` for output contents.
|
|
25
25
|
- `task_wait` accepts `timeout`, `tail_lines` (maximum 100), and `done_pattern` (a regular expression string).
|
data/docs/plugins.md
CHANGED
|
@@ -505,6 +505,7 @@ full label, `plugin.rb (bundle my-bundle)`.
|
|
|
505
505
|
| `ctx.repo_root` | the git checkout holding `cwd`, or nil |
|
|
506
506
|
| `ctx.scratch?` | whether the session is a `chi scratch` one (deleted when it ends) |
|
|
507
507
|
| `ctx.delegate?` | whether the session is a delegate child: a task another session handed over |
|
|
508
|
+
| `ctx.frontend` | what runs the session, read now: `:repl` (the REPL, `-p` without `--non-interactive`), `:one_shot` (`-p --non-interactive`: one turn, then exit) or `:worker` (a session worker: the web, an attached TUI). Only a worker's answers reach the web while it runs; a REPL's show there once the session is opened. Until the host sets it (while the plugins load, inside `Engine.new`) it reads `:one_shot`, so read it in hooks and init tasks, not in `register` |
|
|
508
509
|
| `ctx.fork?` | whether the session is a fork (`ctx.sessions.fork`): it has a parent but isn't a delegate child, and started from a conversation, so its first prompt isn't its first user message |
|
|
509
510
|
| `ctx.model` | the model the session runs on now: its resolved ref (`host:id`), right after `/model` too |
|
|
510
511
|
| `ctx.model_key` | that model's memory overlay key (`<name>.<key>.md`, what `memory_write current_model_only` writes) |
|
|
@@ -1240,8 +1241,9 @@ nothing without `gh`, a repository or an open PR), and resolves PR URLs for
|
|
|
1240
1241
|
`chi context add` and the web's "+ URL". Its script prints the PR as text
|
|
1241
1242
|
and a summary of what changed in counts, authors and states only; it wakes
|
|
1242
1243
|
the session for a review requesting changes, checks turning red, or the PR
|
|
1243
|
-
merged or closed. In the web, a `path:line` in
|
|
1244
|
-
of the session's PR ([Line links](context.md#line-links)
|
|
1244
|
+
merged or closed. In the web, a `path:line` in a worker's answer links to
|
|
1245
|
+
that line of the session's PR ([Line links](context.md#line-links); `line_links:
|
|
1246
|
+
always` for the REPL and `chi -p` too). `auto_attach:
|
|
1245
1247
|
offer` asks with a card first (`/pr-attach N`, `/pr-decline N`), `off`
|
|
1246
1248
|
attaches nothing ([Attach, offer or off](context.md#attach-offer-or-off)).
|
|
1247
1249
|
See [Attached context](context.md#github-prs-the-github-pr-bundle).
|
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`; 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:
|
|
@@ -158,7 +161,7 @@ pbpaste | chi send 3fa2 # the clipboard is the messa
|
|
|
158
161
|
- 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
162
|
- 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
163
|
- `--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.
|
|
164
|
+
- 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.
|
|
162
165
|
- One line per session: `sent`, `sent as a session command`, `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`.
|
|
163
166
|
|
|
164
167
|
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)`.
|
|
@@ -197,7 +200,7 @@ A session can **continue** an earlier one: "the next day of the same routine" (a
|
|
|
197
200
|
- **The recap first:** a previous link with a live worker is asked for a recap (the Bridge's `POST /recap`) and given up to 30 s to write it; one with none wrote its recap when its worker left.
|
|
198
201
|
- **The previous link is archived** before the new one starts, by the archive's own rules (see *Archiving a session*): a turn running, a prompt queued or the step-limit question refuse it, a live idle worker is stopped, a plain REPL's session is refused. Its finished delegates are archived with it, as any archive does.
|
|
199
202
|
- **Its open delegates move to the new link** instead: each direct delegate that is running, waiting for an answer, has a live worker or a reply its parent wasn't given, or has such a delegate below it (checked again after the recap wait, so one delegated meanwhile moves too). It moves with its own delegates, stays unarchived, and from then on is the new link's: its card folds under the new link, its reports ring the new link (an idle one runs a wake turn for them, as for its own delegates), `delegate_result` and the `⑂` chip count it there, and what its parent was already told (`delegates.json`) and any report still waiting come along, so nothing is reported twice. A moved delegate gets a context note from `session chain`, `Your parent session is now 4b7e9a01 (…), the next link of its session chain: …`, as its system prompt keeps naming the parent it started with. The move writes `<child>/parent.json` (`{parent_id, from, at}`) beside the child's session file, never the file itself, so a live child worker saving its own copy changes nothing; every reader applies it (a later move overwrites it; it is never removed). A continue is **refused for an open delegate that can't move** (`ContinueRefused`, naming it): one whose live worker runs an older chi (`older chi worker 0.46.1; restart it`: `chi sessions restart <id>`, or stop it) or that is open in a chi REPL (close it there). An older `chi web` or CLI still running shows a moved delegate under the archived link until it is restarted.
|
|
200
|
-
- **If the continue fails part-way:** a new link that fails to start gives its moved delegates back before it is deleted (each gets a note saying so; its worker is stopped first, if one started, and a worker that won't stop is reported, the link left), with any report one of them rang it with meanwhile; the previous link, back in the lists, is woken for reports waiting. A continue that dies in the middle leaves a `move.json` in the new link's folder: the new link's next worker finishes that move (only the delegates it names) as it starts. Until the new link's own worker holds it, a ring from a moved delegate doesn't start another (a `starting` marker in its folder, which that worker removes; a stale one, over 60 s old or from a process that is gone, is ignored). An open delegate below an archived delegate refuses the continue (`under archived …`): unarchive that one, or archive this one.
|
|
203
|
+
- **If the continue fails part-way:** a new link that fails to start gives its moved delegates back before it is deleted (each gets a note saying so; its worker is stopped first, if one started, and a worker that won't stop is reported, the link left), with any report one of them rang it with meanwhile; the previous link, back in the lists, is woken for reports waiting. A continue that dies in the middle leaves a `move.json` in the new link's folder: the new link's next worker finishes that move (only the delegates it names, and without a second note to one already told) as it starts. Until the new link's own worker holds it, a ring from a moved delegate doesn't start another (a `starting` marker in its folder, which that worker removes; a stale one, over 60 s old or from a process that is gone, is ignored). An open delegate below an archived delegate refuses the continue (`under archived …`): unarchive that one, or archive this one.
|
|
201
204
|
- **Refused too:** a link that is continued already (naming the link that continues it; continue that one, or `last:<id>`), a previous link whose folder is gone, and one whose model names a host config.yml no longer has. A refusal starts nothing and archives nothing; a new link that fails to start puts back what was archived. Every refusal that can be known up front (the archive's ones included) comes before the recap wait. One continue of a session runs at a time (a lock in its folder; other chains don't wait), so a double click finds the first one's link and is refused, while a `last:<id>` continue that waited follows the chain to its new end.
|
|
202
205
|
- **Web:** `POST /api/sessions` takes `continues: "<id, prefix or last:id>"`, with `idle: true` (a new link waiting for its first message, previewed by the previous link's, or `preview`) or a `prompt`, and not `dir`, `model` or `llm_context` (400 `invalid_continues`: the previous link's are taken). 201 is the new session's summary, `continues` set, with `moved`: the ids of the delegates that moved to it (`[]` for none); 404 `not_found`; 400 `ambiguous_id` for a prefix of several sessions, `invalid_model` for a model whose host is gone; 409 `continued` with `next_id` (the link that continues it already, which the page opens instead), `open_children` with `ids` (open delegates that can't move), `folder_gone`, `busy`, `scratch` or `owned_by_tui`. Every session summary carries `continues`.
|
|
203
206
|
- **The page:** the info bar's `continue →` continues the open session (`continues: <id>, idle: true`; it reads `continuing…` through the recap wait) and opens the new link with `Continue where we left off.` in the composer, not sent (the same line for every chain; a draft already there stays instead); the previous link is archived, a toast says so (`Continued 3fa2c1d0 in 4b7e9a01; 3fa2c1d0 is archived; 1 open delegate moved`), and a moved delegate's card folds under the new link. A link continued already has no `continue →`: its `→` link goes to the next link (a stale page's continue answered 409 `continued` opens `next_id`). A continue refused for open delegates that can't move shows a toast that names each one (its preview and what the server says to do: `older chi worker 0.46.1; restart it`, `open in a chi REPL; close it there`), each a link to its page; any other refusal shows the server's words. The info bar of a link carries `← 2026-10-08` (the previous link's day) and, on an earlier link, `2026-10-10 →`, each a link with its preview and id in the tooltip. A link whose title is the opener and nothing else (its first message once sent) shows its chain's first link's title instead, on its card, in the popover and in its info bar's title line, display only (its messages and data keep the opener; its own title when the first link is gone). The lists fold a chain into the card of its latest listed link, whatever the earlier links' archived state (typing in an archived link brings it back, folded there still), their delegates with them: the card's `↩ day N` chip (`day N of M` when later links aren't listed) opens a popover of the other links, newest first (`day N`, its date, its recap's first sentence or its preview, `archived`), a click opens one; the chip shows a live dot or `waiting` for a link that has a worker or waits on the user, and a waiting link puts its chain's card first. All sessions folds nothing while a search is typed or "include archived" is on: every link is a card of its own then, its chip `day N of M`, with its own delegates folded in, so a search finds an earlier link and its delegates.
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: github-pr
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.4.0
|
|
4
4
|
scope: system
|
|
5
5
|
description: Attaches the branch's open GitHub pull request to a session as context (and any PR URL with chi context add or the web's "+ URL"); the agent gets a note when reviews, comments, checks or its state change; in the web, a path:line in an answer links to that line of the PR; the auto_attach setting can offer the PR with a card first, or attach nothing
|
|
6
6
|
trust_level: reviewed
|
|
@@ -11,7 +11,7 @@ needs:
|
|
|
11
11
|
hint: brew install gh, then gh auth login
|
|
12
12
|
plugin:
|
|
13
13
|
file: plugin.rb
|
|
14
|
-
sha256: sha256:
|
|
14
|
+
sha256: sha256:2510f8e5bfc77b2a6343b47d56bf6be9da09fd8741f52928462945a55d83f0ce
|
|
15
15
|
scripts:
|
|
16
16
|
pr_context.rb: sha256:a883d2780e89c68c0d7b6d5e0fcb22c273f17bb167e79064e0e99db6bd2ee6f0
|
|
17
17
|
context_providers:
|
|
@@ -23,8 +23,11 @@ require "tmpdir"
|
|
|
23
23
|
# model's text stays as it was. The after_turn hook links from a per-PR
|
|
24
24
|
# cache and never runs gh (the web holds the answer until the hooks are
|
|
25
25
|
# done); before_turn, before_generation and the init task fill the cache
|
|
26
|
-
# off the turn's thread
|
|
27
|
-
#
|
|
26
|
+
# off the turn's thread (`gh api`), in a session worker only: the REPL and
|
|
27
|
+
# -p --non-interactive show no web page while they run (ctx.frontend; a chi
|
|
28
|
+
# without it counts as a worker), and a scratch session never fetches.
|
|
29
|
+
# `bundles: github-pr: line_links:` false turns it off, "always" fetches in
|
|
30
|
+
# the REPL and -p too (still not for a scratch session).
|
|
28
31
|
#
|
|
29
32
|
# `bundles: github-pr: auto_attach:` says what the init task does with the
|
|
30
33
|
# branch's open PR: `attach` (the default), `offer` (a card with Attach and
|
|
@@ -37,6 +40,8 @@ class Plugin
|
|
|
37
40
|
COMMAND_SECONDS = 20
|
|
38
41
|
# before_generation looks for a newly attached PR at most this often.
|
|
39
42
|
GENERATION_CHECK_SECONDS = 30
|
|
43
|
+
# line_links' values: off, a worker's only (the default), every frontend.
|
|
44
|
+
LINE_LINKS_MODES = { false => :off, true => :worker, "always" => :always }.freeze
|
|
40
45
|
# auto_attach's values (YAML's bare true/false: attach/off).
|
|
41
46
|
AUTO_ATTACH_MODES = { "attach" => :attach, "offer" => :offer, "off" => :off, true => :attach, false => :off }.freeze
|
|
42
47
|
# /pr-attach and /pr-decline take a PR number (`42` or `#42`).
|
|
@@ -50,6 +55,7 @@ class Plugin
|
|
|
50
55
|
@cache = PrCache.new
|
|
51
56
|
@generation_checked_at = nil
|
|
52
57
|
@warned_mode = false
|
|
58
|
+
@warned_line_links = false
|
|
53
59
|
end
|
|
54
60
|
|
|
55
61
|
def register(chi)
|
|
@@ -58,13 +64,13 @@ class Plugin
|
|
|
58
64
|
# Inline (an init task has its own thread), whatever the attach
|
|
59
65
|
# returned (attached already, a child, a scratch session), and after
|
|
60
66
|
# it, so a PR it just attached is warm for the first answer.
|
|
61
|
-
refresh(ctx, attached_prs(ctx)) if
|
|
67
|
+
refresh(ctx, attached_prs(ctx)) if prefetch?(ctx)
|
|
62
68
|
summary
|
|
63
69
|
end
|
|
64
70
|
chi.on(:before_turn) do |event, ctx|
|
|
65
71
|
# The prompt too: messages is the history before this turn, and a
|
|
66
72
|
# delegate child's task is its first prompt.
|
|
67
|
-
kick(ctx, attached_prs(ctx) + PrRef.from_messages(event[:messages], prompt: event[:prompt])) if
|
|
73
|
+
kick(ctx, attached_prs(ctx) + PrRef.from_messages(event[:messages], prompt: event[:prompt])) if prefetch?(ctx)
|
|
68
74
|
log_first_prompt(event, ctx)
|
|
69
75
|
end
|
|
70
76
|
chi.command("/pr-attach", "Attach the PR this session was offered: /pr-attach 42", anytime: true) do |args, ctx|
|
|
@@ -150,7 +156,34 @@ class Plugin
|
|
|
150
156
|
|
|
151
157
|
private
|
|
152
158
|
|
|
153
|
-
|
|
159
|
+
# `bundles: github-pr: line_links:`: :off, :worker (true or unset) or
|
|
160
|
+
# :always; an unknown value (a quoted "false") is :worker, logged once.
|
|
161
|
+
def line_links_mode(ctx)
|
|
162
|
+
value = ctx.settings["line_links"]
|
|
163
|
+
return :worker if value.nil?
|
|
164
|
+
|
|
165
|
+
LINE_LINKS_MODES.fetch(value.is_a?(String) ? value.strip.downcase : value) do
|
|
166
|
+
ctx.log.warn(:pr_line_links_unknown, value: value.to_s[0, 40]) unless @warned_line_links
|
|
167
|
+
@warned_line_links = true
|
|
168
|
+
:worker
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def line_links?(ctx) = line_links_mode(ctx) != :off
|
|
173
|
+
|
|
174
|
+
# Whether to fetch PR data for line links (gh api): not for a scratch
|
|
175
|
+
# session (gone at exit), and by default only in a session worker, the
|
|
176
|
+
# one frontend whose answers the web shows while it runs. A chi before
|
|
177
|
+
# ctx.frontend (0.49.0) counts as a worker.
|
|
178
|
+
def prefetch?(ctx)
|
|
179
|
+
return false if ctx.scratch?
|
|
180
|
+
|
|
181
|
+
case line_links_mode(ctx)
|
|
182
|
+
when :off then false
|
|
183
|
+
when :always then true
|
|
184
|
+
else (ctx.respond_to?(:frontend) ? ctx.frontend : :worker) == :worker
|
|
185
|
+
end
|
|
186
|
+
end
|
|
154
187
|
|
|
155
188
|
def offer_card_id(number) = "github-pr-offer-#{number}"
|
|
156
189
|
|
|
@@ -275,7 +308,7 @@ class Plugin
|
|
|
275
308
|
# A PR attached while the turn runs (a parent attaching it right after
|
|
276
309
|
# `delegate wait: false`), checked at most every GENERATION_CHECK_SECONDS.
|
|
277
310
|
def check_attached(ctx)
|
|
278
|
-
return unless
|
|
311
|
+
return unless prefetch?(ctx)
|
|
279
312
|
return if @generation_checked_at && now - @generation_checked_at < GENERATION_CHECK_SECONDS
|
|
280
313
|
|
|
281
314
|
@generation_checked_at = now
|
|
@@ -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
|
|
@@ -30,7 +30,7 @@ module Samagotchi
|
|
|
30
30
|
# until it is done: a worker that starts and finds one finishes the move
|
|
31
31
|
# (#adopt), and a failed continue moves the same children back.
|
|
32
32
|
module ChildMove
|
|
33
|
-
# <new link dir>/move.json: {from, ids}, while a move is under way.
|
|
33
|
+
# <new link dir>/move.json: {from, ids, noted}, while a move is under way.
|
|
34
34
|
INTENT_FILE = "move.json"
|
|
35
35
|
# <new link dir>/starting: {pid, at}, from the move to the worker's spawn.
|
|
36
36
|
STARTING_FILE = "starting"
|
|
@@ -47,8 +47,11 @@ module Samagotchi
|
|
|
47
47
|
# link, their subtrees with them), +refuse+ the open ones it can't move
|
|
48
48
|
# (SessionManager::OpenChild, why: an older chi worker or a chi REPL).
|
|
49
49
|
Plan = Data.define(:move, :refuse)
|
|
50
|
-
# A move under way: from the session, the children moved
|
|
51
|
-
|
|
50
|
+
# A move under way: from the session, the children moved, and those
|
|
51
|
+
# already sent their note (so a move finished again notes no one twice).
|
|
52
|
+
Intent = Data.define(:from, :ids, :noted) do
|
|
53
|
+
def initialize(from:, ids:, noted: []) = super
|
|
54
|
+
end
|
|
52
55
|
|
|
53
56
|
module_function
|
|
54
57
|
|
|
@@ -100,16 +103,26 @@ module Samagotchi
|
|
|
100
103
|
# Move +ids+ (direct children of +from+) to +to+: the intent, the
|
|
101
104
|
# cursors +to+ lacks (delegates.json), the overrides, the rings waiting
|
|
102
105
|
# in +from+'s children/, a note to each child, then the intent goes.
|
|
103
|
-
# Safe to run again on the same move
|
|
106
|
+
# Safe to run again on the same move: the intent records each child
|
|
107
|
+
# noted, and a run finishing it (#adopt) notes only the rest. A crash
|
|
108
|
+
# between a note and its record notes that child twice (better than
|
|
109
|
+
# none).
|
|
104
110
|
# @param undo [Boolean] a failed continue moving them back: its note says so
|
|
105
111
|
def apply(ids, from:, to:, state_dir:, undo: false)
|
|
106
112
|
return if ids.empty?
|
|
107
113
|
|
|
108
|
-
|
|
114
|
+
intent = Intent.new(from: from, ids: ids)
|
|
115
|
+
left = read_intent(to, state_dir: state_dir)
|
|
116
|
+
intent = intent.with(noted: left.noted & ids) if left && left.from == from && left.ids.sort == ids.sort
|
|
117
|
+
write_intent(to, intent, state_dir: state_dir)
|
|
109
118
|
copy_cursors(ids, from: from, to: to, state_dir: state_dir)
|
|
110
119
|
ids.each { |id| Session.reparent(id, to: to, from: from, state_dir: state_dir) }
|
|
111
120
|
move_rings(ids, from: from, to: to, state_dir: state_dir)
|
|
112
|
-
ids.each
|
|
121
|
+
(ids - intent.noted).each do |id|
|
|
122
|
+
note_child(id, to: to, undo: undo, state_dir: state_dir)
|
|
123
|
+
intent = intent.with(noted: intent.noted + [id])
|
|
124
|
+
write_intent(to, intent, state_dir: state_dir)
|
|
125
|
+
end
|
|
113
126
|
clear_intent(to, state_dir: state_dir)
|
|
114
127
|
Log.info(:worker, undo ? "delegates_moved_back" : "delegates_moved", from: from[0, 8], to: to[0, 8], count: ids.size)
|
|
115
128
|
end
|
|
@@ -143,7 +156,8 @@ module Samagotchi
|
|
|
143
156
|
data = JSON.parse(File.read(File.join(Session.session_dir(session_id, state_dir: state_dir), INTENT_FILE)))
|
|
144
157
|
return nil unless data.is_a?(Hash) && Session.valid_id?(data["from"]) && data["ids"].is_a?(Array)
|
|
145
158
|
|
|
146
|
-
|
|
159
|
+
noted = data["noted"].is_a?(Array) ? data["noted"].select { |id| Session.valid_id?(id) } : []
|
|
160
|
+
Intent.new(from: data["from"], ids: data["ids"].select { |id| Session.valid_id?(id) }, noted: noted)
|
|
147
161
|
rescue JSON::ParserError, SystemCallError
|
|
148
162
|
nil
|
|
149
163
|
end
|
|
@@ -151,7 +165,7 @@ module Samagotchi
|
|
|
151
165
|
def write_intent(session_id, intent, state_dir:)
|
|
152
166
|
dir = Session.session_dir(session_id, state_dir: state_dir)
|
|
153
167
|
FileUtils.mkdir_p(dir)
|
|
154
|
-
AtomicFile.write(File.join(dir, INTENT_FILE), JSON.generate({ "from" => intent.from, "ids" => intent.ids }))
|
|
168
|
+
AtomicFile.write(File.join(dir, INTENT_FILE), JSON.generate({ "from" => intent.from, "ids" => intent.ids, "noted" => intent.noted }))
|
|
155
169
|
end
|
|
156
170
|
|
|
157
171
|
def clear_intent(session_id, state_dir:)
|
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -2470,7 +2470,8 @@ module Samagotchi
|
|
|
2470
2470
|
model_key: -> { @model_key },
|
|
2471
2471
|
state_dir: -> { session_state_dir },
|
|
2472
2472
|
scratch: -> { @scratch },
|
|
2473
|
-
llm_context: -> { @session&.llm_context }
|
|
2473
|
+
llm_context: -> { @session&.llm_context },
|
|
2474
|
+
frontend: -> { interface }
|
|
2474
2475
|
)
|
|
2475
2476
|
end
|
|
2476
2477
|
|
|
@@ -2569,7 +2570,7 @@ module Samagotchi
|
|
|
2569
2570
|
# A dropped stream's step asked again: it streams from the start.
|
|
2570
2571
|
if event[:restarted]
|
|
2571
2572
|
@generation_thought = nil
|
|
2572
|
-
watch&.started(event[:iteration])
|
|
2573
|
+
watch&.started(event[:iteration], restarted: true)
|
|
2573
2574
|
end
|
|
2574
2575
|
when :generation_chunk then note_generation_thought(progress[:thinking])
|
|
2575
2576
|
end
|
|
@@ -32,16 +32,20 @@ module Samagotchi
|
|
|
32
32
|
@clock = clock
|
|
33
33
|
@slow_logged = false
|
|
34
34
|
@iteration = nil
|
|
35
|
+
@restarted = false
|
|
35
36
|
end
|
|
36
37
|
|
|
37
|
-
# A new generation (:generation_started)
|
|
38
|
-
|
|
38
|
+
# A new generation (:generation_started), or a dropped stream's step
|
|
39
|
+
# asked again (restarted: true: the first fire after it carries
|
|
40
|
+
# restarted: true — what came before is void).
|
|
41
|
+
def started(iteration, restarted: false)
|
|
39
42
|
@iteration = iteration
|
|
40
43
|
@started_at = @last_fire_at = @clock.call
|
|
41
44
|
@thinking = +""
|
|
42
45
|
@text = +""
|
|
43
46
|
@thinking_chars = 0
|
|
44
47
|
@text_chars = 0
|
|
48
|
+
@restarted = restarted
|
|
45
49
|
end
|
|
46
50
|
|
|
47
51
|
# One chunk's new thinking and visible text.
|
|
@@ -70,6 +74,9 @@ module Samagotchi
|
|
|
70
74
|
elapsed_ms: ((now - @started_at) * 1000).round,
|
|
71
75
|
# A question would hold the HTTP read for minutes.
|
|
72
76
|
ask_user: ->(**) {} }
|
|
77
|
+
# The first fire after a restarted retry says so, once.
|
|
78
|
+
event[:restarted] = true if @restarted
|
|
79
|
+
@restarted = false
|
|
73
80
|
@thinking = +""
|
|
74
81
|
@text = +""
|
|
75
82
|
@last_fire_at = now
|
|
@@ -383,7 +383,7 @@ module Samagotchi
|
|
|
383
383
|
# a worker without listing (`chi send --new --model`, delegate):
|
|
384
384
|
# ModelListStore is what they check an id against.
|
|
385
385
|
def save_model_list(name, models)
|
|
386
|
-
ModelListStore.save(name, Array(models).map(&:id))
|
|
386
|
+
ModelListStore.save(name, Array(models).map(&:id), env: @env)
|
|
387
387
|
end
|
|
388
388
|
|
|
389
389
|
def fresh_list(name, entry)
|