samagotchi 0.41.0 → 0.42.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 +29 -1
- data/docs/cli.md +8 -6
- data/docs/memory.md +18 -0
- data/docs/sessions.md +5 -5
- data/docs/testing.md +16 -0
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/child_reports.rb +8 -4
- data/lib/samagotchi/context_command.rb +5 -2
- data/lib/samagotchi/engine.rb +31 -1
- data/lib/samagotchi/model_notes.rb +7 -4
- data/lib/samagotchi/prompt_note.rb +53 -0
- data/lib/samagotchi/self_report.rb +32 -0
- data/lib/samagotchi/session.rb +16 -1
- data/lib/samagotchi/session_commands.rb +24 -5
- data/lib/samagotchi/session_metrics.rb +21 -4
- data/lib/samagotchi/system_prompt.rb +14 -1
- data/lib/samagotchi/terminal_ui/attached_loop.rb +12 -0
- data/lib/samagotchi/terminal_ui/formatting.rb +4 -0
- data/lib/samagotchi/turn_flow.rb +8 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +15 -5
- data/lib/samagotchi/web/public/app.js +8 -2
- data/lib/samagotchi/web/public/format.js +24 -0
- data/lib/samagotchi/web/public/session_state.js +3 -0
- data/lib/samagotchi/web/public/timing.js +3 -1
- data/lib/samagotchi/web/public/turn_view.js +15 -2
- data/lib/samagotchi/web/session_summary.rb +2 -0
- data/lib/samagotchi/worker.rb +16 -4
- data/lib/samagotchi/worker_wakes.rb +2 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d41b91ef2514e877a756d7f66ba02f58b65c7d85f7446728b02ec06f652299b3
|
|
4
|
+
data.tar.gz: 37e0869508863e5db69395b847ac147101f60a79208a5ba9e789e3427eb9716e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: abb02fdd05aec927500fbb42a750b9da3dc9e1c8e24cbc55bfdc933e797e206ee8e3a3502d0a4074804b0342f856726d2b04d2eb15c2aa61682e23439bca38ea
|
|
7
|
+
data.tar.gz: 681406d11e96949fd168b252a6717fe2f6dcfaa765400bf66a43a7131dc4440ff7f077312f3ccd219ddce8c05e88f15e4bc808c52652d265beed59eed89c8c1c
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,33 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.42.0] - 2026-10-08
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Which model notes a session's prompt carried: each prompt build (and `/model`) records them in the session file
|
|
16
|
+
(`prompt_notes`: name, scope, size and a short digest), and they are shown in `/stats` (`model notes:
|
|
17
|
+
model_notes_deepseek (system, 612 chars)`), at the end of `/model`'s line (`; notes: …`, after a switch the new
|
|
18
|
+
model's), as a `notes: deepseek` chip in the web's info bar and as a `model notes` row in `chi self`. Nothing shows
|
|
19
|
+
without notes. See [docs/memory.md](docs/memory.md) ("Model notes").
|
|
20
|
+
- For development: `script/model_notes_report.rb`, which groups stored sessions by model and by the model notes
|
|
21
|
+
their prompt carried and compares the calls to the first edit and commit, commits per 100 steps, bare `&` in
|
|
22
|
+
`execute`, the longest run without an edit and the Continues and steers each session needed (`--model`,
|
|
23
|
+
`--since`, `--min-steps`, `--json`). It isn't part of the gem. See [docs/testing.md](docs/testing.md).
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- The web showed no failure line after a reload for a failed turn whose steps stayed (new in 0.41.0), so the turn
|
|
28
|
+
looked unfinished; it ends with the same `✕ turn failed: <why>; partial progress kept (!rollback restores the
|
|
29
|
+
pre-turn state)` line as live (from the turn record, which keeps the failure now; a turn recorded before this
|
|
30
|
+
shows none).
|
|
31
|
+
- A delegate report could reach the parent's model twice: a reminder or continue turn that failed before any
|
|
32
|
+
progress kept the report it had read, and the next turn brought it again. A canceled continue turn's report was
|
|
33
|
+
never delivered. A report's ring now goes only when the turn that read it stays in the conversation.
|
|
34
|
+
- `/llm-context` (the web's `llm ctx` chip, `--llm-context` on `--attach`) against a worker on an older chi answered
|
|
35
|
+
`400 not a session command`; the web (`501 not_supported`) and the attached terminal say the worker runs an older
|
|
36
|
+
chi, with the line that restarts it.
|
|
37
|
+
|
|
11
38
|
## [0.41.0] - 2026-10-08
|
|
12
39
|
|
|
13
40
|
### Added
|
|
@@ -1955,7 +1982,8 @@ and long-lived sessions.
|
|
|
1955
1982
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
1956
1983
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
1957
1984
|
|
|
1958
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
1985
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.42.0...HEAD
|
|
1986
|
+
[0.42.0]: https://github.com/dm1try/samagotchi/compare/v0.41.0...v0.42.0
|
|
1959
1987
|
[0.41.0]: https://github.com/dm1try/samagotchi/compare/v0.40.0...v0.41.0
|
|
1960
1988
|
[0.40.0]: https://github.com/dm1try/samagotchi/compare/v0.39.0...v0.40.0
|
|
1961
1989
|
[0.39.0]: https://github.com/dm1try/samagotchi/compare/v0.38.0...v0.39.0
|
data/docs/cli.md
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
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))
|
|
25
|
-
- `chi self` — print version, source dir (checkout or installed gem), config/memory/session paths, model/host and bundles. Run by a session's `execute`, the `model` row (and host, loop, profile, thinking, served model) is **that session's** model, labelled `(this session <id8>; default …)`; elsewhere it is the default, `(default)`. `model key` is the memory overlay key (`<name>.<key>.md`). `chi self --model` prints only the model a new session starts on, an alias resolved to its target (the desktop helper's hint)
|
|
25
|
+
- `chi self` — print version, source dir (checkout or installed gem), config/memory/session paths, model/host and bundles. Run by a session's `execute`, the `model` row (and host, loop, profile, thinking, served model) is **that session's** model, labelled `(this session <id8>; default …)`; elsewhere it is the default, `(default)`. `model key` is the memory overlay key (`<name>.<key>.md`), and `model notes` the model notes a prompt of that model loads (`model_notes_deepseek (system, 612 chars)`, `none`; inside a session its `--mute` list is left out). `chi self --model` prints only the model a new session starts on, an alias resolved to its target (the desktop helper's hint)
|
|
26
26
|
- `chi update [--dry-run] [--no-gem] [--no-bundles] [--no-desktop]` — update an installed chi: the gem, the system bundle, the shipped bundles you installed and the desktop helper, in one table (see [Updating](#updating))
|
|
27
27
|
- `chi bundle install|upgrade|uninstall|status|diff|list|build|trash` — manage memory bundles (see [Bundle hooks](hooks.md#bundle-hooks-unified-workflow-bundle)); `list` shows the installed ones and the ones shipped with chi, which `install <name>` installs; `core` and `dev` are profiles that install a set of them (see [Bundle profiles](memory.md#bundle-profiles-core-and-dev), [Guardrails](guardrails.md), [Plugins](plugins.md#the-btw-bundle), [the mcp bundle](plugins.md#the-mcp-bundle) [the loop-guard bundle](plugins.md#the-loop-guard-bundle), [the check-in bundle](plugins.md#the-check-in-bundle) and [the skills bundle](plugins.md#the-skills-bundle)). `trash` lists and empties the bundle trash (moved files from uninstalls/upgrades). A usage error (an unknown subcommand or flag, a missing argument, a bad `build --scope`) exits 2, as for the other subcommands. Plain `chi` and `chi web` exit 1 on an unknown flag, a flag missing its value or a stray argument
|
|
28
28
|
|
|
@@ -340,7 +340,8 @@ note or image) is deleted as its worker exits, and `/exit` says so; set
|
|
|
340
340
|
|
|
341
341
|
`chi sessions stop ID...` stops each session's worker and waits for it to exit, so
|
|
342
342
|
a `chi --resume ID` after it starts a fresh one. A worker still running an
|
|
343
|
-
older chi (from before an upgrade) takes turns but not commands
|
|
343
|
+
older chi (from before an upgrade) takes turns, but not commands, or not the
|
|
344
|
+
ones newer than it (`/llm-context`, `--llm-context` on `--attach`); the attached
|
|
344
345
|
terminal and the Web UI say so, with that restart line.
|
|
345
346
|
|
|
346
347
|
`chi sessions restart ID...` hands each session to a new worker on the newest
|
|
@@ -428,7 +429,7 @@ request (with the cached tokens, the cache writes, the re-prefilled tokens and t
|
|
|
428
429
|
reports them),
|
|
429
430
|
the decode speed, generation latency, cancellations, retries (the network's),
|
|
430
431
|
thinking cuts (generations a plugin such as loop-guard cut) and output cap hits
|
|
431
|
-
(generations that ended at the provider's cap), context used and window, the memory indexes the session's prompt holds ([Memory](memory.md#the-indexs-size)), the prompt profile and the model the server says it ran, and the
|
|
432
|
+
(generations that ended at the provider's cap), context used and window, the memory indexes the session's prompt holds ([Memory](memory.md#the-indexs-size)), the model notes it carried (`model notes: model_notes_deepseek (system, 612 chars)`, none without any; [Memory](memory.md#model-notes)), the prompt profile and the model the server says it ran, and the
|
|
432
433
|
cost when the provider reports one (OpenRouter's `usage.cost`; no prices are
|
|
433
434
|
guessed). The speed line is the last generation's and the session's average
|
|
434
435
|
(weighted by tokens): llama.cpp reports it exactly (`87 tok/s out, 1.9k tok/s
|
|
@@ -708,7 +709,7 @@ In interactive assist mode, you can switch the request model without restarting:
|
|
|
708
709
|
- `/model host:model` or `/model host:alias`: qualified host routing (only `:` names a host; `openai/gpt-4o` is a model id). `host:alias` applies the alias; an alias whose target names another host is refused (`alias 'tiny' names host 'box', not 'openrouter'`). Aliases apply once: an alias pointing to another alias sends that name as written.
|
|
709
710
|
- `/model --default <name>`: set session model and persist as new default (`default.model`) in `config.yml` for future sessions (supports `host:model` full ref). It writes only the file: an exported `SAMAGOTCHI_DEFAULT_MODEL` (or `--model`) still wins over it.
|
|
710
711
|
- `/model <name> --alias <alias>`: create alias for current effective model (alias value may be bare or `host:model`).
|
|
711
|
-
- `/model`: show the effective model (and default when diverged: `runtime model: <effective> (default: <default>, profile=<name>, <source>)`, e.g. `profile=qwen36, server (chat_template)`).
|
|
712
|
+
- `/model`: show the effective model (and default when diverged: `runtime model: <effective> (default: <default>, profile=<name>, <source>)`, e.g. `profile=qwen36, server (chat_template)`). When the session's prompt carries model notes the line ends with `; notes: model_notes_deepseek (system, 612 chars)`; a switch's `runtime model set to …` names the new model's ([Memory](memory.md#model-notes)).
|
|
712
713
|
- `/model clear` (or `default`/`none`/`off`): clear the session override, reverting to the configured default.
|
|
713
714
|
- `/guardrails`: the guardrail rules (by source), what failed to load, and your stored approvals, numbered; `/guardrails revoke N` removes approval N (see [Guardrails](guardrails.md)).
|
|
714
715
|
- `/models`: list model ids aggregated across all `hosts:` (an alias shows next to its id: a bare alias on every host listing the id, a `host:model` one only under its host; grouped `host (host:port):` with per-host `unreachable` warnings, e.g. an unset `api_key_env`; lists cached 60s, 10 minutes for a remote host; lazy — no startup prefill). At most 20 ids per host, then `… and N more`; `/models <text>` lists every id containing `<text>` (any case), e.g. `/models qwen` on OpenRouter.
|
|
@@ -885,8 +886,9 @@ Behavior notes:
|
|
|
885
886
|
tool steps keeps them, as a cancel does, with the dim `partial progress kept; !rollback restores the pre-turn state`
|
|
886
887
|
(the model reads a note saying how many steps ran and why it stopped; `chi send --wait` and a delegating parent
|
|
887
888
|
read `the turn failed after N tool steps: <why>; its work so far stays`). The
|
|
888
|
-
REPL and attached mode say the same; the web
|
|
889
|
-
|
|
889
|
+
REPL and attached mode say the same; the web ends a failed turn whose steps stayed with `✕ turn failed: <summary>;
|
|
890
|
+
partial progress kept (!rollback restores the pre-turn state)`, after a reload too, and says `■ canceled (Ctrl-C)`
|
|
891
|
+
(`stopped` for its Stop button, `by a hook` for a hook's). A turn a bundle's plugin stopped names it: `■ turn stopped by loop-guard` (the web:
|
|
890
892
|
`■ stopped by loop-guard`).
|
|
891
893
|
- Visible text the canceled request had streamed stays in the conversation, marked `[interrupted]`, so the next
|
|
892
894
|
message (or a continue) picks up from the half-finished reply; the canceled request's thinking and any unfinished
|
data/docs/memory.md
CHANGED
|
@@ -88,6 +88,24 @@ Working habits for this model:
|
|
|
88
88
|
- **Timing**: a note takes effect at the next prompt build: a session's start,
|
|
89
89
|
a resume, a web worker waking, `/model`. A note written mid-session doesn't
|
|
90
90
|
change the running prompt.
|
|
91
|
+
- **Recorded and shown**: each prompt build writes the notes it carried to the
|
|
92
|
+
session file (`prompt_notes`: name, scope, size in characters and a short
|
|
93
|
+
digest of the text), so a session says later what it ran with; `/model`
|
|
94
|
+
switches record the new model's. `/stats` has a
|
|
95
|
+
`model notes: model_notes_deepseek (system, 612 chars)` line, `/model`
|
|
96
|
+
ends its model line with `; notes: …` (after a switch, the new model's), the
|
|
97
|
+
web's info bar a `notes: deepseek` chip (the full line in its tooltip), and
|
|
98
|
+
`chi self` a `model notes` row after `model key`: the notes a prompt of the
|
|
99
|
+
reported model loads now, the session's `--mute` list left out when it runs
|
|
100
|
+
in one, `none` without any. None of the others shows a line without notes.
|
|
101
|
+
A resumed session, or a web worker that woke, shows the saved ones until its
|
|
102
|
+
first turn builds the prompt again.
|
|
103
|
+
- **Judged**: `ruby script/model_notes_report.rb` (a development script, not
|
|
104
|
+
shipped) groups stored sessions by model and recorded notes and compares the
|
|
105
|
+
calls to the first edit and commit, commits per 100 steps, bare `&`, the
|
|
106
|
+
longest run without an edit and the Continues and steers each needed; run a
|
|
107
|
+
note on and muted for the same kind of task. See
|
|
108
|
+
[testing.md](testing.md) ("The model notes report").
|
|
91
109
|
- Without a model note the system prompt is what it was.
|
|
92
110
|
|
|
93
111
|
|
data/docs/sessions.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sessions
|
|
2
2
|
|
|
3
|
-
Sessions are plain files — no DB. Each session is `~/.local/state/samagotchi/sessions/<uuid>.json` (XDG-aware via `XDG_STATE_HOME`) plus a sidecar dir `<uuid>/` with `input/`/`notes/`/`output/`/`owner.lock`/`bridge.json` (`Session.session_dir`). Besides the messages and turn state, the JSON keeps `used_memory_names` (what the session read), `preloaded_memory_names` (`--memory`) and `muted_memory_names` (`--mute`), the last two written before the worker starts so a respawn builds the same prompt, and `parent_id`, the session that delegated this one (see *Delegating*; `null` otherwise); files from before those keys read as empty lists and no parent. `model_name` is the resolved model ref (an alias applied, `host:id` when it names a host), so a resumed session keeps its model when an alias is retargeted; `model_typed` is the name it was given as when that differs (an alias), for the `models:` lookup after a resume (`null` otherwise).
|
|
3
|
+
Sessions are plain files — no DB. Each session is `~/.local/state/samagotchi/sessions/<uuid>.json` (XDG-aware via `XDG_STATE_HOME`) plus a sidecar dir `<uuid>/` with `input/`/`notes/`/`output/`/`owner.lock`/`bridge.json` (`Session.session_dir`). Besides the messages and turn state, the JSON keeps `used_memory_names` (what the session read), `preloaded_memory_names` (`--memory`) and `muted_memory_names` (`--mute`), the last two written before the worker starts so a respawn builds the same prompt, `prompt_notes`, the model notes the system prompt carried at its last build (`[{name, scope, chars, digest}]`, see [Memory](memory.md#model-notes)), and `parent_id`, the session that delegated this one (see *Delegating*; `null` otherwise); files from before those keys read as empty lists and no parent. `model_name` is the resolved model ref (an alias applied, `host:id` when it names a host), so a resumed session keeps its model when an alias is retargeted; `model_typed` is the name it was given as when that differs (an alias), for the `models:` lookup after a resume (`null` otherwise).
|
|
4
4
|
|
|
5
5
|
**Retention (file-based, opt-out via env):**
|
|
6
6
|
|
|
@@ -21,7 +21,7 @@ A session is deleted if **expired by age OR overflow by count** (unless `keep_st
|
|
|
21
21
|
|
|
22
22
|
**Which chi a worker runs:** a new or woken worker runs the newest chi installed for this Ruby (`gem 'samagotchi', '>= 0.a'`, prereleases count), whoever starts it: a `chi web` or terminal still on an older chi starts it on the newest one, so a stale process doesn't spread old code. A worker already running keeps its version until it is restarted. Should the newest install fail to load, the worker runs the starter's own version and logs `boot_fallback` (its stdio is `/dev/null`). Under `bundle exec` the worker drops Bundler's env first, so it activates the newest gem too. Two consequences: a wrapper pin (`chi _0.17.0_ web`) pins only that process, not the workers it starts; and a source checkout (`bin/chi` in a worktree, `bundle exec`) starts its workers on its own `lib/`, but a session it made that is later woken by an installed chi (`chi web`, `chi send`) runs the installed code. The names an older starter hands a newer worker (the boot line, its env keys, `input/` files, the session JSON fields and the Bridge routes sessions use on each other) are pinned in `spec/worker_contract_spec.rb`.
|
|
23
23
|
|
|
24
|
-
**Stopping a worker:** `chi sessions stop ID` marks the session stopped (an empty `<id>/stopped` file, not a session.json field, which a worker saving at that moment would write over; a resume removes it), sends its worker TERM and waits (up to 10 s) until the worker has let go of `owner.lock`, so a `chi --resume ID` right after spawns a fresh worker. That is also how to restart a worker still running an older chi, which the attached terminal reports when a command or a question dismiss gets a 404, and the web when an answer, a dismiss, a command or a cancel does (`501 not_supported`; `BridgeClient.stale_worker_message`). The web's `POST /api/sessions/:id/stop` waits the same way, for 2 s.
|
|
24
|
+
**Stopping a worker:** `chi sessions stop ID` marks the session stopped (an empty `<id>/stopped` file, not a session.json field, which a worker saving at that moment would write over; a resume removes it), sends its worker TERM and waits (up to 10 s) until the worker has let go of `owner.lock`, so a `chi --resume ID` right after spawns a fresh worker. That is also how to restart a worker still running an older chi, which the attached terminal reports when a command or a question dismiss gets a 404, and the web when an answer, a dismiss, a command or a cancel does (`501 not_supported`; `BridgeClient.stale_worker_message`). A command this chi has that the older worker doesn't (`/llm-context`, the web's `llm ctx` chip, against a 0.39.0 worker) says the same, not `not a session command` (`SessionCommands.builtin_name`); a typo keeps its `400 unknown_command`. The web's `POST /api/sessions/:id/stop` waits the same way, for 2 s.
|
|
25
25
|
|
|
26
26
|
**Deleting a session:** `chi sessions delete ID...`, `/exit --delete` in a terminal and the Web UI's delete all go through `SessionManager.delete_session`: it resolves a unique prefix, removes `<id>.json` and the whole `<id>/` dir, and returns what it removed. A session a plain REPL has open (`owner.lock` kind `tui`) is always refused. One a worker runs is refused unless the caller asks to stop it: then it stops the worker as `chi sessions stop` does and deletes once `owner.lock` is free (`--force` on the CLI, 10 s; the web always, 2 s; `/exit --delete` after the worker agreed to exit, 10 s). A worker that outlives the wait leaves the session in place. The web's route is `DELETE /api/sessions/:id` (200 `{status: "deleted", session_id, stopped}`; 409 `owned_by_tui` or `still_stopping`; 404).
|
|
27
27
|
|
|
@@ -65,7 +65,7 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
|
|
|
65
65
|
**Ordering:**
|
|
66
66
|
|
|
67
67
|
- `Session.list` / `SessionManager.list_sessions` / `GET /api/sessions?sort=&order=&limit=&offset=` default to `updated_at desc` (newest activity first). Also supports `created_at`, `asc`. `X-Total-Count` header when paginated.
|
|
68
|
-
- Web UI (`chi web`): the page's scope is in its URL. Started in a git repo, `chi web` opens `/?dir=<that folder>`: that project's sessions, and new chats start in that folder; the header chip says `<project> · all`, and `all` opens the same place without `?dir` (every session; a new chat there starts in the server's own folder, shown on the start page, and cards name their folder; `← <project>` goes back to the project view it came from, or to the server's own project). One server serves every project: a second `chi web` (from another repo) finds it through `GET /api/info` and prints (with `--open`, opens) its page for its own folder instead of starting another. The 3 latest sessions sit above the chat (`hide ▴` puts them away behind a `▾ N sessions` pill, remembered; in a window under 900 px wide, where the strip scrolls sideways, `▴ hide` sits in the top bar where the pill comes back; a window under 700 px tall hides them by itself, so a question or approval card in the stage keeps room, and the pill shows them until the window is tall again). A parent and its delegates are one card there: its `▸ 2 delegates · 1 live · 1 waiting` chip (the waiting part in the attention colour, the card's border too) opens their rows in a popover under it (a row per delegate at any depth, indented; a click opens that one, Esc or a click outside closes it, and the strip's order holds while it is open). A fork is a card of its own, and so is a delegate whose parent isn't listed (archived, another project), with its `↳ <parent>` chip; the pill counts cards, and every live or waiting session. "All sessions" (or `/`) opens every session at `#/sessions`, with a search over preview, id and status (Esc or Back returns). There a parent's card holds its delegates the same way, the chip opening their rows inside the card (each with an archive button on hover), open by itself while one of them waits on you; the count says `14 (+7 delegates)`, a search that matches a delegate shows its parent's card with the list open and the matching rows marked, and select mode picks cards (archiving a parent takes its delegates). The open session is in the URL (`#/s/<id>`), so a reload or a copied link opens it again; the chi logo top left goes back to the empty start for a new chat. The message box grows with its text; drag its top edge to keep it taller (double-click resets). The info bar copies `chi --attach <id>` for a terminal. Its `llm ctx` chip names the LLM context strategy the next turn runs under (`llm ctx stale,forget · session`, in the accent colour when the session set its own; the tooltip gives each value's source), and its popover sets the session's own strategy, apply rule and budget, or follows the model again, by running `/llm-context` in the worker (the reply shows as a command bubble); `GET /api/sessions/:id`'s `session.llm_context` carries the values (the live worker's, else resolved from the session file), and a `command_ran` that changed them carries the new ones. It shows the context used (`ctx 42%`) and the last generation's decode speed (`87 tok/s`, `~64 tok/s` when estimated; see `/stats` in [cli.md](cli.md#session-commands)); hovering `ctx` there or on a session card lists the session's tokens in (summed over every request, with the cached share), out (with reasoning) and the cost when the provider reports one, for this session only, and the memory indexes its prompt holds (`memory index: ~3.4k tokens in this session's prompt (system 2.0k, project 1.4k)`; see [Memory](memory.md#the-indexs-size)). The start page's model picker (bottom left of the composer, the chosen name `▾`) chooses the model a new chat starts on; it opens a search list over the conversation (↓ or a click; downward when the start page leaves no room above): an empty search shows the last 5 picks (Recent) and then each host's model ids A–Z under the host, the default host first; typed words must each match the host or the id (a substring, else for 3+ characters the letters in order, so `deepseek4.1 fla` finds `openrouter · deepseek/deepseek-v4.1-flash`), ⏎ picks, Esc/Tab close, a pick puts the focus in the message box: `GET /api/models` lists the hosts' models as chi spells them (`{default, models: [{name, host, id, sampling?}], warning?}`; `sampling` is the model's configured request parameters as `/model` words them, the row's tooltip in the list; bare for the default host, `host:model` for the others, from the same cached lists as `/models`, a bounded wait, a host that is down noted in `warning`), and `POST /api/sessions` takes `model` (blank means the default). The start page's first message goes as later ones do: `POST /api/sessions` with `idle: true` and `preview` (the message, which names the session until its turn is saved), then `POST /api/sessions/:id/turn`, so a failed first turn puts its prompt back in the composer too; a first line that is one of the new session's commands (`/model …`) goes to `POST /api/sessions/:id/command` instead, as later ones do. The browser remembers the last choice; a running session's model is in the info bar and changes only with `/model`. In the composer a line is a command when it is `!…` or starts with one of the session's command names (its `commands` list; `/stats`, `/exit` and the other terminal words get the page's own reply); an unknown `/word` (`/usr/bin/env is missing`) goes to the model as a prompt, as in a terminal; a one-word `/word` no command answers (`/modle`) is not sent: the page shows `Unknown command /modle. Did you mean /model? /help lists the commands.` (the "Did you mean" part only when a name is close). A `POST /turn` (or a session's first `prompt`) that is one of the worker's commands runs as that command too (`202 {status, command_id, session_id}`), so `chi send`, `chi -p` and a page whose command list lags all get the command. ↑/↓ in the composer walk the prompt history the TUI's ↑ walks ([Persistent Prompt History](cli.md#persistent-prompt-history)): ↑ with the caret on the first line, ↓ from the last line while browsing, ↓ past the newest brings the draft back; `GET /api/history` returns `{entries: [...]}`, oldest first (the page fetches it on focus and after each send). The server records a sent line in the routes once the worker took it (`POST /turn` accepted, a `!command` through `POST /command` or as a session's first line); `"history": false` in the body keeps it out (an image-only send).
|
|
68
|
+
- Web UI (`chi web`): the page's scope is in its URL. Started in a git repo, `chi web` opens `/?dir=<that folder>`: that project's sessions, and new chats start in that folder; the header chip says `<project> · all`, and `all` opens the same place without `?dir` (every session; a new chat there starts in the server's own folder, shown on the start page, and cards name their folder; `← <project>` goes back to the project view it came from, or to the server's own project). One server serves every project: a second `chi web` (from another repo) finds it through `GET /api/info` and prints (with `--open`, opens) its page for its own folder instead of starting another. The 3 latest sessions sit above the chat (`hide ▴` puts them away behind a `▾ N sessions` pill, remembered; in a window under 900 px wide, where the strip scrolls sideways, `▴ hide` sits in the top bar where the pill comes back; a window under 700 px tall hides them by itself, so a question or approval card in the stage keeps room, and the pill shows them until the window is tall again). A parent and its delegates are one card there: its `▸ 2 delegates · 1 live · 1 waiting` chip (the waiting part in the attention colour, the card's border too) opens their rows in a popover under it (a row per delegate at any depth, indented; a click opens that one, Esc or a click outside closes it, and the strip's order holds while it is open). A fork is a card of its own, and so is a delegate whose parent isn't listed (archived, another project), with its `↳ <parent>` chip; the pill counts cards, and every live or waiting session. "All sessions" (or `/`) opens every session at `#/sessions`, with a search over preview, id and status (Esc or Back returns). There a parent's card holds its delegates the same way, the chip opening their rows inside the card (each with an archive button on hover), open by itself while one of them waits on you; the count says `14 (+7 delegates)`, a search that matches a delegate shows its parent's card with the list open and the matching rows marked, and select mode picks cards (archiving a parent takes its delegates). The open session is in the URL (`#/s/<id>`), so a reload or a copied link opens it again; the chi logo top left goes back to the empty start for a new chat. The message box grows with its text; drag its top edge to keep it taller (double-click resets). The info bar copies `chi --attach <id>` for a terminal. A session whose prompt carried model notes has a `notes: deepseek` chip there (the names without `model_notes_`; the tooltip lists each with its scope and size; `session.prompt_notes` in `GET /api/sessions/:id`, the live worker's, else the session file's; see [Memory](memory.md#model-notes)). Its `llm ctx` chip names the LLM context strategy the next turn runs under (`llm ctx stale,forget · session`, in the accent colour when the session set its own; the tooltip gives each value's source), and its popover sets the session's own strategy, apply rule and budget, or follows the model again, by running `/llm-context` in the worker (the reply shows as a command bubble); `GET /api/sessions/:id`'s `session.llm_context` carries the values (the live worker's, else resolved from the session file), and a `command_ran` that changed them carries the new ones. It shows the context used (`ctx 42%`) and the last generation's decode speed (`87 tok/s`, `~64 tok/s` when estimated; see `/stats` in [cli.md](cli.md#session-commands)); hovering `ctx` there or on a session card lists the session's tokens in (summed over every request, with the cached share), out (with reasoning) and the cost when the provider reports one, for this session only, and the memory indexes its prompt holds (`memory index: ~3.4k tokens in this session's prompt (system 2.0k, project 1.4k)`; see [Memory](memory.md#the-indexs-size)). The start page's model picker (bottom left of the composer, the chosen name `▾`) chooses the model a new chat starts on; it opens a search list over the conversation (↓ or a click; downward when the start page leaves no room above): an empty search shows the last 5 picks (Recent) and then each host's model ids A–Z under the host, the default host first; typed words must each match the host or the id (a substring, else for 3+ characters the letters in order, so `deepseek4.1 fla` finds `openrouter · deepseek/deepseek-v4.1-flash`), ⏎ picks, Esc/Tab close, a pick puts the focus in the message box: `GET /api/models` lists the hosts' models as chi spells them (`{default, models: [{name, host, id, sampling?}], warning?}`; `sampling` is the model's configured request parameters as `/model` words them, the row's tooltip in the list; bare for the default host, `host:model` for the others, from the same cached lists as `/models`, a bounded wait, a host that is down noted in `warning`), and `POST /api/sessions` takes `model` (blank means the default). The start page's first message goes as later ones do: `POST /api/sessions` with `idle: true` and `preview` (the message, which names the session until its turn is saved), then `POST /api/sessions/:id/turn`, so a failed first turn puts its prompt back in the composer too; a first line that is one of the new session's commands (`/model …`) goes to `POST /api/sessions/:id/command` instead, as later ones do. The browser remembers the last choice; a running session's model is in the info bar and changes only with `/model`. In the composer a line is a command when it is `!…` or starts with one of the session's command names (its `commands` list; `/stats`, `/exit` and the other terminal words get the page's own reply); an unknown `/word` (`/usr/bin/env is missing`) goes to the model as a prompt, as in a terminal; a one-word `/word` no command answers (`/modle`) is not sent: the page shows `Unknown command /modle. Did you mean /model? /help lists the commands.` (the "Did you mean" part only when a name is close). A `POST /turn` (or a session's first `prompt`) that is one of the worker's commands runs as that command too (`202 {status, command_id, session_id}`), so `chi send`, `chi -p` and a page whose command list lags all get the command. ↑/↓ in the composer walk the prompt history the TUI's ↑ walks ([Persistent Prompt History](cli.md#persistent-prompt-history)): ↑ with the caret on the first line, ↓ from the last line while browsing, ↓ past the newest brings the draft back; `GET /api/history` returns `{entries: [...]}`, oldest first (the page fetches it on focus and after each send). The server records a sent line in the routes once the worker took it (`POST /turn` accepted, a `!command` through `POST /command` or as a session's first line); `"history": false` in the body keeps it out (an image-only send).
|
|
69
69
|
- Web notifications: when a session needs you while the tab is hidden or behind another window (a question or a guardrail approval, a plugin card with buttons during a turn such as check-in's, a failed turn, a turn done after 10 s or more; a delegated session only for its questions, a canceled turn never), the page title counts it, `(N) Chi`, until you come back to the tab. The bell at the right of the top bar also turns on OS notifications ("needs an answer", "needs approval", "needs you", "turn failed", "done in 42 s", under the session's first message; a click opens the session): its first click asks the browser for the permission, and the choice is kept per browser. Any session of the page's scope counts, not only the open one. What was already so when the page loaded never counts, and several chi tabs show one notification per event. A chi tab in front tells the others (a `BroadcastChannel`), so while you look at one, the ones behind neither notify nor count what it shows; coming back to a tab clears those events in the other tabs' titles too. The hub's `session` frames carry what this needs: `pending_question` (`{id, kind}`, kind `question` or `approval`), `pending_card` (`{id, bundle}`: the running turn's card with actions, from `pending_card.json` that the worker keeps in the session's folder while one is open; a card without actions, such as loop-guard's, never counts) and `last_turn` (`{outcome, ended_at, seconds, origin}`, saved in the session's JSON as the engine ends each turn).
|
|
70
70
|
- The web frontend is a zero-build ES-module stack in `lib/samagotchi/web/public/`: `data.js` (retrieval, typed SSE `openStream` for a session and `openEvents` for the session list), `sessions_list.js` (the list as a pure reducer over `GET /api/events`: a snapshot replaces it, an upsert keeps a known card in place, a removal drops it; `families()` groups delegates under their parent), `families_view.js` (a parent's family chip, its rows and the strip's popover), `notify.js` (which of those events needs the user, for the notifications), `app.js` (presentation/state; a session with no live stream gets one when its `session` event says `bridge_up`, after one re-read: `event_seq` starts over in each worker, so a dropped stream is never resumed with its old cursor), `format.js` (pure formatters such as `previewOf`), `prompt_history.js` (the composer's ↑/↓ over the shared prompt history). Unit-tested via `npm test` (`node --test spec/web/public/*.test.js`).
|
|
71
71
|
- Selecting a session in the web UI is read-only: `GET /api/sessions/:id` never spawns a worker (it reads a live worker's snapshot when one runs, else the session file). A prompt (`POST /turn`) or a command (`POST /command`) wakes the worker, and `/stream` briefly waits for a freshly-spawned bridge before answering. A caught-up SSE reconnect holds the stream open; `reset` markers are only sent for reconnects behind the ring window, or with a cursor from another worker (event ids are `<event_seq>-<epoch>`, one epoch per worker).
|
|
@@ -198,8 +198,8 @@ The `delegate` tool hands a task to a **child session**: an ordinary chi session
|
|
|
198
198
|
- **Delegate reports: a child's reply comes back by itself.** When a child ends a turn that was its parent's (its task, a follow-up, a continue the parent answered, a reminder: not a turn its user typed into it directly), it rings the parent: a small file in the parent's `children/` dir, written after the child's turn is saved. So does a question the parent's model decides (the child's own `ask_user_question`, the step-limit continue; not an approval or a hook's question, which the user answers on the child's card), the turn after such a question whoever starts it, and the child's worker crashing. The parent reads the child as `delegate_result` would and hands its model a **report**: the same text (`session:`, `status:`, the reply), under its own header (`[Delegate report, delivered by chi …; not your user's message. Tell your user what it found …]`), as an input message with `source: "delegate_report"`.
|
|
199
199
|
- Parent mid-turn: the report joins the turn at its next iteration boundary (a "delegate report" bubble in the web, `delegate report> ab12cd34 answered: …` in the attached terminal).
|
|
200
200
|
- Parent idle: it runs a turn for the report (a *wake turn*, `last_turn.origin: "delegate_report"`) within the worker's 5 s tick, or wakes a worker for it when the parent's has idle-exited. Several reports that wait together are one turn and one message. A stopped, archived or scratch parent, or one open in a `--no-shared` REPL, is not woken: the rings wait on disk and join the next turn it runs (the user's message first). A worker just started (`chi send` to a stopped parent) runs no wake turn in its first 2 s, so the message it was started for goes first.
|
|
201
|
-
- The parent's cursor per child (the newest reply it was given and the child as it was then) is in its `delegates.json`, shared with `delegate_result` and `wait: true`: a reply the model already had is never reported again, also after the parent's worker restarts. A ring is deleted only when the turn that took its report
|
|
202
|
-
- Limits: `session.max_wakes` (default `10`) wake turns in a row with no human input; past it the reports wait for the next message and the UIs show `N delegate reports waiting; they join your next message`. A wake turn that fails pauses wakes until the next human input; its reports' rings stay, so they join that input's turn, and
|
|
201
|
+
- The parent's cursor per child (the newest reply it was given and the child as it was then) is in its `delegates.json`, shared with `delegate_result` and `wait: true`: a reply the model already had is never reported again, also after the parent's worker restarts. A ring is deleted only when the turn that took its report stays in the conversation (completed, canceled, or failed but kept: one that got to tool steps, or a reminder or continue turn, which keep what they read); a turn rolled back (a failed one that got nowhere, a canceled continue turn) leaves it for the next one (and the report isn't handed back to the composer). So the model reads each report once, unless a later `!rollback` erases a kept turn (what it read goes with it).
|
|
202
|
+
- Limits: `session.max_wakes` (default `10`) wake turns in a row with no human input; past it the reports wait for the next message and the UIs show `N delegate reports waiting; they join your next message`. A wake turn that fails, or is stopped before the model call (a Stop, a `before_turn` hook's cancel), pauses wakes until the next human input; its reports' rings stay, so they join that input's turn, and a failure's note tells the model so. A continue offer waiting in the parent takes the reports into its continue turn instead.
|
|
203
203
|
- `session.delegate_reports`: `wake` (default), `queue` (no wake turns: reports join the parent's next turn) or `off` (no rings; only `delegate_result` asks, as before). Env `SAMAGOTCHI_SESSION_DELEGATE_REPORTS`, `SAMAGOTCHI_SESSION_MAX_WAKES`.
|
|
204
204
|
- The web's bell rings at a wake turn's end however short (nobody started it, so nobody watched it). The text a model writes about a report mid-turn, before its next tool call, stays visible after the report's bubble once the turn ends.
|
|
205
205
|
- Every child starts with the shipped `delegated` system memory preloaded (`preloaded_memory_names: ["system/delegated"]`; `--mute delegated` would hide it): put everything in the final reply, don't delegate further, don't write memories or change config, stay in the folder. Its system prompt also says `Delegated by session <parent id>`. The parent's own `--mute` list does not carry over.
|
data/docs/testing.md
CHANGED
|
@@ -71,3 +71,19 @@ strategies (freed tokens, wrongly forgotten outputs, one-step re-reads, re-prefi
|
|
|
71
71
|
model's answers to chi's offer, with its call rate and notes). It is opt-in: the suite
|
|
72
72
|
runs its code only on small synthetic sessions, never on yours. See
|
|
73
73
|
[internals/llm-context-bench.md](internals/llm-context-bench.md).
|
|
74
|
+
|
|
75
|
+
## The model notes report
|
|
76
|
+
|
|
77
|
+
`ruby script/model_notes_report.rb` reads a sessions folder (`--sessions DIR`, else chi's own) and groups the
|
|
78
|
+
sessions by model and by the model notes their prompt carried (the session file's `prompt_notes`, as
|
|
79
|
+
`name@digest`; `none` without notes, or in a file from before they were recorded). For each session and group it
|
|
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
|
+
first commit (an `execute` call that runs `git [options] commit`) among the tool calls, commits per 100 steps (model
|
|
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` that had no prompt of their own), steers (lines that reached a
|
|
84
|
+
running turn from the user, `chi send` or a parent, and a Continue's text), follow-up prompts and plugin nudges. A
|
|
85
|
+
group has the medians of the places and runs, commits per 100 steps pooled and the counts summed. `--help` defines
|
|
86
|
+
each column; `--model GLOB`, `--since DATE` and `--min-steps N` filter, `--json` gives JSON. It reads only and prints
|
|
87
|
+
no session text (ids, model names, note names and digests, dates, numbers), and the suite runs it only on small
|
|
88
|
+
synthetic sessions. It is how a note is judged on and off (`chi --mute model_notes_<name>`) for the same kind of
|
|
89
|
+
task: see [memory.md](memory.md) ("Model notes").
|
|
@@ -110,9 +110,13 @@ module Samagotchi
|
|
|
110
110
|
# thread, the drain inside them).
|
|
111
111
|
#
|
|
112
112
|
# A turn takes reports (#take, at each boundary) and the worker settles
|
|
113
|
-
# them as the turn ends: #commit when
|
|
114
|
-
#
|
|
115
|
-
#
|
|
113
|
+
# them as the turn ends: #commit when the turn stays in the conversation
|
|
114
|
+
# (completed, canceled, or failed but kept: its work stayed, or a reminder
|
|
115
|
+
# or continue turn's): the cursors move on (DelegateCursors) and the rings
|
|
116
|
+
# go; #release when its end rolled it back (a failed turn that got
|
|
117
|
+
# nowhere, a canceled continue turn): the next turn reads the same rings
|
|
118
|
+
# again. So the model gets each report once (a later !rollback that
|
|
119
|
+
# erases a kept turn drops what it read, as before).
|
|
116
120
|
class ChildReports
|
|
117
121
|
SOURCE = Steer::DELEGATE_REPORT
|
|
118
122
|
|
|
@@ -178,7 +182,7 @@ module Samagotchi
|
|
|
178
182
|
@taken = {}
|
|
179
183
|
end
|
|
180
184
|
|
|
181
|
-
# The turn
|
|
185
|
+
# The turn was rolled back: its rings stay for the next one.
|
|
182
186
|
def release
|
|
183
187
|
@taken = {}
|
|
184
188
|
end
|
|
@@ -75,7 +75,9 @@ module Samagotchi
|
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
# @param argv [Array<String>] the arguments after "context"
|
|
78
|
-
|
|
78
|
+
# @param clock [#call] now, for ls's ages ("4s ago")
|
|
79
|
+
def initialize(argv, stdin: $stdin, stdout: $stdout, stderr: $stderr, state_dir: nil, env: ENV, cwd: Dir.pwd,
|
|
80
|
+
clock: -> { Time.now })
|
|
79
81
|
@argv = argv.dup
|
|
80
82
|
@stdin = stdin
|
|
81
83
|
@stdout = stdout
|
|
@@ -83,6 +85,7 @@ module Samagotchi
|
|
|
83
85
|
@state_dir = state_dir || Session.default_state_dir
|
|
84
86
|
@env = env
|
|
85
87
|
@cwd = cwd
|
|
88
|
+
@clock = clock
|
|
86
89
|
end
|
|
87
90
|
|
|
88
91
|
# @return [Integer] exit status: 0 done, 1 refused or failed, 2 usage
|
|
@@ -437,7 +440,7 @@ module Samagotchi
|
|
|
437
440
|
def age(iso)
|
|
438
441
|
return "-" unless iso
|
|
439
442
|
|
|
440
|
-
seconds = (
|
|
443
|
+
seconds = (@clock.call - Time.iso8601(iso)).to_i
|
|
441
444
|
if seconds < 60 then "#{seconds}s ago"
|
|
442
445
|
elsif seconds < 3600 then "#{seconds / 60}m ago"
|
|
443
446
|
elsif seconds < 86_400 then "#{seconds / 3600}h ago"
|
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -54,6 +54,7 @@ require_relative "engine_peers"
|
|
|
54
54
|
require_relative "bundle_needs"
|
|
55
55
|
require_relative "model_overlay"
|
|
56
56
|
require_relative "model_notes"
|
|
57
|
+
require_relative "prompt_note"
|
|
57
58
|
require_relative "served_model"
|
|
58
59
|
require_relative "image_store"
|
|
59
60
|
require_relative "vision_context"
|
|
@@ -478,6 +479,26 @@ module Samagotchi
|
|
|
478
479
|
muted: @muted_memory_names)
|
|
479
480
|
end
|
|
480
481
|
|
|
482
|
+
# The model notes the session's prompt carried at its last build
|
|
483
|
+
# (Session#prompt_notes): a resumed session's or a woken worker's saved
|
|
484
|
+
# ones until this process builds its prompt; without a session, the
|
|
485
|
+
# notes the effective model's prompt would load.
|
|
486
|
+
# @return [Array<PromptNote>]
|
|
487
|
+
def prompt_notes
|
|
488
|
+
return @session.prompt_notes.dup if @session
|
|
489
|
+
|
|
490
|
+
model_notes.map { |note| PromptNote.from_note(note) }
|
|
491
|
+
rescue StandardError
|
|
492
|
+
[]
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
# Records +notes+ (ModelNotes::Note) on the session as the ones its
|
|
496
|
+
# prompt carries; saved with it.
|
|
497
|
+
def record_prompt_notes(notes)
|
|
498
|
+
@session.prompt_notes = Array(notes).map { |note| PromptNote.from_note(note) } if @session && notes
|
|
499
|
+
end
|
|
500
|
+
private :record_prompt_notes
|
|
501
|
+
|
|
481
502
|
# +name+'s resolved ref (ModelRef#ref).
|
|
482
503
|
def model_ref_for(name)
|
|
483
504
|
@host_registry.model_ref(name).ref
|
|
@@ -856,6 +877,9 @@ module Samagotchi
|
|
|
856
877
|
@profile_resolution = nil
|
|
857
878
|
sync_model_key!
|
|
858
879
|
@prompt_builder.reset!
|
|
880
|
+
# The notes the new model's prompt loads at its next build, so /model,
|
|
881
|
+
# /stats and the web name them before the next turn does.
|
|
882
|
+
record_prompt_notes(model_notes) if @session
|
|
859
883
|
sync_kernel_client!
|
|
860
884
|
@client.invalidate_context_window!
|
|
861
885
|
@metrics.forget_model_reports!
|
|
@@ -921,7 +945,8 @@ module Samagotchi
|
|
|
921
945
|
snapshot = @metrics.snapshot
|
|
922
946
|
target = @host_registry.resolve(@effective_model_name)
|
|
923
947
|
served, served_for = served_model_for(snapshot, target: target)
|
|
924
|
-
snapshot = snapshot.merge(served_model: served, served_model_for: served_for, llm_context: llm_context_summary
|
|
948
|
+
snapshot = snapshot.merge(served_model: served, served_model_for: served_for, llm_context: llm_context_summary,
|
|
949
|
+
prompt_notes: prompt_notes.map(&:to_h))
|
|
925
950
|
unless snapshot.dig(:context, :window_tokens)
|
|
926
951
|
window = current_context_window(target)
|
|
927
952
|
if window
|
|
@@ -1028,6 +1053,8 @@ module Samagotchi
|
|
|
1028
1053
|
# :metrics [Hash] @metrics.snapshot (per-session analytics)
|
|
1029
1054
|
# :pending_question [Hash, nil] current pending structured question
|
|
1030
1055
|
# :used_memory_names [Array<String>] deduped memory names active this session
|
|
1056
|
+
# :prompt_notes [Array<Hash>] the model notes the prompt carried
|
|
1057
|
+
# ({name:, scope:, chars:, digest:}; #prompt_notes)
|
|
1031
1058
|
# :parent_id [String, nil] the session that delegated this one
|
|
1032
1059
|
# :model_name [String] the model turns run on now (after /model)
|
|
1033
1060
|
# :served_model, :served_model_for [String, nil] what a generation of
|
|
@@ -1048,6 +1075,7 @@ module Samagotchi
|
|
|
1048
1075
|
used_memory_names: @used_memories.names,
|
|
1049
1076
|
preloaded_memory_names: preloaded_memory_names,
|
|
1050
1077
|
muted_memory_names: @muted_memory_names.dup,
|
|
1078
|
+
prompt_notes: prompt_notes.map(&:to_h),
|
|
1051
1079
|
parent_id: @session&.parent_id,
|
|
1052
1080
|
model_name: effective_model_ref,
|
|
1053
1081
|
served_model: served_pair[0],
|
|
@@ -1462,6 +1490,8 @@ module Samagotchi
|
|
|
1462
1490
|
prompt = @prompt_builder.build(chat: chat, thinking: level, layers: llm_context_layers(target))
|
|
1463
1491
|
# The memory indexes this prompt holds: /stats, the snapshot, analytics.json.
|
|
1464
1492
|
@metrics.memory_index = @prompt_builder.memory_index
|
|
1493
|
+
# The model notes it carries: the session records them (/stats, /model, the web).
|
|
1494
|
+
record_prompt_notes(@prompt_builder.prompt_notes)
|
|
1465
1495
|
prompt
|
|
1466
1496
|
end
|
|
1467
1497
|
|
|
@@ -49,21 +49,24 @@ module Samagotchi
|
|
|
49
49
|
# @param muted [Array<String>] the session's muted names (MutedMemories)
|
|
50
50
|
# @param small [#call, nil] → whether the model is a small one; nil:
|
|
51
51
|
# guardrails.small_models (Guardrails::ModelSize)
|
|
52
|
+
# @param env [Hash] the XDG env the memories dirs resolve from
|
|
53
|
+
# @param warn [Boolean] false: no size warnings (a read-only report such
|
|
54
|
+
# as chi self; the prompt's build gives them)
|
|
52
55
|
# @return [Array<Note>]
|
|
53
|
-
def for(name:, key:, fallback_key: nil, muted: [], small: nil)
|
|
56
|
+
def for(name:, key:, fallback_key: nil, muted: [], small: nil, env: ENV, warn: true)
|
|
54
57
|
return [] if name.nil? || name.to_s.strip.empty?
|
|
55
58
|
|
|
56
59
|
small ||= -> { Guardrails::ModelSize.small?(name, key) }
|
|
57
60
|
small_once = memo(small)
|
|
58
61
|
seen = Set.new
|
|
59
62
|
notes = SCOPES.flat_map do |scope|
|
|
60
|
-
files(scope).filter_map do |path|
|
|
63
|
+
files(scope, env).filter_map do |path|
|
|
61
64
|
next unless seen.add?(File.expand_path(path))
|
|
62
65
|
|
|
63
66
|
note_at(path, scope, name: name, key: key, fallback_key: fallback_key, muted: muted, small: small_once)
|
|
64
67
|
end
|
|
65
68
|
end
|
|
66
|
-
warn_sizes(notes)
|
|
69
|
+
warn_sizes(notes) if warn
|
|
67
70
|
notes
|
|
68
71
|
end
|
|
69
72
|
|
|
@@ -96,7 +99,7 @@ module Samagotchi
|
|
|
96
99
|
@warn_mutex.synchronize { @warned.clear }
|
|
97
100
|
end
|
|
98
101
|
|
|
99
|
-
def files(scope) = files_in(MemoryPaths.scope_dir(scope))
|
|
102
|
+
def files(scope, env = ENV) = files_in(MemoryPaths.scope_dir(scope, env: env))
|
|
100
103
|
|
|
101
104
|
# The `model_notes_*.md` files in +dir+, by their names on disk: the
|
|
102
105
|
# prefix exactly as written (a case-insensitive disk's glob matches
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
# A model note a session's system prompt carried (ModelNotes::Note
|
|
5
|
+
# without its body): what the session file's `prompt_notes` records at
|
|
6
|
+
# each prompt build, so /stats, /model, the web and a later comparison
|
|
7
|
+
# read what the prompt really held. name: the memory name; scope:
|
|
8
|
+
# "system" or "project"; chars: the body's length in the prompt; digest:
|
|
9
|
+
# a short SHA-256 of that body (ModelNotes::Note#digest).
|
|
10
|
+
PromptNote = Data.define(:name, :scope, :chars, :digest) do
|
|
11
|
+
# @param note [ModelNotes::Note]
|
|
12
|
+
def self.from_note(note)
|
|
13
|
+
new(name: note.name, scope: note.scope, chars: note.chars, digest: note.digest)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# A saved or wire record (string or symbol keys); nil without a name.
|
|
17
|
+
def self.from_h(raw)
|
|
18
|
+
return raw if raw.is_a?(PromptNote)
|
|
19
|
+
return nil unless raw.is_a?(Hash)
|
|
20
|
+
|
|
21
|
+
get = ->(key) { raw.key?(key.to_s) ? raw[key.to_s] : raw[key] }
|
|
22
|
+
name = get.call(:name).to_s.strip
|
|
23
|
+
return nil if name.empty?
|
|
24
|
+
|
|
25
|
+
chars = get.call(:chars)
|
|
26
|
+
new(name: name, scope: get.call(:scope)&.to_s, chars: chars.is_a?(Numeric) ? chars.to_i : nil,
|
|
27
|
+
digest: get.call(:digest)&.to_s)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The records of a saved list (others dropped).
|
|
31
|
+
# @return [Array<PromptNote>]
|
|
32
|
+
def self.list(raw)
|
|
33
|
+
Array(raw).filter_map { |entry| from_h(entry) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# "model_notes_deepseek (system, 612 chars), model_notes_x (project,
|
|
37
|
+
# 80 chars)": the line /model, /stats and chi self show; "" without
|
|
38
|
+
# notes. As format.js promptNotesText (spec/shared/labels_matrix.json).
|
|
39
|
+
# @param notes [Array<PromptNote, Hash>]
|
|
40
|
+
def self.text(notes)
|
|
41
|
+
list(notes).map(&:label).join(", ")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# "model_notes_deepseek (system, 612 chars)".
|
|
45
|
+
def label
|
|
46
|
+
details = [scope, chars && "#{chars} chars"].compact.reject { |part| part.to_s.empty? }
|
|
47
|
+
details.empty? ? name : "#{name} (#{details.join(", ")})"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The session file's record (string keys).
|
|
51
|
+
def to_file = to_h.transform_keys(&:to_s)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -10,6 +10,8 @@ require_relative "session"
|
|
|
10
10
|
require_relative "log_path"
|
|
11
11
|
require_relative "model_profile"
|
|
12
12
|
require_relative "model_overlay"
|
|
13
|
+
require_relative "model_notes"
|
|
14
|
+
require_relative "prompt_note"
|
|
13
15
|
require_relative "thinking"
|
|
14
16
|
require_relative "served_model"
|
|
15
17
|
require_relative "host_registry"
|
|
@@ -59,6 +61,7 @@ module Samagotchi
|
|
|
59
61
|
["log", log_path(env)],
|
|
60
62
|
["model", model ? model_label(model, default, env) : "(not configured)"],
|
|
61
63
|
["model key", model ? model_key_for(model, env) : "-"],
|
|
64
|
+
["model notes", model ? model_notes_for(model, env) : "-"],
|
|
62
65
|
["host", model ? host_for(model, env) : "-"],
|
|
63
66
|
["api key", model ? api_key_for(model, env) : "-"],
|
|
64
67
|
["loop", model ? loop_for(model, env) : "-"],
|
|
@@ -187,6 +190,35 @@ module Samagotchi
|
|
|
187
190
|
ModelOverlay.key_for(model) || "-"
|
|
188
191
|
end
|
|
189
192
|
|
|
193
|
+
# "model_notes_deepseek (system, 612 chars)": the model notes a prompt
|
|
194
|
+
# of +model+ loads (ModelNotes: matched on its bare id and key, the
|
|
195
|
+
# session's --mute list left out when chi self runs in one), "none"
|
|
196
|
+
# without any. Read from the memories dirs now: a session's prompt
|
|
197
|
+
# takes them at its next build (/stats names the ones it carries).
|
|
198
|
+
def model_notes_for(model, env)
|
|
199
|
+
bare = HostRegistry.new(env: env).bare_name(model)
|
|
200
|
+
notes = ModelNotes.for(name: bare, key: ModelOverlay.key_for(bare), muted: session_muted(env), env: env,
|
|
201
|
+
warn: false)
|
|
202
|
+
text = PromptNote.text(notes.map { |note| PromptNote.from_note(note) })
|
|
203
|
+
text.empty? ? "none" : text
|
|
204
|
+
rescue StandardError => e
|
|
205
|
+
"(unreadable: #{e.message})"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# The --mute list of the session chi self runs in (its execute exports
|
|
209
|
+
# SAMAGOTCHI_PARENT_SESSION); [] outside one.
|
|
210
|
+
def session_muted(env)
|
|
211
|
+
id = env[PARENT_SESSION_ENV].to_s
|
|
212
|
+
return [] if id.empty? || !Session.valid_id?(id)
|
|
213
|
+
|
|
214
|
+
state_dir = Session.default_state_dir(env: env)
|
|
215
|
+
return [] unless Session.exist?(id, state_dir: state_dir)
|
|
216
|
+
|
|
217
|
+
MutedMemories.normalize_list(Session.load(id, state_dir: state_dir).muted_memory_names)
|
|
218
|
+
rescue StandardError
|
|
219
|
+
[]
|
|
220
|
+
end
|
|
221
|
+
|
|
190
222
|
def model_name
|
|
191
223
|
ModelProfile.required_model_name
|
|
192
224
|
rescue ArgumentError
|
data/lib/samagotchi/session.rb
CHANGED
|
@@ -7,6 +7,7 @@ require "time"
|
|
|
7
7
|
require_relative "atomic_file"
|
|
8
8
|
require_relative "context_note"
|
|
9
9
|
require_relative "llm_context_override"
|
|
10
|
+
require_relative "prompt_note"
|
|
10
11
|
|
|
11
12
|
require_relative "paths"
|
|
12
13
|
require_relative "project_scope"
|
|
@@ -44,6 +45,17 @@ module Samagotchi
|
|
|
44
45
|
# from it (--mute): the worker rebuilds the same prompt on a respawn.
|
|
45
46
|
# Names as given; the engine normalizes them.
|
|
46
47
|
attr_accessor :preloaded_memory_names, :muted_memory_names
|
|
48
|
+
# The model notes the session's system prompt carried at its last build
|
|
49
|
+
# (Array<PromptNote>: name, scope, chars, digest; ModelNotes), set by the
|
|
50
|
+
# Engine at each build (a start, a resume's or a woken worker's first
|
|
51
|
+
# turn, /model) and saved, so what a session ran with can be read later.
|
|
52
|
+
attr_reader :prompt_notes
|
|
53
|
+
|
|
54
|
+
# @param notes [Array<PromptNote, Hash>] a file's hashes are read
|
|
55
|
+
# (PromptNote.list)
|
|
56
|
+
def prompt_notes=(notes)
|
|
57
|
+
@prompt_notes = PromptNote.list(notes)
|
|
58
|
+
end
|
|
47
59
|
# The name model_name was typed as when that differs from the resolved
|
|
48
60
|
# ref stored in model_name (an alias: `small` for `gemma-small`), else
|
|
49
61
|
# nil. The models: lookup takes it (HostRegistry#lookup_names).
|
|
@@ -118,7 +130,7 @@ module Samagotchi
|
|
|
118
130
|
first_preview: "", test_run: false, pending_question: nil,
|
|
119
131
|
used_memory_names: [], project_root: nil,
|
|
120
132
|
preloaded_memory_names: [], muted_memory_names: [], parent_id: nil, scratch: false,
|
|
121
|
-
last_turn: nil, model_typed: nil, delegate: false, llm_context: nil)
|
|
133
|
+
last_turn: nil, model_typed: nil, delegate: false, llm_context: nil, prompt_notes: [])
|
|
122
134
|
@id = id
|
|
123
135
|
@metadata_version = metadata_version
|
|
124
136
|
@mode = mode
|
|
@@ -136,6 +148,7 @@ module Samagotchi
|
|
|
136
148
|
@used_memory_names = self.class.name_list(used_memory_names)
|
|
137
149
|
@preloaded_memory_names = self.class.name_list(preloaded_memory_names)
|
|
138
150
|
@muted_memory_names = self.class.name_list(muted_memory_names)
|
|
151
|
+
self.prompt_notes = prompt_notes
|
|
139
152
|
@project_root = project_root
|
|
140
153
|
@parent_id = parent_id&.to_s
|
|
141
154
|
@scratch = !!scratch
|
|
@@ -269,6 +282,7 @@ module Samagotchi
|
|
|
269
282
|
"project_root" => nil,
|
|
270
283
|
"preloaded_memory_names" => [],
|
|
271
284
|
"muted_memory_names" => [],
|
|
285
|
+
"prompt_notes" => [],
|
|
272
286
|
"parent_id" => nil,
|
|
273
287
|
"scratch" => false,
|
|
274
288
|
"last_turn" => nil,
|
|
@@ -393,6 +407,7 @@ module Samagotchi
|
|
|
393
407
|
when "test_run" then !!@test_run
|
|
394
408
|
when "delegate" then @delegate
|
|
395
409
|
when "llm_context" then llm_context_file
|
|
410
|
+
when "prompt_notes" then @prompt_notes.map(&:to_file)
|
|
396
411
|
when "used_memory_names", "preloaded_memory_names", "muted_memory_names"
|
|
397
412
|
Array(instance_variable_get(:"@#{key}"))
|
|
398
413
|
else instance_variable_get(:"@#{key}")
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "config"
|
|
4
4
|
require_relative "model_profile"
|
|
5
5
|
require_relative "served_model"
|
|
6
|
+
require_relative "prompt_note"
|
|
6
7
|
require_relative "turn_flow"
|
|
7
8
|
require_relative "tools/execute"
|
|
8
9
|
require_relative "tools/builtins"
|
|
@@ -110,6 +111,14 @@ module Samagotchi
|
|
|
110
111
|
@builtin_registry ||= register_builtins(Commands::Registry.new).freeze
|
|
111
112
|
end
|
|
112
113
|
|
|
114
|
+
# The name of this chi's own session command +line+ is (one a worker
|
|
115
|
+
# runs), or nil. A worker that answers such a line unknown_command runs
|
|
116
|
+
# an older chi that doesn't have it yet (/llm-context against 0.39.0):
|
|
117
|
+
# the UIs say so (BridgeClient.stale_worker_message) instead of "not a
|
|
118
|
+
# session command".
|
|
119
|
+
# @return [String, nil]
|
|
120
|
+
def self.builtin_name(line) = builtin_registry.lookup(line)&.name
|
|
121
|
+
|
|
113
122
|
# @return [String] the model /model clear goes back to
|
|
114
123
|
attr_reader :default_model
|
|
115
124
|
|
|
@@ -362,9 +371,9 @@ module Samagotchi
|
|
|
362
371
|
def model_command(input)
|
|
363
372
|
suffix = input.delete_prefix(MODEL_COMMAND).strip
|
|
364
373
|
if suffix.empty?
|
|
365
|
-
return ["runtime model: #{model_name}#{model_note}#{served_note}#{sampling_note}#{thinking_note}", false] if model_name == @engine.model_ref_for(@default_model)
|
|
374
|
+
return ["runtime model: #{model_name}#{model_note}#{served_note}#{sampling_note}#{thinking_note}#{notes_note}", false] if model_name == @engine.model_ref_for(@default_model)
|
|
366
375
|
|
|
367
|
-
return ["runtime model: #{model_name}#{model_note("default: #{@default_model}")}#{served_note}#{sampling_note}#{thinking_note}", false]
|
|
376
|
+
return ["runtime model: #{model_name}#{model_note("default: #{@default_model}")}#{served_note}#{sampling_note}#{thinking_note}#{notes_note}", false]
|
|
368
377
|
end
|
|
369
378
|
|
|
370
379
|
# Parse flags: --default and --alias <name> / --alias=<name> (tolerant order)
|
|
@@ -434,14 +443,14 @@ module Samagotchi
|
|
|
434
443
|
return ["--default cannot be combined with clear/default/none/off", false] if persist_default
|
|
435
444
|
|
|
436
445
|
switch_model(@default_model)
|
|
437
|
-
return ["runtime model reset to #{model_name}#{model_note}", true]
|
|
446
|
+
return ["runtime model reset to #{model_name}#{model_note}#{notes_note}", true]
|
|
438
447
|
end
|
|
439
448
|
|
|
440
449
|
switch_model(arg, persist_default: persist_default)
|
|
441
450
|
if persist_default
|
|
442
|
-
["runtime model set to #{model_name}#{model_note} and default updated", true]
|
|
451
|
+
["runtime model set to #{model_name}#{model_note} and default updated#{notes_note}", true]
|
|
443
452
|
else
|
|
444
|
-
["runtime model set to #{model_name}#{model_note}", true]
|
|
453
|
+
["runtime model set to #{model_name}#{model_note}#{notes_note}", true]
|
|
445
454
|
end
|
|
446
455
|
end
|
|
447
456
|
end
|
|
@@ -482,6 +491,16 @@ module Samagotchi
|
|
|
482
491
|
""
|
|
483
492
|
end
|
|
484
493
|
|
|
494
|
+
# "; notes: model_notes_deepseek (system, 612 chars)": the model notes
|
|
495
|
+
# the session's prompt carries (after a switch, the ones the new
|
|
496
|
+
# model's prompt loads); "" without any.
|
|
497
|
+
def notes_note
|
|
498
|
+
text = PromptNote.text(@engine.prompt_notes)
|
|
499
|
+
text.empty? ? "" : "; notes: #{text}"
|
|
500
|
+
rescue StandardError
|
|
501
|
+
""
|
|
502
|
+
end
|
|
503
|
+
|
|
485
504
|
# "; thinking: off (models: qwen)" when the model has a level set.
|
|
486
505
|
def thinking_note
|
|
487
506
|
summary = @engine.thinking_summary
|
|
@@ -37,6 +37,8 @@ module Samagotchi
|
|
|
37
37
|
# The most turns (with their tool calls) a live snapshot carries: the
|
|
38
38
|
# unsaved ones of a collector whose saves keep failing stay bounded.
|
|
39
39
|
LIVE_RECORDS_CAP = 20
|
|
40
|
+
# The most characters of a failed turn's error message its record keeps.
|
|
41
|
+
FAILURE_MESSAGE_CHARS = 2000
|
|
40
42
|
|
|
41
43
|
# A generation shorter than this gets no decode speed of its own: one
|
|
42
44
|
# tiny generation would skew the session's average.
|
|
@@ -308,7 +310,7 @@ module Samagotchi
|
|
|
308
310
|
when :turn_canceled
|
|
309
311
|
end_turn(status: "canceled", reason: event[:cancellation_reason], by: event[:cancelled_by])
|
|
310
312
|
when :turn_failed
|
|
311
|
-
end_turn(status: "failed")
|
|
313
|
+
end_turn(status: "failed", failure: failure_fields(event))
|
|
312
314
|
end
|
|
313
315
|
|
|
314
316
|
@mutex.synchronize { @last_activity_at = now.iso8601(3) }
|
|
@@ -497,9 +499,10 @@ module Samagotchi
|
|
|
497
499
|
end
|
|
498
500
|
|
|
499
501
|
# A canceled turn's record keeps why (+reason+: "user", "ctrl_c", ...)
|
|
500
|
-
# and who stopped it (+by+: a hook's bundle),
|
|
501
|
-
#
|
|
502
|
-
|
|
502
|
+
# and who stopped it (+by+: a hook's bundle), a failed one what its line
|
|
503
|
+
# says (+failure+, #failure_fields), so a reloaded web history can show
|
|
504
|
+
# the cancel or failure line the live one did.
|
|
505
|
+
def end_turn(status: "completed", reason: nil, by: nil, failure: nil)
|
|
503
506
|
@mutex.synchronize do
|
|
504
507
|
if @turn
|
|
505
508
|
# A turn that failed mid-stream never saw its generation end:
|
|
@@ -522,6 +525,7 @@ module Samagotchi
|
|
|
522
525
|
}
|
|
523
526
|
record[:cancellation_reason] = reason.to_s unless reason.nil? || reason.to_s.empty?
|
|
524
527
|
record[:cancelled_by] = by.to_s unless by.nil? || by.to_s.empty?
|
|
528
|
+
record[:failure] = failure if failure
|
|
525
529
|
record.merge!(turn_token_fields(@turn))
|
|
526
530
|
@turn_records << record
|
|
527
531
|
count_turn(record)
|
|
@@ -530,6 +534,19 @@ module Samagotchi
|
|
|
530
534
|
end
|
|
531
535
|
end
|
|
532
536
|
|
|
537
|
+
# What turn_failed's line is made of (format.js failedTurnText): a
|
|
538
|
+
# provider error's summary, else the message (capped: a server's error
|
|
539
|
+
# page can be long) and class, and the steps that stayed (kept_steps).
|
|
540
|
+
def failure_fields(event)
|
|
541
|
+
summary = event[:summary].to_s
|
|
542
|
+
return { summary: summary, kept_steps: event[:kept_steps] }.compact unless summary.empty?
|
|
543
|
+
|
|
544
|
+
message = event[:message].to_s
|
|
545
|
+
message = "#{message[0, FAILURE_MESSAGE_CHARS - 1]}…" if message.length > FAILURE_MESSAGE_CHARS
|
|
546
|
+
{ message: message.empty? ? nil : message, error_class: event[:error_class]&.to_s,
|
|
547
|
+
kept_steps: event[:kept_steps] }.compact
|
|
548
|
+
end
|
|
549
|
+
|
|
533
550
|
# The model the server says answered, and the name asked for then (a
|
|
534
551
|
# /model switch makes it stale until the next generation reports).
|
|
535
552
|
def record_served_model(event)
|
|
@@ -80,13 +80,17 @@ module Samagotchi
|
|
|
80
80
|
key = [chat, thinking, layers]
|
|
81
81
|
@built ||= {}
|
|
82
82
|
@index_sizes ||= {}
|
|
83
|
+
@notes_loaded ||= {}
|
|
83
84
|
unless @built.key?(key)
|
|
85
|
+
@section_notes = nil
|
|
84
86
|
@built[key] = system_prompt_with_index(
|
|
85
87
|
assist_system_prompt(chat: chat, thinking: thinking, layers: layers), chat: chat, thinking: thinking, layers: layers
|
|
86
88
|
)
|
|
87
89
|
@index_sizes[key] = @measured_index
|
|
90
|
+
@notes_loaded[key] = @section_notes || []
|
|
88
91
|
end
|
|
89
92
|
@memory_index = @index_sizes[key]
|
|
93
|
+
@prompt_notes = @notes_loaded[key]
|
|
90
94
|
@built[key]
|
|
91
95
|
end
|
|
92
96
|
|
|
@@ -98,11 +102,19 @@ module Samagotchi
|
|
|
98
102
|
# @return [Hash, nil]
|
|
99
103
|
attr_reader :memory_index
|
|
100
104
|
|
|
105
|
+
# The model notes the prompt #build returned last carries
|
|
106
|
+
# (Array<ModelNotes::Note>, [] with none), nil before a build and after
|
|
107
|
+
# #reset!. Read when the prompt is built, as the section is.
|
|
108
|
+
# @return [Array<ModelNotes::Note>, nil]
|
|
109
|
+
attr_reader :prompt_notes
|
|
110
|
+
|
|
101
111
|
# Drops the built prompts: the next #build reads the profile, tools,
|
|
102
112
|
# indexes and memories again (a model or profile switch, changed tools).
|
|
103
113
|
def reset!
|
|
104
114
|
@built = nil
|
|
105
115
|
@index_sizes = nil
|
|
116
|
+
@notes_loaded = nil
|
|
117
|
+
@prompt_notes = nil
|
|
106
118
|
@stable_lengths = nil
|
|
107
119
|
end
|
|
108
120
|
|
|
@@ -323,6 +335,7 @@ module Samagotchi
|
|
|
323
335
|
# identity: a heading per scope, each note under its name, the models:
|
|
324
336
|
# line left out. nil without any, so the prompt stays as it was.
|
|
325
337
|
def model_notes_section
|
|
338
|
+
@section_notes = []
|
|
326
339
|
notes = Array(@model_notes_lookup.call)
|
|
327
340
|
return nil if notes.empty?
|
|
328
341
|
|
|
@@ -330,7 +343,7 @@ module Samagotchi
|
|
|
330
343
|
notes.group_by(&:scope).map do |scope, list|
|
|
331
344
|
heading = ref.to_s.empty? ? "Model notes (scope=#{scope}):" : "Model notes (for #{ref}, scope=#{scope}):"
|
|
332
345
|
[heading, *list.map { |note| "## #{note.name}\n#{note.body}" }].join("\n\n")
|
|
333
|
-
end.join("\n\n")
|
|
346
|
+
end.join("\n\n").tap { @section_notes = notes }
|
|
334
347
|
rescue StandardError
|
|
335
348
|
nil
|
|
336
349
|
end
|
|
@@ -522,6 +522,9 @@ module Samagotchi
|
|
|
522
522
|
return
|
|
523
523
|
end
|
|
524
524
|
return @screen.commit(stale_worker("run commands")) if reply.status == 404
|
|
525
|
+
|
|
526
|
+
name = older_worker_command(reply, line)
|
|
527
|
+
return @screen.commit(stale_worker("run #{name}")) if name
|
|
525
528
|
return @screen.commit("could not run the command (#{LATE})") if too_late?(reply)
|
|
526
529
|
|
|
527
530
|
detail = reply.json&.fetch("detail", nil) || reply.json&.fetch("error", nil)
|
|
@@ -828,6 +831,14 @@ module Samagotchi
|
|
|
828
831
|
# so it shows above what it got.
|
|
829
832
|
def echo_answer(text) = @screen.commit("#{paint(QUESTION_PROMPT, 33)}#{text}")
|
|
830
833
|
|
|
834
|
+
# The name of this chi's own command +line+ when the worker answered it
|
|
835
|
+
# unknown_command (it runs an older chi without it), else nil.
|
|
836
|
+
def older_worker_command(reply, line)
|
|
837
|
+
return nil unless reply.status == 400 && reply.json&.fetch("error", nil) == "unknown_command"
|
|
838
|
+
|
|
839
|
+
SessionCommands.builtin_name(line)
|
|
840
|
+
end
|
|
841
|
+
|
|
831
842
|
def stale_worker(cant)
|
|
832
843
|
BridgeClient.stale_worker_message(@client.session_id, cant: cant)
|
|
833
844
|
end
|
|
@@ -1002,6 +1013,7 @@ module Samagotchi
|
|
|
1002
1013
|
|
|
1003
1014
|
why =
|
|
1004
1015
|
if reply.status == 404 then stale_worker("run commands")
|
|
1016
|
+
elsif (name = older_worker_command(reply, line)) then stale_worker("run #{name}")
|
|
1005
1017
|
elsif too_late?(reply) then LATE
|
|
1006
1018
|
else "the worker answered #{reply.status}"
|
|
1007
1019
|
end
|
|
@@ -6,6 +6,7 @@ require_relative "../image_store"
|
|
|
6
6
|
require_relative "../turn_note"
|
|
7
7
|
require_relative "../steer"
|
|
8
8
|
require_relative "../memory_bundle/index_size"
|
|
9
|
+
require_relative "../prompt_note"
|
|
9
10
|
|
|
10
11
|
module Samagotchi
|
|
11
12
|
class TerminalUI
|
|
@@ -485,6 +486,9 @@ module Samagotchi
|
|
|
485
486
|
lines << "prompt profile: #{snapshot[:profile]} (#{snapshot[:profile_source]})" if snapshot[:profile]
|
|
486
487
|
memory_index = memory_index_text(snapshot[:memory_index])
|
|
487
488
|
lines << "memory index: #{memory_index}" unless memory_index.empty?
|
|
489
|
+
# The model notes the session's prompt carried (Engine#prompt_notes).
|
|
490
|
+
notes = PromptNote.text(snapshot[:prompt_notes])
|
|
491
|
+
lines << "model notes: #{notes}" unless notes.empty?
|
|
488
492
|
llm_context = llm_context_stats_text(snapshot[:llm_context])
|
|
489
493
|
lines << "llm context: #{llm_context}" if llm_context
|
|
490
494
|
if snapshot[:served_model]
|
data/lib/samagotchi/turn_flow.rb
CHANGED
|
@@ -46,11 +46,18 @@ module Samagotchi
|
|
|
46
46
|
# (context summarizes the interrupted turn for a "no, <reason>")
|
|
47
47
|
attr_reader :offer
|
|
48
48
|
|
|
49
|
+
# How many times the conversation went back to a checkpoint (a failed
|
|
50
|
+
# or canceled turn's restore, !rollback): a worker compares it across a
|
|
51
|
+
# turn to tell whether the turn's messages (the delegate reports it
|
|
52
|
+
# merged) left the conversation.
|
|
53
|
+
attr_reader :restores
|
|
54
|
+
|
|
49
55
|
def initialize(engine:)
|
|
50
56
|
@engine = engine
|
|
51
57
|
@checkpoint = nil
|
|
52
58
|
@continue_checkpoint = nil
|
|
53
59
|
@offer = nil
|
|
60
|
+
@restores = 0
|
|
54
61
|
end
|
|
55
62
|
|
|
56
63
|
def awaiting_continue? = !@offer.nil?
|
|
@@ -198,6 +205,7 @@ module Samagotchi
|
|
|
198
205
|
restored = Array(checkpoint) + arrived
|
|
199
206
|
restored = TurnNote.replace_trailing(restored, note) if note
|
|
200
207
|
@engine.rollback_to(restored)
|
|
208
|
+
@restores += 1
|
|
201
209
|
end
|
|
202
210
|
|
|
203
211
|
def conversation_of(result)
|
data/lib/samagotchi/version.rb
CHANGED
data/lib/samagotchi/web/app.rb
CHANGED
|
@@ -626,6 +626,8 @@ module Samagotchi
|
|
|
626
626
|
# What the worker's server said it served for that model (after a turn).
|
|
627
627
|
if snapshot
|
|
628
628
|
session_json = session_json.merge(served_model: snapshot["served_model"], served_model_for: snapshot["served_model_for"])
|
|
629
|
+
# The model notes its prompt carries now (a /model there changes them).
|
|
630
|
+
session_json = session_json.merge(prompt_notes: snapshot["prompt_notes"]) if snapshot["prompt_notes"].is_a?(Array)
|
|
629
631
|
end
|
|
630
632
|
# The LLM context strategy the next turn runs under (the info bar's
|
|
631
633
|
# chip): the worker's, else worked out here from the session file.
|
|
@@ -680,8 +682,9 @@ module Samagotchi
|
|
|
680
682
|
# history again (nor a render of every earlier answer). Live: the
|
|
681
683
|
# worker's GET tail alone (no session file read); else the file. The
|
|
682
684
|
# session part is what the page reads of it (id, status, used memory
|
|
683
|
-
# names
|
|
684
|
-
#
|
|
685
|
+
# names, the model notes its prompt carried: the first turn's build or
|
|
686
|
+
# a /model records them). ?turn_id=: that turn's answer (a queued
|
|
687
|
+
# turn's page re-reads after the next one started). ?recent=1 (the page since this
|
|
685
688
|
# release): the timing's records are the newest turn's (#recent_timing),
|
|
686
689
|
# which the page merges by id; without it, the whole timing as before.
|
|
687
690
|
def handle_tail_read(req, id)
|
|
@@ -692,12 +695,14 @@ module Samagotchi
|
|
|
692
695
|
tail = live_tail(id, turn_id: turn_id)
|
|
693
696
|
state = tail && tail["session_state_snapshot"]
|
|
694
697
|
if state.is_a?(Hash)
|
|
695
|
-
session_json = { id: id, status: state["status"], used_memory_names: Array(state["used_memory_names"])
|
|
698
|
+
session_json = { id: id, status: state["status"], used_memory_names: Array(state["used_memory_names"]),
|
|
699
|
+
prompt_notes: Array(state["prompt_notes"]) }
|
|
696
700
|
messages = last_assistant_for_display([tail["answer"]].compact)
|
|
697
701
|
else
|
|
698
702
|
session = @session_class.load(id, state_dir: default_state_dir)
|
|
699
703
|
state = nil
|
|
700
|
-
session_json = { id: id, status: displayed_status(session), used_memory_names: Array(session.used_memory_names)
|
|
704
|
+
session_json = { id: id, status: displayed_status(session), used_memory_names: Array(session.used_memory_names),
|
|
705
|
+
prompt_notes: session.prompt_notes.map(&:to_h) }
|
|
701
706
|
messages = last_assistant_for_display(session.messages, turn_id: turn_id)
|
|
702
707
|
end
|
|
703
708
|
live_metrics = state && state["metrics"]
|
|
@@ -839,7 +844,12 @@ module Samagotchi
|
|
|
839
844
|
when 202
|
|
840
845
|
record_history(body, line) if PromptHistory.shell_line?(line)
|
|
841
846
|
json_response(202, reply.json || { status: "accepted" })
|
|
842
|
-
when 400
|
|
847
|
+
when 400
|
|
848
|
+
# One of this chi's own commands the worker doesn't know: it runs an older chi.
|
|
849
|
+
name = reply.json&.dig("error") == "unknown_command" && SessionCommands.builtin_name(line)
|
|
850
|
+
next error_response(501, "not_supported", BridgeClient.stale_worker_message(id, cant: "run #{name}")) if name
|
|
851
|
+
|
|
852
|
+
error_response(400, reply.json&.dig("error") || "unknown_command", reply_detail(reply, "not a session command"))
|
|
843
853
|
end
|
|
844
854
|
end
|
|
845
855
|
rescue SessionManager::OwnedByTUI => e
|
|
@@ -28,7 +28,7 @@ import { MODEL_KEY } from "./model_pick.js";
|
|
|
28
28
|
import { emptySessionState } from "./session_state.js";
|
|
29
29
|
import { createModelPicker } from "./model_picker.js";
|
|
30
30
|
import { chipName, chipsHtml, imageFiles, restoredChips, thumbsHtml, turnRefs, turnText } from "./images.js";
|
|
31
|
-
import { escapeHtml, memChipText, memChipTitle, noteHtml, normalize, steerRowHtml, ownerBadge, previewForCard, relativeTime, failedTurnText, modelLabel, deleteConfirmText, recapLabel, canStopSession, goneSessionNotice, withLiveStatus, withoutWorker, recapPlace, delegatedBy, childrenSummary } from "./format.js";
|
|
31
|
+
import { escapeHtml, memChipText, memChipTitle, noteHtml, normalize, steerRowHtml, ownerBadge, previewForCard, relativeTime, failedTurnText, modelLabel, deleteConfirmText, recapLabel, canStopSession, goneSessionNotice, withLiveStatus, withoutWorker, recapPlace, delegatedBy, childrenSummary, promptNotesChip } from "./format.js";
|
|
32
32
|
import { createTurnView, turnHistoryHtml } from "./turn_view.js";
|
|
33
33
|
import { BADGES, emptyAnswerHtml, htmlElement, stateBadgeHtml, userBubbleHtml } from "./turn_html.js";
|
|
34
34
|
import { createStageView } from "./stage_view.js";
|
|
@@ -1126,6 +1126,7 @@ function renderSessionView(data) {
|
|
|
1126
1126
|
current.usedMemories = s.used_memory_names || [];
|
|
1127
1127
|
current.preloadedMemories = s.preloaded_memory_names || [];
|
|
1128
1128
|
current.mutedMemories = s.muted_memory_names || [];
|
|
1129
|
+
current.promptNotes = Array.isArray(s.prompt_notes) ? s.prompt_notes : [];
|
|
1129
1130
|
current.llmContext = s.llm_context || null;
|
|
1130
1131
|
llmContextControl.refresh();
|
|
1131
1132
|
current.parentId = s.parent_id || null;
|
|
@@ -1232,6 +1233,8 @@ async function readTurnEnd() {
|
|
|
1232
1233
|
if (id !== selected) return;
|
|
1233
1234
|
if (d.session) {
|
|
1234
1235
|
if (Array.isArray(d.session.used_memory_names)) current.usedMemories = d.session.used_memory_names;
|
|
1236
|
+
// The notes chip: the first turn's build (or a /model) records them.
|
|
1237
|
+
if (Array.isArray(d.session.prompt_notes)) current.promptNotes = d.session.prompt_notes;
|
|
1235
1238
|
if (d.session.status) current.status = d.session.status;
|
|
1236
1239
|
if (!(await mergeTurnTiming(id, d.timing))) return;
|
|
1237
1240
|
liveTurn.timing.finishLine(line);
|
|
@@ -1778,11 +1781,14 @@ function updateInfoBar() {
|
|
|
1778
1781
|
const parentHtml = parentChip ? `<a class="model parent" href="${escapeHtml(sessionHash(current.parentId))}" title="${escapeHtml(parentChip.title)}">delegated by ${escapeHtml(String(current.parentId).slice(0, 8))}</a>` : "";
|
|
1779
1782
|
// A parent names its delegates; the link opens all sessions.
|
|
1780
1783
|
const kids = childrenSummary({ id: selected }, allSessions);
|
|
1784
|
+
// The model notes the session's prompt carried.
|
|
1785
|
+
const notes = promptNotesChip(current.promptNotes);
|
|
1786
|
+
const notesHtml = notes ? `<span class="model notes-chip" title="${escapeHtml(notes.title)}">${escapeHtml(notes.text)}</span>` : "";
|
|
1781
1787
|
const kidsHtml = kids ? `<a class="model children-chip${kids.waitingText ? " waiting" : ""}" href="${escapeHtml(ALL_SESSIONS_HASH)}" title="${escapeHtml(`${kids.title} (all sessions)`)}">${escapeHtml(kids.text)}${kids.waitingText ? ` · <span class="kids-waiting">${escapeHtml(kids.waitingText)}</span>` : ""}</a>` : "";
|
|
1782
1788
|
const attachCmd = `chi --attach ${selected}`;
|
|
1783
1789
|
const copied = Date.now() - attachCopiedAt < 2000;
|
|
1784
1790
|
const copyHtml = `<button type="button" class="copy-attach" title="Copy "${escapeHtml(attachCmd)}" to attach a terminal">${copied ? "copied ✓" : "copy chi --attach"}</button>`;
|
|
1785
|
-
const metaHtml = `<div class="meta"><span class="id" title="${escapeHtml(selected)}">${escapeHtml(idShort)}</span>${copyHtml}<span class="${statusClass}">${escapeHtml(current.status||"")}</span><span class="model${label.mismatch ? " served-mismatch" : ""}" title="${escapeHtml(label.title)}">${escapeHtml(label.text)}</span>${parentHtml}${kidsHtml}${llmContextChipHtml(current.llmContext, escapeHtml)}${current.usedMemories.length ? `<span class="model mem-chip" title="${escapeHtml(memChipTitle(current.usedMemories))}">${escapeHtml(memChipText(current.usedMemories))}</span>` : ""}<span class="dir" title="${escapeHtml(current.dir||"")}"><bdi>${escapeHtml(current.dir||"")}</bdi></span>${ctxText ? `<span class="model ctx" title="${escapeHtml(ctxTip)}">${escapeHtml(ctxText)}</span>` : ""}${speedTextNow ? `<span class="model speed" title="${escapeHtml(speedTip)}">${escapeHtml(speedTextNow)}</span>` : ""}${timingText ? `<span class="model session-time"></span>` : ""}${workerBadgeHtml()}</div>`;
|
|
1791
|
+
const metaHtml = `<div class="meta"><span class="id" title="${escapeHtml(selected)}">${escapeHtml(idShort)}</span>${copyHtml}<span class="${statusClass}">${escapeHtml(current.status||"")}</span><span class="model${label.mismatch ? " served-mismatch" : ""}" title="${escapeHtml(label.title)}">${escapeHtml(label.text)}</span>${parentHtml}${kidsHtml}${llmContextChipHtml(current.llmContext, escapeHtml)}${notesHtml}${current.usedMemories.length ? `<span class="model mem-chip" title="${escapeHtml(memChipTitle(current.usedMemories))}">${escapeHtml(memChipText(current.usedMemories))}</span>` : ""}<span class="dir" title="${escapeHtml(current.dir||"")}"><bdi>${escapeHtml(current.dir||"")}</bdi></span>${ctxText ? `<span class="model ctx" title="${escapeHtml(ctxTip)}">${escapeHtml(ctxText)}</span>` : ""}${speedTextNow ? `<span class="model speed" title="${escapeHtml(speedTip)}">${escapeHtml(speedTextNow)}</span>` : ""}${timingText ? `<span class="model session-time"></span>` : ""}${workerBadgeHtml()}</div>`;
|
|
1786
1792
|
const previewHtml = `<div class="preview-line"><span class="preview">${escapeHtml(current.firstPreview || "—")}</span> · <span class="mem" title="${escapeHtml(current.usedMemories.join(", "))}">mem: ${escapeHtml(memText)}</span></div>`;
|
|
1787
1793
|
// The session clock ticks every second while a turn runs: only its text
|
|
1788
1794
|
// changes, the bar's other nodes (a tooltip, a button mid-press) stay.
|
|
@@ -301,3 +301,27 @@ export function memChipTitle(names) {
|
|
|
301
301
|
const list = names || [];
|
|
302
302
|
return list.length ? `memories: ${list.join(", ")}` : "no memories in this session";
|
|
303
303
|
}
|
|
304
|
+
|
|
305
|
+
// "model_notes_deepseek (system, 612 chars), model_notes_x (project, 80
|
|
306
|
+
// chars)": the model notes the session's prompt carried (its prompt_notes);
|
|
307
|
+
// "" without any. As PromptNote.text (spec/shared/labels_matrix.json).
|
|
308
|
+
export function promptNotesText(notes) {
|
|
309
|
+
return (Array.isArray(notes) ? notes : [])
|
|
310
|
+
.filter((note) => note && typeof note.name === "string" && note.name.trim())
|
|
311
|
+
.map((note) => {
|
|
312
|
+
const details = [note.scope, Number.isFinite(note.chars) ? `${Math.trunc(note.chars)} chars` : null].filter(Boolean);
|
|
313
|
+
return details.length ? `${note.name.trim()} (${details.join(", ")})` : note.name.trim();
|
|
314
|
+
})
|
|
315
|
+
.join(", ");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// The info bar's model notes chip, for a session whose prompt carried
|
|
319
|
+
// some: "notes: deepseek" (the names without model_notes_), the full line
|
|
320
|
+
// its tooltip; null without notes.
|
|
321
|
+
export function promptNotesChip(notes) {
|
|
322
|
+
const text = promptNotesText(notes);
|
|
323
|
+
if (!text) return null;
|
|
324
|
+
const names = notes.filter((note) => note && typeof note.name === "string" && note.name.trim())
|
|
325
|
+
.map((note) => note.name.trim().replace(/^model_notes_/, ""));
|
|
326
|
+
return { text: `notes: ${names.join(", ")}`, title: `model notes in this session's prompt: ${text}` };
|
|
327
|
+
}
|
|
@@ -14,6 +14,9 @@ export function emptySessionState() {
|
|
|
14
14
|
// in the info bar's tooltip).
|
|
15
15
|
preloadedMemories: [],
|
|
16
16
|
mutedMemories: [],
|
|
17
|
+
// The model notes the session's prompt carried (its prompt_notes: name,
|
|
18
|
+
// scope, chars, digest; the info bar's notes chip).
|
|
19
|
+
promptNotes: [],
|
|
17
20
|
// A delegated session's parent (the info bar's "delegated by" chip).
|
|
18
21
|
parentId: null,
|
|
19
22
|
ctxPct: null,
|
|
@@ -233,8 +233,10 @@ function turnGroup(g, items, timing) {
|
|
|
233
233
|
|
|
234
234
|
// A canceled turn that ran more than one step or any call has no answer:
|
|
235
235
|
// its partial text stays in the block, as it does live (turn_view.js
|
|
236
|
-
// turnEnded promotes only a completed turn or a lone step with no tools).
|
|
236
|
+
// turnEnded promotes only a completed turn or a lone step with no tools). A
|
|
237
|
+
// failed turn never answered: its texts are steps.
|
|
237
238
|
function canceledSteps(g, record, tools) {
|
|
239
|
+
if (record?.status === "failed") return true;
|
|
238
240
|
return record?.status === "canceled" && (g.texts.length > 1 || tools.length > 0);
|
|
239
241
|
}
|
|
240
242
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// removed at the end: the answer bubble takes its place.
|
|
12
12
|
import { leavesBlock } from "./card.js";
|
|
13
13
|
import { copySourceAttr } from "./copy.js";
|
|
14
|
-
import { escapeHtml, messageBodyHtml, noteHtml, normalize, sourceLabel, steerRowHtml } from "./format.js";
|
|
14
|
+
import { escapeHtml, failedTurnText, messageBodyHtml, noteHtml, normalize, sourceLabel, steerRowHtml } from "./format.js";
|
|
15
15
|
import { thumbsHtml } from "./images.js";
|
|
16
16
|
import { shouldFollowScroll } from "./scroll.js";
|
|
17
17
|
import { hookNoticeLabel } from "./turn_events.js";
|
|
@@ -701,6 +701,17 @@ function setLabel(summary, gen) {
|
|
|
701
701
|
summary.innerHTML = html;
|
|
702
702
|
}
|
|
703
703
|
|
|
704
|
+
// A re-rendered failed turn's line (a .bubble.cancel, as live's
|
|
705
|
+
// addEndBubble), in turn_failed's words (failedTurnText) from what the turn
|
|
706
|
+
// record kept of it (SessionMetrics' failure: summary, message,
|
|
707
|
+
// error_class, kept_steps); "" for any other record, or a failed one saved
|
|
708
|
+
// before records kept that.
|
|
709
|
+
function failedLineHtml(record) {
|
|
710
|
+
const failure = record?.status === "failed" ? record.failure : null;
|
|
711
|
+
if (!failure || typeof failure !== "object") return "";
|
|
712
|
+
return `<div class="bubble cancel">${escapeHtml(failedTurnText(failure))}</div>`;
|
|
713
|
+
}
|
|
714
|
+
|
|
704
715
|
// A reloaded history in the turn view (renderHistory's object path): per
|
|
705
716
|
// turn the prompt, a collapsed block of its steps when it had any (texts,
|
|
706
717
|
// and with the server's parts the thinking, params and output, from the
|
|
@@ -789,8 +800,10 @@ export function turnHistoryHtml(items, timing, { thumbs }) {
|
|
|
789
800
|
// timing after its prompt and steps, where the live view leaves it.
|
|
790
801
|
else if (group.answer === null) parts.push(timingHtml(group));
|
|
791
802
|
else parts.push(answerHtml(items[group.answer], timingHtml(group)));
|
|
792
|
-
// A canceled turn ends with its cancel line,
|
|
803
|
+
// A canceled turn ends with its cancel line, a failed one whose steps
|
|
804
|
+
// stayed with its failure line, as the live view left them.
|
|
793
805
|
parts.push(cancelLineHtml(group.record, escapeHtml));
|
|
806
|
+
parts.push(failedLineHtml(group.record));
|
|
794
807
|
return parts.join("");
|
|
795
808
|
}).join("");
|
|
796
809
|
}
|
|
@@ -47,6 +47,8 @@ module Samagotchi
|
|
|
47
47
|
# The session's --memory and --mute lists (info-bar tooltip).
|
|
48
48
|
preloaded_memory_names: Array(session.preloaded_memory_names),
|
|
49
49
|
muted_memory_names: Array(session.muted_memory_names),
|
|
50
|
+
# The model notes its prompt carried (the info bar's notes chip).
|
|
51
|
+
prompt_notes: Array(session.prompt_notes).map(&:to_h),
|
|
50
52
|
# The session that delegated this one (the `delegate` tool), else nil.
|
|
51
53
|
parent_id: session.parent_id,
|
|
52
54
|
# Started by the delegate tool (Session#delegate?); a fork has a
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -507,6 +507,7 @@ module Samagotchi
|
|
|
507
507
|
@wakes.hand_over(reports)
|
|
508
508
|
Log.info(:worker, "delegate_wake_turn", reports: reports.size, in_a_row: @wakes.count!)
|
|
509
509
|
@turn_flow.before_prompt_turn
|
|
510
|
+
restores = @turn_flow.restores
|
|
510
511
|
run_engine_turn(nil, continue: true, origin: reports.first.origin, max_iterations: IterationLimit.for) do |result, error|
|
|
511
512
|
if error
|
|
512
513
|
# Rolled back, the rings stay (settle_child_reports): the user's
|
|
@@ -519,6 +520,11 @@ module Samagotchi
|
|
|
519
520
|
@wakes.pause!
|
|
520
521
|
else
|
|
521
522
|
@continue_offer.after_turn(result)
|
|
523
|
+
# Stopped before it got anywhere (a Stop or a before_turn hook's
|
|
524
|
+
# cancel before the model call) and restored: its rings stay, and
|
|
525
|
+
# wait for the user's next message as after a failure, rather
|
|
526
|
+
# than waking again at once.
|
|
527
|
+
@wakes.pause! if result.canceled? && @turn_flow.restores != restores
|
|
522
528
|
end
|
|
523
529
|
end
|
|
524
530
|
true
|
|
@@ -656,6 +662,7 @@ module Samagotchi
|
|
|
656
662
|
# a fresh worker's first), so the list is this turn's from the start.
|
|
657
663
|
@merged_this_turn = []
|
|
658
664
|
@reporting_turn = reports_to_parent?(turn_args[:origin])
|
|
665
|
+
restores = @turn_flow.restores
|
|
659
666
|
begin
|
|
660
667
|
@session.status = Session::STATUS_RUNNING
|
|
661
668
|
@session.save(state_dir: @state_dir)
|
|
@@ -682,15 +689,20 @@ module Samagotchi
|
|
|
682
689
|
SessionInbox.write_output(@session_dir, response)
|
|
683
690
|
end
|
|
684
691
|
save_or_log(:turn) { save_session }
|
|
685
|
-
#
|
|
686
|
-
#
|
|
687
|
-
|
|
692
|
+
# The reports the turn merged were read unless its end took the turn
|
|
693
|
+
# back out of the conversation (TurnFlow restored a checkpoint: a
|
|
694
|
+
# failed prompt or wake turn that got nowhere, a canceled continue
|
|
695
|
+
# turn). A failed turn whose work stayed, and a reminder or continue
|
|
696
|
+
# turn that failed (kept with its failed note), read them.
|
|
697
|
+
settle_child_reports(rolled_back: @turn_flow.restores != restores)
|
|
688
698
|
ring_parent_after_turn
|
|
689
699
|
end
|
|
690
700
|
|
|
691
701
|
# The delegate reports this turn took: a kept turn moves the cursors on
|
|
692
702
|
# and deletes their rings; a rolled-back one leaves the rings for the
|
|
693
|
-
# next turn.
|
|
703
|
+
# next turn. Once either way: the model reads a report in the
|
|
704
|
+
# conversation, or the next turn brings it (unless a later !rollback
|
|
705
|
+
# erases a kept turn, which drops what it read).
|
|
694
706
|
def settle_child_reports(rolled_back:)
|
|
695
707
|
rolled_back ? @child_reports.release : @child_reports.commit
|
|
696
708
|
end
|
|
@@ -73,7 +73,8 @@ module Samagotchi
|
|
|
73
73
|
# A wake turn starts. @return [Integer] the wake turns in a row now
|
|
74
74
|
def count! = @in_a_row += 1
|
|
75
75
|
|
|
76
|
-
# A wake turn failed
|
|
76
|
+
# A wake turn failed (or was stopped before it got anywhere): none
|
|
77
|
+
# starts again until a human's input.
|
|
77
78
|
def pause!
|
|
78
79
|
@paused = true
|
|
79
80
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: samagotchi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.42.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -373,6 +373,7 @@ files:
|
|
|
373
373
|
- lib/samagotchi/prompt.rb
|
|
374
374
|
- lib/samagotchi/prompt_history.rb
|
|
375
375
|
- lib/samagotchi/prompt_literal_guard.rb
|
|
376
|
+
- lib/samagotchi/prompt_note.rb
|
|
376
377
|
- lib/samagotchi/prompt_warmup.rb
|
|
377
378
|
- lib/samagotchi/question_desk.rb
|
|
378
379
|
- lib/samagotchi/recap_setup.rb
|