samagotchi 0.44.0 → 0.45.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 +70 -1
- data/docs/cli.md +29 -7
- data/docs/context.md +2 -1
- data/docs/plugins.md +4 -2
- data/docs/sessions.md +8 -4
- data/lib/samagotchi/bridge/turn_accumulator.rb +14 -0
- data/lib/samagotchi/bridge.rb +22 -10
- data/lib/samagotchi/bundles/github-pr/manifest.yml +3 -3
- data/lib/samagotchi/bundles/github-pr/plugin.rb +22 -4
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/self_map.md +2 -1
- data/lib/samagotchi/commands/registry.rb +54 -9
- data/lib/samagotchi/engine.rb +8 -3
- data/lib/samagotchi/host_registry.rb +28 -3
- data/lib/samagotchi/llm_context_strategy.rb +37 -0
- data/lib/samagotchi/model_notes.rb +8 -6
- data/lib/samagotchi/model_overlay.rb +3 -2
- data/lib/samagotchi/model_profile.rb +71 -2
- data/lib/samagotchi/plugin/context.rb +19 -7
- data/lib/samagotchi/self_report.rb +31 -2
- data/lib/samagotchi/session_commands.rb +24 -7
- data/lib/samagotchi/session_manager.rb +19 -1
- data/lib/samagotchi/session_metrics.rb +39 -6
- data/lib/samagotchi/sessions_command.rb +15 -5
- data/lib/samagotchi/terminal_ui/attached_loop.rb +85 -3
- data/lib/samagotchi/terminal_ui/formatting.rb +19 -1
- data/lib/samagotchi/terminal_ui.rb +84 -12
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +7 -9
- data/lib/samagotchi/web/public/app.js +23 -15
- data/lib/samagotchi/web/public/command_complete.js +3 -1
- data/lib/samagotchi/web/public/ctx.js +42 -15
- data/lib/samagotchi/web/public/index.html +3 -1
- data/lib/samagotchi/web/public/llm_context_chip.js +2 -4
- data/lib/samagotchi/web/public/stage_model.js +18 -4
- data/lib/samagotchi/web/public/stage_view.js +3 -3
- data/lib/samagotchi/web/public/turn_events.js +16 -4
- data/lib/samagotchi/web/session_hub.rb +9 -1
- data/lib/samagotchi/web/session_summary.rb +19 -2
- data/lib/samagotchi/worker.rb +156 -20
- data/lib/samagotchi/worker_inbound.rb +6 -5
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1dc1da4c0179829c1e78da27358f269fba9fcd1ee2a2412b73493bab37a1d81d
|
|
4
|
+
data.tar.gz: b04878e89e0d634c817e748cc9fe435728e4c6c62863d719a53b4080b1ea5ff0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c0331854a38cff99772512941a1ce8227dc10448c00d4d4c758ba532cfdd19b311e7f4d9733bce7745645231794ec93212a658a542563a8f56008c97492f8c90
|
|
7
|
+
data.tar.gz: 984e5ce3436b750c872bf0a7dd10f1282ed7f4eaf9c084d9f1add023bdcee5088c8f48acbd9eb3e3d01e2efa8dad200c3a350a4a903725aa1de6597407c9ee36
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,74 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.45.0] - 2026-10-08
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Plugins: `ctx.fork?` says whether the session is a fork (`ctx.sessions.fork`: it has a parent and isn't a
|
|
16
|
+
delegate child). See [docs/plugins.md](docs/plugins.md).
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Commands sent while a turn runs no longer all answer `busy`. Those that only show something answer at once
|
|
21
|
+
(`/models`, and `/model`, `/llm-context` and `/guardrails` alone). Those that change something (`/model X`,
|
|
22
|
+
`/llm-context strategy …`, `/guardrails revoke N`) and `!cmd` run after the turn, in the order sent with the
|
|
23
|
+
prompts: the terminals say `(queued: runs after this turn)`, the web draws the command's bubble dashed until it
|
|
24
|
+
ran (after a reload too), and the web's `llm ctx` chip can Set mid-turn. A `!cmd` waiting after a canceled or
|
|
25
|
+
failed turn is dropped, so the turn's `!rollback` stays open, and one still waiting when the worker stops is
|
|
26
|
+
answered `dropped`. `!rollback` and `/continue` stay refused (`busy: Ctrl-C the turn first, then !rollback`).
|
|
27
|
+
`/archive` typed during a turn archives once it ends, in the REPL and an attached terminal. `chi --attach ID
|
|
28
|
+
--model X` on a running turn waits for its end. See "Typing during a turn" in [docs/cli.md](docs/cli.md).
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- `/stats`, `/model` and the web named no model notes for a fresh session (a REPL's before the first turn, a new
|
|
33
|
+
web or worker session), though its first turn's prompt carries them; they now name the notes that prompt will
|
|
34
|
+
load. A session whose prompt carried none keeps saying so.
|
|
35
|
+
- `chi self`'s `model notes` row resolved a note's model overlay from the process's `XDG_CONFIG_HOME` while it
|
|
36
|
+
read the note itself from the env it was given, so the row could count no overlay (or another config's); it now
|
|
37
|
+
reads both from the same env, and passes the fallback key the session's prompt does, so a model typed as an
|
|
38
|
+
alias whose note overlay is keyed by the alias's name shows it — the row agrees with the notes the session's prompt loads.
|
|
39
|
+
- In the plain REPL a session command typed at a continue offer (other than `/continue`) was read as an invalid
|
|
40
|
+
answer and lost; it now runs and the offer stays open, as in a worker.
|
|
41
|
+
- A session command sent as a message behind queued prompts (`chi send -m "/model x"` after `chi send -m "task"`)
|
|
42
|
+
could be answered `busy` by the next queued prompt's turn instead of running before it; it now runs once its
|
|
43
|
+
own message's turn comes.
|
|
44
|
+
- The web's ctx meter under an LLM context budget smaller than the window (`llm_context.budget_tokens`) read the
|
|
45
|
+
percentage of the window after a reload (`ctx 12%`) where live it read the budget's (`ctx 25%`); it now counts
|
|
46
|
+
against the budget both ways, as the `[CONTEXT: …]` line and the status line do, and a generation's final counts
|
|
47
|
+
no longer flip it back to the window mid-turn. The ctx tooltip's context line says so: `context: ~16.0k of the
|
|
48
|
+
64.0k budget, 128.0k window (server)`.
|
|
49
|
+
- The web's strip of latest sessions, back from All sessions with the pointer still on it, could keep a waiting
|
|
50
|
+
family behind other cards until the pointer moved: an update that came while All sessions opened was drawn in
|
|
51
|
+
the order held for the pointer.
|
|
52
|
+
- github-pr 0.3.1: in `auto_attach: offer` mode a forked session (`/btw keep`, a plugin's fork) now gets its
|
|
53
|
+
`first_prompt` row in `offers.ndjson` (its first prompt after the conversation it started from), as a new
|
|
54
|
+
session does; it got none.
|
|
55
|
+
- A resumed or attached session (`chi --resume`, `chi --attach`) drew a turn that failed with its steps kept as its
|
|
56
|
+
prompt and tool rows followed by an earlier step's text, as if that were the answer; the join now draws the line
|
|
57
|
+
the live view left (`✕ turn failed: HTTP 500: boom · 2.0s`, then ` partial progress kept; !rollback restores the
|
|
58
|
+
pre-turn state` when the steps stayed) after the rows, with no answer text, as the web does.
|
|
59
|
+
- The web's stage trail: when the ✂ row of an applied LLM context edit landed in its step inside the
|
|
60
|
+
closed cloud it was out of sight and, unlike a hook's notice, flashed nothing. It now flashes in the
|
|
61
|
+
trail for a few seconds, as its row reads (`✂ forgot 2 outputs, stubbed 1 stale read · frees ~4.1k
|
|
62
|
+
tokens (paid off)`), as a hook notice does.
|
|
63
|
+
- The web's stage trail: when a message cut a generation that was still thinking, its row (`↪ cut in
|
|
64
|
+
for your message`, `… a message sent with chi send`, `… the parent agent's message`) landed in its
|
|
65
|
+
step inside the closed cloud and, out of sight, flashed nothing. It now flashes in the trail for a
|
|
66
|
+
few seconds, as its row reads, as the ✂ row and a hook notice do.
|
|
67
|
+
- `chi send --new --model` and `delegate` warned `host 'h' doesn't list model 'x'` for an id the host serves, when
|
|
68
|
+
the on-disk model list still named the model a one-model server served before it was reloaded: a saved list
|
|
69
|
+
older than 10 minutes that misses the id now re-lists that one host once (bounded at 5 s) and judges again
|
|
70
|
+
against what it lists now, saving the fresh ids. A list from the last 10 minutes is taken at its word, so a host
|
|
71
|
+
that serves ids it never lists (a gateway's round-robin aliases) is not asked on every launch; a re-list that
|
|
72
|
+
fails, times out or lists nothing warns from the saved list.
|
|
73
|
+
- The session lists' `ctx %` (the web's session cards, `chi sessions list` and its `ctx 12%` column, the
|
|
74
|
+
`list_sessions` tool's `ctx_pct`) and `/stats`' `context used:` still divided by the window when the session's
|
|
75
|
+
LLM context budget (`llm_context.budget_tokens`) was smaller, where the live meter counts the smaller of the
|
|
76
|
+
two; they now count the same way, and `/stats` says so: `context used: 16000 tokens (25.0% of the 64000
|
|
77
|
+
budget)`.
|
|
78
|
+
|
|
11
79
|
## [0.44.0] - 2026-10-08
|
|
12
80
|
|
|
13
81
|
### Added
|
|
@@ -2026,7 +2094,8 @@ and long-lived sessions.
|
|
|
2026
2094
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
2027
2095
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
2028
2096
|
|
|
2029
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
2097
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.45.0...HEAD
|
|
2098
|
+
[0.45.0]: https://github.com/dm1try/samagotchi/compare/v0.44.0...v0.45.0
|
|
2030
2099
|
[0.44.0]: https://github.com/dm1try/samagotchi/compare/v0.43.0...v0.44.0
|
|
2031
2100
|
[0.43.0]: https://github.com/dm1try/samagotchi/compare/v0.42.0...v0.43.0
|
|
2032
2101
|
[0.42.0]: https://github.com/dm1try/samagotchi/compare/v0.41.0...v0.42.0
|
data/docs/cli.md
CHANGED
|
@@ -429,7 +429,7 @@ request (with the cached tokens, the cache writes, the re-prefilled tokens and t
|
|
|
429
429
|
reports them),
|
|
430
430
|
the decode speed, generation latency, cancellations, retries (the network's),
|
|
431
431
|
thinking cuts (generations a plugin such as loop-guard cut) and output cap hits
|
|
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
|
+
(generations that ended at the provider's cap), context used and window (the used figure is counted against the smaller of the window and the session's LLM context budget, and says so: `context used: 16000 tokens (25.0% of the 64000 budget)`), 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
|
|
433
433
|
cost when the provider reports one (OpenRouter's `usage.cost`; no prices are
|
|
434
434
|
guessed). The speed line is the last generation's and the session's average
|
|
435
435
|
(weighted by tokens): llama.cpp reports it exactly (`87 tok/s out, 1.9k tok/s
|
|
@@ -451,7 +451,8 @@ saved with the session and applies from the next turn's start; the reply says
|
|
|
451
451
|
what it costs: `forget` on or off re-reads the whole prompt once, `stale` turned
|
|
452
452
|
on stages the reads already superseded as one batch under the apply rule, and a
|
|
453
453
|
layer turned off sends its stubs whole again (turning it back on brings them
|
|
454
|
-
back).
|
|
454
|
+
back). `/llm-context` alone answers mid-turn too; a change sent while a turn runs
|
|
455
|
+
waits for the turn's end (see *Typing during a turn*). `/stats` shows
|
|
455
456
|
the same values on its `llm context:` line. The web's info bar shows them as the
|
|
456
457
|
`llm ctx` chip (`llm ctx stale,forget · session`), whose popover sets them
|
|
457
458
|
through `/llm-context`. See "LLM context: a session's own strategy" in
|
|
@@ -468,11 +469,28 @@ REPL alike:
|
|
|
468
469
|
A line that comes after the turn's last step runs as the next turn, and so
|
|
469
470
|
does one sent after Ctrl-C: it doesn't merge into the turn being cancelled.
|
|
470
471
|
Reminder turns take merged lines too.
|
|
471
|
-
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
472
|
+
- Commands answer, wait or are refused, by what they do:
|
|
473
|
+
- At once, beside the turn: `/stats`, `/recap`, `/help`, `/context`, a
|
|
474
|
+
plugin's anytime command (`/btw`), and the commands that only show
|
|
475
|
+
something: `/models` (with a filter too), and `/model`, `/llm-context` and
|
|
476
|
+
`/guardrails` alone.
|
|
477
|
+
- After the turn: the ones that change something (`/model X`, `/model clear`,
|
|
478
|
+
`/llm-context strategy …`, `/guardrails revoke N`) and `!cmd`, whose output
|
|
479
|
+
joins the conversation. `(queued: runs after this turn)` says so at once (the
|
|
480
|
+
web draws the command's bubble dashed, with the same words, until it ran);
|
|
481
|
+
when the turn ends they run in the order sent, a prompt sent before one
|
|
482
|
+
running first and one sent after it running after. A `/model X` sent
|
|
483
|
+
mid-turn doesn't change the running turn, nor a line merged into it later.
|
|
484
|
+
After a canceled or failed turn a waiting `!cmd` is dropped
|
|
485
|
+
(`turn canceled: !ls not run; send it again`): running it would close the
|
|
486
|
+
`!rollback` the turn left; the others still run. A command still waiting
|
|
487
|
+
when the session's worker stops is answered `dropped` too.
|
|
488
|
+
- Refused: `!rollback` (`busy: Ctrl-C the turn first, then !rollback`),
|
|
489
|
+
`/continue` and a plugin's command that isn't anytime
|
|
490
|
+
(`busy: wait for the turn to end`). The line goes back into the prompt, so
|
|
491
|
+
Enter runs it once the turn ends.
|
|
492
|
+
|
|
493
|
+
A one-word `/word` no command answers (`/modle`) is not steering text either:
|
|
476
494
|
the hint prints and the line is dropped.
|
|
477
495
|
- A question (`ask_user_question`, a guardrails approval) turns the prompt into
|
|
478
496
|
a yellow `? ` and lists its choices under it, fitted to the terminal; only a
|
|
@@ -486,6 +504,10 @@ REPL alike:
|
|
|
486
504
|
(`/exit --delete` deletes the session then too). In an
|
|
487
505
|
attached terminal it detaches at once and the turn goes on in the worker
|
|
488
506
|
(`/exit` then says the worker stays up: a turn is running).
|
|
507
|
+
- `/archive` mid-turn archives once the turn ends:
|
|
508
|
+
`(archives after this turn; Ctrl-C cancels it)` in the plain REPL,
|
|
509
|
+
`(archives after this turn)` in an attached terminal, which also waits for
|
|
510
|
+
the commands queued behind the turn.
|
|
489
511
|
|
|
490
512
|
With stdin that isn't a terminal (a pipe), the REPL reads a line only between
|
|
491
513
|
turns.
|
data/docs/context.md
CHANGED
|
@@ -225,7 +225,8 @@ later when an offer is wanted: one JSON line per event, `offered` (session,
|
|
|
225
225
|
project root, working directory, branch, PR URL, main checkout or worktree,
|
|
226
226
|
model), `attached` and `declined` (with the seconds since the offer), and
|
|
227
227
|
`first_prompt` (the first 160 characters of every session's first message,
|
|
228
|
-
offered or not;
|
|
228
|
+
offered or not; a fork's first after the conversation it started from, once;
|
|
229
|
+
not scratch sessions or delegate children). Over 1 MB it is
|
|
229
230
|
renamed to `offers.ndjson.1` (one old file is kept). Only you can read it
|
|
230
231
|
(0600), and nothing leaves your machine.
|
|
231
232
|
|
data/docs/plugins.md
CHANGED
|
@@ -94,8 +94,9 @@ commands it knows.
|
|
|
94
94
|
|
|
95
95
|
#### `anytime: true`
|
|
96
96
|
|
|
97
|
-
A normal command waits for its turn: typed while a turn runs, it is
|
|
98
|
-
as busy (the REPL puts it back into the prompt
|
|
97
|
+
A plugin's normal command waits for its turn: typed while a turn runs, it is
|
|
98
|
+
refused as busy (the REPL puts it back into the prompt; a plugin command can't
|
|
99
|
+
ask to run after the turn, as the built-in `/model X` does). An `anytime: true` command
|
|
99
100
|
runs **at once, on its own thread, beside the running turn**, and the turn
|
|
100
101
|
goes on:
|
|
101
102
|
|
|
@@ -504,6 +505,7 @@ full label, `plugin.rb (bundle my-bundle)`.
|
|
|
504
505
|
| `ctx.repo_root` | the git checkout holding `cwd`, or nil |
|
|
505
506
|
| `ctx.scratch?` | whether the session is a `chi scratch` one (deleted when it ends) |
|
|
506
507
|
| `ctx.delegate?` | whether the session is a delegate child: a task another session handed over |
|
|
508
|
+
| `ctx.fork?` | whether the session is a fork (`ctx.sessions.fork`): it has a parent but isn't a delegate child, and started from a conversation, so its first prompt isn't its first user message |
|
|
507
509
|
| `ctx.model` | the model the session runs on now: its resolved ref (`host:id`), right after `/model` too |
|
|
508
510
|
| `ctx.model_key` | that model's memory overlay key (`<name>.<key>.md`, what `memory_write current_model_only` writes) |
|
|
509
511
|
| `ctx.settings` | the bundle's settings, frozen |
|
data/docs/sessions.md
CHANGED
|
@@ -21,11 +21,11 @@ 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`). 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.
|
|
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. A command sent during a turn that waits for its end (`/model X`, `!cmd`; see *Typing during a turn* in [cli.md](cli.md)) and hasn't run when the worker leaves (a stop, a crash) is answered `command_ran {status: "dropped"}`, so no UI keeps showing it waiting.
|
|
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
|
|
|
28
|
-
<a id="archiving-a-session"></a>**Archiving a session:** `chi sessions archive ID...`, `/archive` in a terminal and the Web UI's `archive` go through `SessionManager.archive_session`: it writes `<id>/archived` (`{"archived_at": …}`; not a session.json field, which every save rewrites) for the session and its delegated children. `Session.list` leaves archived sessions out unless `include_archived:`, and everything built on it follows: `chi sessions list` (`--archived` shows them, `[archived]`), the summaries (`--live`, json/tsv, the desktop panel, the agent's `list_sessions`) and the retention prune, which neither deletes nor counts them against `max_count`. `children_of` (max_children, `delegate_result`) still sees them, and `--resume`/`--attach ID`, `chi send`/`chi note ID` reach them as before. Refused: a turn running in the session or in one of its children (naming the child; 409 `busy` on the web), a prompt queued in `input/` (it would run on the next resume) or a live worker's step-limit question still open, in it or a child (409 `busy` too), a plain REPL owning it (`owned_by_tui`), a `chi scratch` session (`scratch`). A live idle worker is stopped first, and the marker is written even if it is still shutting down. Input a human typed brings a session back (`{"unarchived_at": …}`): a prompt from the web, an attached or plain terminal or `chi send` (origin `web:*`, `tui:*`, `cli:send`, none), steering merged into a running turn, a `/continue` answer or a question answered (the step-limit question too); a delegate's follow-up, a plugin's turn, a reminder, the idle recap, a note and just opening it don't. A child's input brings only the child back. The retention sweep ages an unarchived session from when it was unarchived, so unarchiving an old session doesn't let the next sweep take it. `/archive` leaves the session first, as `/exit` does, then archives it: in an attached terminal the worker is asked to exit (one that stays up for another UI is stopped by the archive; a running turn, a queued prompt or an open step-limit question refuses it), and an empty session is discarded instead; `chi scratch` refuses it. `chi sessions unarchive ID...` and the web's `unarchive` bring back the whole family. Web routes: `POST /api/sessions/:id/archive` (200 `{status, session_id, archived, stopped, discarded}`, ids) and `POST /api/sessions/:id/unarchive` (200 `{status, session_id, unarchived}`). The session hub and `GET /api/sessions` keep archived sessions, `archived: true` in their summaries: the page leaves them out of the strip, its count and the list at render, and "include archived" by the all-sessions search shows them, dimmed with an `archived` badge. The info bar marks a session whose live worker runs an older chi than the newest installed (`worker_version` in its summary): `chi 0.18.1 · Restart`, which asks to confirm and calls `POST /api/sessions/:id/restart` (a refusal's reason shows in a toast); a worker from before restarts shows `chi sessions stop ID` instead. The info bar reads `archive · stop · delete` (`unarchive` on an archived one); archiving the open session stays on it, with an Undo toast.
|
|
28
|
+
<a id="archiving-a-session"></a>**Archiving a session:** `chi sessions archive ID...`, `/archive` in a terminal and the Web UI's `archive` go through `SessionManager.archive_session`: it writes `<id>/archived` (`{"archived_at": …}`; not a session.json field, which every save rewrites) for the session and its delegated children. `Session.list` leaves archived sessions out unless `include_archived:`, and everything built on it follows: `chi sessions list` (`--archived` shows them, `[archived]`), the summaries (`--live`, json/tsv, the desktop panel, the agent's `list_sessions`) and the retention prune, which neither deletes nor counts them against `max_count`. `children_of` (max_children, `delegate_result`) still sees them, and `--resume`/`--attach ID`, `chi send`/`chi note ID` reach them as before. Refused: a turn running in the session or in one of its children (naming the child; 409 `busy` on the web), a prompt queued in `input/` (it would run on the next resume) or a live worker's step-limit question still open, in it or a child (409 `busy` too), a plain REPL owning it (`owned_by_tui`), a `chi scratch` session (`scratch`). A live idle worker is stopped first, and the marker is written even if it is still shutting down. Input a human typed brings a session back (`{"unarchived_at": …}`): a prompt from the web, an attached or plain terminal or `chi send` (origin `web:*`, `tui:*`, `cli:send`, none), steering merged into a running turn, a `/continue` answer or a question answered (the step-limit question too); a delegate's follow-up, a plugin's turn, a reminder, the idle recap, a note and just opening it don't. A child's input brings only the child back. The retention sweep ages an unarchived session from when it was unarchived, so unarchiving an old session doesn't let the next sweep take it. `/archive` leaves the session first, as `/exit` does, then archives it: in an attached terminal the worker is asked to exit (one that stays up for another UI is stopped by the archive; a running turn, a queued prompt or an open step-limit question refuses it), and an empty session is discarded instead; `chi scratch` refuses it. Typed while a turn runs, `/archive` waits for the turn's end (an attached terminal for the commands queued behind it too) and then archives. `chi sessions unarchive ID...` and the web's `unarchive` bring back the whole family. Web routes: `POST /api/sessions/:id/archive` (200 `{status, session_id, archived, stopped, discarded}`, ids) and `POST /api/sessions/:id/unarchive` (200 `{status, session_id, unarchived}`). The session hub and `GET /api/sessions` keep archived sessions, `archived: true` in their summaries: the page leaves them out of the strip, its count and the list at render, and "include archived" by the all-sessions search shows them, dimmed with an `archived` badge. The info bar marks a session whose live worker runs an older chi than the newest installed (`worker_version` in its summary): `chi 0.18.1 · Restart`, which asks to confirm and calls `POST /api/sessions/:id/restart` (a refusal's reason shows in a toast); a worker from before restarts shows `chi sessions stop ID` instead. The info bar reads `archive · stop · delete` (`unarchive` on an archived one); archiving the open session stays on it, with an Undo toast.
|
|
29
29
|
|
|
30
30
|
**Lazy sweep:** automatic prune runs only while `chi web` runs, at most once per `session.sweep_interval_hours` (default 24): the web's session hub gives it a chance on its periodic full scan, and `GET /api/sessions` does too. No cron, and a terminal-only chi never sweeps. Manual prune is always available.
|
|
31
31
|
|
|
@@ -56,7 +56,7 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
|
|
|
56
56
|
|
|
57
57
|
`--dry-run` is the safe preview. `--keep N` keeps the N newest sessions that stay: one that goes anyway (a leftover scratch session, one left empty, one older than `--days`) takes no place in the count. Web has no prune endpoint; use the CLI.
|
|
58
58
|
|
|
59
|
-
`chi sessions list` shows each session's saved recap, its first sentence without the "The user was…" opening (as on the web cards), in place of its last prompt, cut to 60 characters; a session with no recap shows its last prompt. A session whose last turn a hook stopped carries `[looped]` when the hook was loop-guard (the thinking watch's cut, or its stop) and `[stopped by <hook>]` for any other hook, after `[archived]` (`--format json` has it as `stopped_by`; from `Session#last_turn`, so it stays until a later turn ends differently). The web's session cards wear the same marker as a badge: `looped` or `stopped by <hook>` after the status dot (a session that also waits on you shows only its waiting badge: the stop is behind the need), and the all-view's search matches its words too. A delegated session's row ends with `↳ <parent's short id>` (plain and `--live`; `--format json` has `parent_id`); rows keep their `updated_at` order, so a parent may be on another page than its children. After the status, `ctx 12%` says how full the context was after the session's last counted turn (from its `analytics.json`; blank when no turn had a server count or the window is unknown; `ctx_pct` in `--format json`). The web's session cards show it quietly too (`12%`), and a session's ctx meter shows it on load, before its next turn streams.
|
|
59
|
+
`chi sessions list` shows each session's saved recap, its first sentence without the "The user was…" opening (as on the web cards), in place of its last prompt, cut to 60 characters; a session with no recap shows its last prompt. A session whose last turn a hook stopped carries `[looped]` when the hook was loop-guard (the thinking watch's cut, or its stop) and `[stopped by <hook>]` for any other hook, after `[archived]` (`--format json` has it as `stopped_by`; from `Session#last_turn`, so it stays until a later turn ends differently). The web's session cards wear the same marker as a badge: `looped` or `stopped by <hook>` after the status dot (a session that also waits on you shows only its waiting badge: the stop is behind the need), and the all-view's search matches its words too. A delegated session's row ends with `↳ <parent's short id>` (plain and `--live`; `--format json` has `parent_id`); rows keep their `updated_at` order, so a parent may be on another page than its children. After the status, `ctx 12%` says how full the context was after the session's last counted turn (from its `analytics.json`; blank when no turn had a server count or the window is unknown; `ctx_pct` in `--format json`). The fill is counted against the session's LLM context budget when one is set and smaller than the window (its own `--llm-context-budget`, else the model's/host's/config's; see [configuration.md](configuration.md#llm-context-a-sessions-own-strategy)), as the live meter counts it; the list, the web card and the session page otherwise agree. The web's session cards show it quietly too (`12%`), and a session's ctx meter shows it on load, before its next turn streams.
|
|
60
60
|
|
|
61
61
|
**Projects:** a session belongs to the git project it was started in: the repository, whichever worktree or subfolder of it (the same project root memories use). `chi sessions list` (with `--live` and `--format` too) and `chi web` show the current folder's project; `--scope=all` shows every session, and so does a folder in no repo (`~`). `--cwd PATH` is a folder filter instead of the project. The project is stored with the session (`project_root` in its JSON, `project` in `--format json`), so a session keeps it after its worktree is deleted; sessions older than that are placed by their folder, and one whose folder is gone shows in `--scope=all` only. Retention, `--resume`/`--attach ID`, `chi send`/`chi note ID` and `chi note --all` (every live session) are not scoped. The agent's `list_sessions` lists its own project's sessions; `cwd: "/"` lists every one.
|
|
62
62
|
|
|
@@ -131,6 +131,10 @@ system message marked `kind: turn_note` (the UIs don't show it; they showed the
|
|
|
131
131
|
When the context window fills past 40% (then 60%, 80%), the native loop leaves a similar line (`kind: context`):
|
|
132
132
|
`[CONTEXT: about 55% of the context window is in use (estimated; bucket=40plus). context moderate — prefer targeted
|
|
133
133
|
and range reads over full-file dumps]`, once per rise; see [context telemetry](internals/context-telemetry.md).
|
|
134
|
+
Under an LLM context budget smaller than the window (`llm_context.budget_tokens`, a session's `/llm-context budget`;
|
|
135
|
+
see [configuration.md](configuration.md#all-settings)), the percentage is of the budget and the line says so
|
|
136
|
+
(`about 62% of the context budget (64k tokens) is in use`); the status line's ctx and the web's ctx meter (live and
|
|
137
|
+
after a reload) count against it too.
|
|
134
138
|
|
|
135
139
|
Failed retries replace the note, they don't pile up. The note's text is escaped like user text (an error may
|
|
136
140
|
quote what a server sent). A cancelled continue turn and `!rollback` go back to before the turn, note included.
|
|
@@ -171,7 +175,7 @@ chi send --wait -m "and the tests?" 3fa2 # a follow-up in the same
|
|
|
171
175
|
chi send --wait 3fa2 # sends nothing: waits for its next reply
|
|
172
176
|
```
|
|
173
177
|
|
|
174
|
-
- `--new` takes no ids (one new session per call). `--dir DIR` is its folder, and so its project (default the current one); `--model M` its model (default the configured one). A host config.yml doesn't have is refused before anything is spawned (exit 1). A `host:id` whose id that host's last saved list doesn't have (`~/.local/state/samagotchi/model_lists.json`, what `chi models` / `/models` / the web last listed for that host, at most a week old) gets one warning line on stderr, with up to three close ids as a hint (`did you mean: gemma-small?`), and the session starts anyway: some hosts serve ids they don't list (a one-model llama.cpp server takes any name). A bare id (no `host:`) isn't checked that way — routing sends it to whichever host lists it — and with no saved list for the host nothing is said; a wrong id then fails the worker's first turn.
|
|
178
|
+
- `--new` takes no ids (one new session per call). `--dir DIR` is its folder, and so its project (default the current one); `--model M` its model (default the configured one). A host config.yml doesn't have is refused before anything is spawned (exit 1). A `host:id` whose id that host's last saved list doesn't have (`~/.local/state/samagotchi/model_lists.json`, what `chi models` / `/models` / the web last listed for that host, at most a week old) gets one warning line on stderr, with up to three close ids as a hint (`did you mean: gemma-small?`), and the session starts anyway: some hosts serve ids they don't list (a one-model llama.cpp server takes any name). A saved list older than 10 minutes that misses the id is re-listed once (that one host, capped at 5 s) before the warning: the list can be stale by content, e.g. a one-model server reloaded with another model; a list re-listed that way names the typed id, nothing is said. A list younger than 10 minutes is taken at its word (a host that serves ids it never lists would otherwise pay the re-list on every launch). A re-list that fails, times out or lists nothing falls back to the saved list. A bare id (no `host:`) isn't checked that way — routing sends it to whichever host lists it — and with no saved list for the host nothing is said; a wrong id then fails the worker's first turn.
|
|
175
179
|
- `--new` prints `<full id> started` on stdout. With `--wait` that line (and the `sent` line for an existing session) goes to stderr, so stdout is exactly the answer: the reply of the turn the message started, in full. It is the worker's `output/` file, the same one `delegate` reads; a turn that ends with only tool calls and no text has none.
|
|
176
180
|
- `--wait` takes one session: `--new` or one id. A session with a running turn is refused (`busy: a turn is running; wait or attach`, exit 1): the message would run after it, and its reply would come back as the answer.
|
|
177
181
|
- Exit codes with `--wait`: 0 answered; 3 the turn waits for an answer from you (a question, a hook's question, a guardrail approval, or the step-limit question: see [chi as a sub-agent](sub-agent.md#the-step-limit)), and the session keeps waiting: stderr gets the whole question (`waiting for an answer (approval): …`, its text, the numbered options, whether free text is allowed) and how to answer it (`chi answer ID --question QID --option N`, or `chi --attach ID`, the web); 4 `--timeout S` passed with the turn still running (it goes on; wait again); 1 the turn ended without a reply (canceled, failed or empty), the worker failed or vanished, or the session was stopped; 130 on Ctrl-C, which leaves the turn running (`still running: chi --attach ID`). There is no default timeout.
|
|
@@ -26,6 +26,7 @@ module Samagotchi
|
|
|
26
26
|
# When each running tool call started, by [iteration, call_index].
|
|
27
27
|
@tool_started_at = {}
|
|
28
28
|
@queued = []
|
|
29
|
+
@queued_commands = []
|
|
29
30
|
@merged_origins = []
|
|
30
31
|
@recap = nil
|
|
31
32
|
@continue_offer = nil
|
|
@@ -199,11 +200,24 @@ module Samagotchi
|
|
|
199
200
|
@mutex.synchronize { @queued.map(&:dup) }
|
|
200
201
|
end
|
|
201
202
|
|
|
203
|
+
# @return [Array<Hash>] commands queued for after the running turn
|
|
204
|
+
# (command_queued with waits, not a card's action) that haven't run
|
|
205
|
+
# or been dropped yet:
|
|
206
|
+
# {command_id:, client_id:, line:}, in arrival order
|
|
207
|
+
def queued_commands
|
|
208
|
+
@mutex.synchronize { @queued_commands.map(&:dup) }
|
|
209
|
+
end
|
|
210
|
+
|
|
202
211
|
private
|
|
203
212
|
|
|
204
213
|
def fold(event)
|
|
205
214
|
type = event[:type]
|
|
206
215
|
case type
|
|
216
|
+
when :command_queued
|
|
217
|
+
# A card's action shows as its card, never as a line (the UIs skip it live).
|
|
218
|
+
@queued_commands << event.slice(:command_id, :client_id, :line) if event[:waits] && !event[:card]
|
|
219
|
+
when :command_ran
|
|
220
|
+
@queued_commands.reject! { |entry| entry[:command_id] == event[:command_id] } if event[:queued]
|
|
207
221
|
when :turn_enqueued
|
|
208
222
|
queued = { enqueued_id: event[:enqueued_id], client_id: event[:client_id], prompt: event[:prompt] }
|
|
209
223
|
queued[:images] = event[:images] if event[:images]
|
data/lib/samagotchi/bridge.rb
CHANGED
|
@@ -75,7 +75,8 @@ module Samagotchi
|
|
|
75
75
|
# SessionInbox::INPUT_FORMAT); nil advertises none
|
|
76
76
|
# @param on_input [#call, nil] called once a turn for this session is
|
|
77
77
|
# queued, to wake the worker loop (Worker::Waker#wake)
|
|
78
|
-
# @param on_command [#call, nil] takes a session command
|
|
78
|
+
# @param on_command [#call, nil] takes a session command, returns its
|
|
79
|
+
# mid-turn policy (:anytime when it runs now)
|
|
79
80
|
# ({command_id:, client_id:, line:}) for the worker loop to run; without
|
|
80
81
|
# one, POST /command answers 501
|
|
81
82
|
# @param on_exit_request [#call, nil] a client asks the worker to exit
|
|
@@ -222,7 +223,9 @@ module Samagotchi
|
|
|
222
223
|
# doesn't keep it.
|
|
223
224
|
# chi_version: the chi this worker runs (an attaching TUI compares it
|
|
224
225
|
# with its own).
|
|
225
|
-
#
|
|
226
|
+
# The commands queued for after the running turn (queued_commands,
|
|
227
|
+
# TurnAccumulator#queued_commands) too.
|
|
228
|
+
# @return [Hash] {messages:, current_turn:, queued:, queued_commands:, recap:, saved_recap:, continue_offer:, pending_question:,
|
|
226
229
|
# guardrail_warning:, plugin_warning:, init_tasks:, cards:, commands:, chi_version:, event_seq:, event_id:}
|
|
227
230
|
def snapshot
|
|
228
231
|
@engine.synchronize_events do
|
|
@@ -231,6 +234,7 @@ module Samagotchi
|
|
|
231
234
|
messages: @engine.messages_checkpoint,
|
|
232
235
|
current_turn: @accumulator.current_turn,
|
|
233
236
|
queued: @accumulator.queued,
|
|
237
|
+
queued_commands: @accumulator.queued_commands,
|
|
234
238
|
recap: @accumulator.recap,
|
|
235
239
|
saved_recap: @engine.saved_recap,
|
|
236
240
|
continue_offer: @accumulator.continue_offer,
|
|
@@ -277,13 +281,15 @@ module Samagotchi
|
|
|
277
281
|
# The worker's own way in for a session command that came as a message
|
|
278
282
|
# (its first prompt, an input file): queued and announced as a POST
|
|
279
283
|
# /command's is (#handle_command).
|
|
284
|
+
# @param after_file [String, nil] the input file it came in (its
|
|
285
|
+
# basename): it runs once that file is taken (Worker#on_command)
|
|
280
286
|
# @return [String] its command_id
|
|
281
|
-
def queue_command(line, client_id: nil, card: false)
|
|
287
|
+
def queue_command(line, client_id: nil, card: false, after_file: nil)
|
|
282
288
|
command = { command_id: SecureRandom.uuid, client_id: client_id, line: line.to_s.strip }
|
|
283
289
|
# A card's action (the step-limit question's answer): the UIs leave
|
|
284
290
|
# its line out.
|
|
285
291
|
command[:card] = true if card
|
|
286
|
-
@engine.synchronize_events { queue_command_locked(command) }
|
|
292
|
+
@engine.synchronize_events { queue_command_locked(command, after_file: after_file) }
|
|
287
293
|
command[:command_id]
|
|
288
294
|
end
|
|
289
295
|
|
|
@@ -727,13 +733,19 @@ module Samagotchi
|
|
|
727
733
|
[{}, 202, { status: "accepted", command_id: command[:command_id], session_id: @session_id }]
|
|
728
734
|
end
|
|
729
735
|
|
|
730
|
-
# With the event log held.
|
|
731
|
-
|
|
732
|
-
|
|
736
|
+
# With the event log held. +on_command+ answers what it did with the
|
|
737
|
+
# line (Commands::Registry::MID_TURN).
|
|
738
|
+
def queue_command_locked(command, after_file: nil)
|
|
739
|
+
policy = after_file ? @on_command.call(command, after_file: after_file) : @on_command.call(command)
|
|
733
740
|
# An anytime command runs now, beside a turn (D8): the UIs show its
|
|
734
|
-
# line here, so the cards it shows come after it.
|
|
735
|
-
|
|
736
|
-
|
|
741
|
+
# line here, so the cards it shows come after it. A queued one runs
|
|
742
|
+
# when the turn ends: the UIs show it waiting until its command_ran.
|
|
743
|
+
marks = case policy
|
|
744
|
+
when :anytime then { anytime: true }
|
|
745
|
+
when :queue then { waits: "turn_end" }
|
|
746
|
+
else {}
|
|
747
|
+
end
|
|
748
|
+
@engine.announce(type: :command_queued, **command, **marks)
|
|
737
749
|
end
|
|
738
750
|
|
|
739
751
|
# A client asks the worker to exit now (`/exit` in the attached TUI), or
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: github-pr
|
|
3
|
-
version: 0.3.
|
|
3
|
+
version: 0.3.1
|
|
4
4
|
scope: system
|
|
5
5
|
description: Attaches the branch's open GitHub pull request to a session as context (and any PR URL with chi context add or the web's "+ URL"); the agent gets a note when reviews, comments, checks or its state change; in the web, a path:line in an answer links to that line of the PR; the auto_attach setting can offer the PR with a card first, or attach nothing
|
|
6
6
|
trust_level: reviewed
|
|
7
|
-
requires_chi: ">= 0.
|
|
7
|
+
requires_chi: ">= 0.45.0"
|
|
8
8
|
needs:
|
|
9
9
|
- command: gh
|
|
10
10
|
why: reads the pull request
|
|
11
11
|
hint: brew install gh, then gh auth login
|
|
12
12
|
plugin:
|
|
13
13
|
file: plugin.rb
|
|
14
|
-
sha256: sha256:
|
|
14
|
+
sha256: sha256:eb6207a0aec4267eb15d28e4db2edb10d458c3ff574c2be56f225b24b7afb733
|
|
15
15
|
scripts:
|
|
16
16
|
pr_context.rb: sha256:a883d2780e89c68c0d7b6d5e0fcb22c273f17bb167e79064e0e99db6bd2ee6f0
|
|
17
17
|
context_providers:
|
|
@@ -218,13 +218,20 @@ class Plugin
|
|
|
218
218
|
# Offer mode: every session's first prompt (offered or not; a web chat's
|
|
219
219
|
# first turn starts before the init task's gh returns), joined to the
|
|
220
220
|
# offers by session at analysis time. First = no user message yet (a
|
|
221
|
-
# context note may be in messages already)
|
|
221
|
+
# context note may be in messages already); a fork starts from its
|
|
222
|
+
# parent's conversation, user messages and all, so its first is the one
|
|
223
|
+
# the log has no first_prompt row for yet.
|
|
222
224
|
def log_first_prompt(event, ctx)
|
|
223
225
|
return if event[:prompt].nil? || auto_attach_mode(ctx) != :offer
|
|
224
|
-
return if
|
|
225
|
-
return if ctx.scratch? || ctx.delegate? || ctx.session_id.nil?
|
|
226
|
+
return if ctx.scratch? || ctx.session_id.nil?
|
|
226
227
|
|
|
227
|
-
offers_log(ctx)
|
|
228
|
+
log = offers_log(ctx)
|
|
229
|
+
if Array(event[:messages]).any? { |message| message_role(message) == "user" }
|
|
230
|
+
return unless ctx.fork? && !log.first_prompt?(ctx.session_id)
|
|
231
|
+
elsif ctx.delegate?
|
|
232
|
+
return
|
|
233
|
+
end
|
|
234
|
+
log.write("first_prompt", session: ctx.session_id, prompt: event[:prompt].to_s[0, FIRST_PROMPT_CHARS])
|
|
228
235
|
rescue StandardError => e
|
|
229
236
|
ctx.log.debug(:pr_offers_log_failed, error: e.class.name)
|
|
230
237
|
end
|
|
@@ -365,6 +372,17 @@ class OffersLog
|
|
|
365
372
|
@log.debug(:pr_offers_log_failed, error: e.class.name)
|
|
366
373
|
nil
|
|
367
374
|
end
|
|
375
|
+
|
|
376
|
+
# Whether the log (or its old file) has session +id+'s first_prompt row.
|
|
377
|
+
def first_prompt?(id)
|
|
378
|
+
[@path, "#{@path}.1"].any? do |path|
|
|
379
|
+
File.exist?(path) && File.foreach(path).any? do |line|
|
|
380
|
+
line.include?(%("session":#{JSON.generate(id)})) && JSON.parse(line)["event"] == "first_prompt"
|
|
381
|
+
rescue JSON::ParserError
|
|
382
|
+
false
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
end
|
|
368
386
|
end
|
|
369
387
|
|
|
370
388
|
# A GitHub pull request, by its URL.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: samagotchi-system
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.45.0
|
|
4
4
|
scope: system
|
|
5
5
|
description: Default system memories — identity, self map, config modification protocol, memory guide and the delegated-session rules
|
|
6
6
|
files:
|
|
7
7
|
identity.md: sha256:29c2f7df9f6c5acbc5ec17000a4490741e55e426af15ca0d81c8706edd9bd027
|
|
8
|
-
self_map.md: sha256:
|
|
8
|
+
self_map.md: sha256:dfc19266259a9af1e0d769b7e0f67996b740b0c9921fdf3dadad973f6e5c144a
|
|
9
9
|
config_modification_protocol.md: sha256:2d4135ce4fafcd391d1782dc3a54f7cbf10cd35ebe9f004b2d6cb256517a8451
|
|
10
10
|
memory_guide.md: sha256:15cd3b24eda853cc458371c6c75809a2d4b5352311d075aaa4384926b5221c27
|
|
11
11
|
delegated.md: sha256:aedaa1e756a244904c5560fff0424e06fb468bbdcb0358bccefe321d67cd2614
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
## Start here
|
|
4
4
|
- `chi self` (via `execute`) prints my version, **source dir**, config path, hooks dir,
|
|
5
5
|
memory dirs, sessions dir, model/host and bundles. Use its source dir; don't hunt via `which`/`gem list`/`find /`.
|
|
6
|
-
Its `model` row is this session's model (live after `/model`), `model key` my overlay key
|
|
6
|
+
Its `model` row is this session's model (live after `/model`), `model key` my overlay key,
|
|
7
|
+
`model notes` the model notes my prompt loads (`/stats` names the ones this session's prompt carried).
|
|
7
8
|
- My current session id is in the system prompt; resume with `chi --resume <id>`.
|
|
8
9
|
- My debug log is the `log` line of `chi self` (default `$XDG_STATE_HOME/samagotchi/samagotchi.log`,
|
|
9
10
|
next to the sessions dir; not `~/.local/state` when `XDG_STATE_HOME` is set). One record per line,
|
|
@@ -10,9 +10,15 @@ module Samagotchi
|
|
|
10
10
|
# An entry the UI runs itself (/stats, /exit, …) is +local+: it is listed
|
|
11
11
|
# for Tab completion and help, and #lookup never returns it.
|
|
12
12
|
class Registry
|
|
13
|
+
# What a line does while a turn runs: runs now on its own thread
|
|
14
|
+
# (:anytime), runs after the turn ends (:queue), or is refused
|
|
15
|
+
# (:refuse, "busy").
|
|
16
|
+
MID_TURN = %i[anytime queue refuse].freeze
|
|
17
|
+
|
|
13
18
|
# @!attribute id [Symbol] the entry's key (:model, :rollback, …)
|
|
14
19
|
# @!attribute name [String] "/model", "!rollback", …
|
|
15
|
-
# @!attribute
|
|
20
|
+
# @!attribute mid_turn [Symbol, #call] what a line of it does while a
|
|
21
|
+
# turn runs (MID_TURN): a symbol, or a lambda line (stripped) → one
|
|
16
22
|
# @!attribute local [Boolean] the UI runs it; completion and help only
|
|
17
23
|
# @!attribute uis [Array<Symbol>, nil] a local entry's UIs (:repl,
|
|
18
24
|
# :attached) for completion; nil means every UI
|
|
@@ -21,9 +27,19 @@ module Samagotchi
|
|
|
21
27
|
# a built-in's with the line, and calls a bundle's with the text
|
|
22
28
|
# after the name
|
|
23
29
|
# @!attribute source [String] "core", or the bundle that added it
|
|
24
|
-
Entry = Struct.new(:id, :name, :description, :
|
|
30
|
+
Entry = Struct.new(:id, :name, :description, :mid_turn, :local, :uis, :match, :handler, :source,
|
|
25
31
|
keyword_init: true) do
|
|
26
32
|
def match?(text) = match.call(text)
|
|
33
|
+
|
|
34
|
+
# Whether every line of it runs at once, a turn running or not (D8).
|
|
35
|
+
def anytime = mid_turn == :anytime
|
|
36
|
+
|
|
37
|
+
# @param text [String] a line of it, stripped
|
|
38
|
+
# @return [Symbol] one of MID_TURN
|
|
39
|
+
def mid_turn_for(text) = mid_turn.respond_to?(:call) ? mid_turn.call(text) : mid_turn
|
|
40
|
+
|
|
41
|
+
# How a listing names #mid_turn: "depends" when it is the line's.
|
|
42
|
+
def mid_turn_label = mid_turn.respond_to?(:call) ? "depends" : mid_turn.to_s
|
|
27
43
|
def slash? = name.start_with?("/")
|
|
28
44
|
def in_ui?(ui) = uis.nil? || uis.include?(ui)
|
|
29
45
|
end
|
|
@@ -49,21 +65,36 @@ module Samagotchi
|
|
|
49
65
|
next if name.empty? || known.include?(name)
|
|
50
66
|
|
|
51
67
|
known << name
|
|
52
|
-
registry.register(name, item[:description].to_s,
|
|
68
|
+
registry.register(name, item[:description].to_s, mid_turn: listed_mid_turn(item), local: item[:local] == true,
|
|
53
69
|
uis: item[:uis]&.map(&:to_sym), source: item[:source] || "core")
|
|
54
70
|
end
|
|
55
71
|
registry
|
|
56
72
|
end
|
|
57
73
|
|
|
74
|
+
# A listed entry's #mid_turn: its mid_turn, else (an older worker's
|
|
75
|
+
# listing) its anytime. "depends" (a line's own, which only the
|
|
76
|
+
# worker's entry knows) and anything unknown are :refuse: the UI
|
|
77
|
+
# sends the line and the worker decides.
|
|
78
|
+
def self.listed_mid_turn(item)
|
|
79
|
+
label = item[:mid_turn]&.to_sym
|
|
80
|
+
return label if MID_TURN.include?(label)
|
|
81
|
+
return :refuse if label
|
|
82
|
+
|
|
83
|
+
item[:anytime] == true ? :anytime : :refuse
|
|
84
|
+
end
|
|
85
|
+
private_class_method :listed_mid_turn
|
|
86
|
+
|
|
87
|
+
# @param anytime [Boolean] mid_turn: :anytime (a bundle's command)
|
|
88
|
+
# @param mid_turn [Symbol, #call, nil] see Entry; nil takes +anytime+
|
|
58
89
|
# @param match [#call, nil] the default matches the name alone or the
|
|
59
90
|
# name, a space and arguments
|
|
60
91
|
# @return [Entry]
|
|
61
|
-
def register(name, description, id: nil, anytime: false, local: false, uis: nil, match: nil,
|
|
92
|
+
def register(name, description, id: nil, anytime: false, mid_turn: nil, local: false, uis: nil, match: nil,
|
|
62
93
|
source: "core", &handler)
|
|
63
94
|
raise ArgumentError, "command #{name} is already registered" if @entries.any? { |entry| entry.name == name }
|
|
64
95
|
|
|
65
96
|
entry = Entry.new(id: id || name.delete_prefix("/").to_sym, name: name, description: description,
|
|
66
|
-
anytime:
|
|
97
|
+
mid_turn: mid_turn || (anytime ? :anytime : :refuse), local: local, uis: uis, match: match || default_match(name),
|
|
67
98
|
handler: handler, source: source)
|
|
68
99
|
@entries << entry
|
|
69
100
|
entry
|
|
@@ -77,6 +108,19 @@ module Samagotchi
|
|
|
77
108
|
|
|
78
109
|
def command?(line) = !lookup(line).nil?
|
|
79
110
|
|
|
111
|
+
# Whether every line of +line+'s command runs at once, a turn running
|
|
112
|
+
# or not (D8). A line whose #mid_turn is :anytime but whose command's
|
|
113
|
+
# isn't always (/model) runs at once only while a turn runs.
|
|
114
|
+
def anytime?(line) = lookup(line)&.anytime == true
|
|
115
|
+
|
|
116
|
+
# What +line+ does while a turn runs (MID_TURN); :refuse for a line
|
|
117
|
+
# no command answers.
|
|
118
|
+
# @return [Symbol]
|
|
119
|
+
def mid_turn(line)
|
|
120
|
+
text = line.to_s.strip
|
|
121
|
+
lookup(text)&.mid_turn_for(text) || :refuse
|
|
122
|
+
end
|
|
123
|
+
|
|
80
124
|
# A line that is one word: a slash and a name, no spaces, no second
|
|
81
125
|
# slash. `/modle` is one; `/foo bar` and `/usr/bin/env` are not (they
|
|
82
126
|
# are prompts).
|
|
@@ -115,12 +159,13 @@ module Samagotchi
|
|
|
115
159
|
def entries = @entries.dup
|
|
116
160
|
|
|
117
161
|
# What a snapshot tells a UI without an Engine (see .from_listing).
|
|
118
|
-
# @return [Array<Hash>] {name:, description:, anytime:,
|
|
119
|
-
# source:} per entry, in registration order
|
|
162
|
+
# @return [Array<Hash>] {name:, description:, anytime:, mid_turn:,
|
|
163
|
+
# local:, uis:, source:} per entry, in registration order; anytime
|
|
164
|
+
# is true only when every line is (mid_turn "anytime")
|
|
120
165
|
def listing
|
|
121
166
|
@entries.map do |entry|
|
|
122
|
-
{ name: entry.name, description: entry.description, anytime: entry.anytime
|
|
123
|
-
local: entry.local ? true : false, uis: entry.uis&.map(&:to_s), source: entry.source }
|
|
167
|
+
{ name: entry.name, description: entry.description, anytime: entry.anytime,
|
|
168
|
+
mid_turn: entry.mid_turn_label, local: entry.local ? true : false, uis: entry.uis&.map(&:to_s), source: entry.source }
|
|
124
169
|
end
|
|
125
170
|
end
|
|
126
171
|
|
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -481,11 +481,16 @@ module Samagotchi
|
|
|
481
481
|
|
|
482
482
|
# The model notes the session's prompt carried at its last build
|
|
483
483
|
# (Session#prompt_notes): a resumed session's or a woken worker's saved
|
|
484
|
-
# ones until this process builds its prompt;
|
|
485
|
-
#
|
|
484
|
+
# ones until this process builds its prompt; a fresh session (no build
|
|
485
|
+
# yet, no messages) has none saved, so the effective model's prompt
|
|
486
|
+
# would load them: the notes #model_notes names, as without a session.
|
|
487
|
+
# The three readers (/model, /stats, the web) share this.
|
|
486
488
|
# @return [Array<PromptNote>]
|
|
487
489
|
def prompt_notes
|
|
488
|
-
return
|
|
490
|
+
return model_notes.map { |note| PromptNote.from_note(note) } unless @session
|
|
491
|
+
|
|
492
|
+
saved = @session.prompt_notes
|
|
493
|
+
return saved.dup if !@prompt_builder.prompt_notes.nil? || !saved.empty? || @session.messages.to_a.any?
|
|
489
494
|
|
|
490
495
|
model_notes.map { |note| PromptNote.from_note(note) }
|
|
491
496
|
rescue StandardError
|