samagotchi 0.43.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 +89 -1
- data/docs/cli.md +29 -7
- data/docs/configuration.md +18 -0
- data/docs/context.md +2 -1
- data/docs/internals/context-telemetry.md +4 -1
- data/docs/internals/engine.md +1 -0
- data/docs/plugins.md +4 -2
- data/docs/sessions.md +8 -4
- data/lib/samagotchi/bridge/card_store.rb +20 -9
- data/lib/samagotchi/bridge/turn_accumulator.rb +14 -0
- data/lib/samagotchi/bridge/turn_notice.rb +3 -1
- 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/context_status.rb +18 -5
- data/lib/samagotchi/engine.rb +8 -3
- data/lib/samagotchi/host_registry.rb +28 -3
- data/lib/samagotchi/kernel_loop.rb +24 -9
- data/lib/samagotchi/llm/chat_loop.rb +4 -3
- data/lib/samagotchi/llm_context_forget.rb +6 -2
- data/lib/samagotchi/llm_context_notice.rb +96 -0
- 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 +87 -4
- data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -0
- data/lib/samagotchi/terminal_ui/formatting.rb +26 -1
- data/lib/samagotchi/terminal_ui/one_shot_sink.rb +1 -0
- data/lib/samagotchi/terminal_ui.rb +84 -12
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +14 -13
- data/lib/samagotchi/web/message_parts.rb +90 -11
- data/lib/samagotchi/web/public/app.js +38 -19
- 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 +7 -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 +50 -4
- data/lib/samagotchi/web/public/turn_html.js +33 -2
- data/lib/samagotchi/web/public/turn_view.js +4 -2
- 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 +2 -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,92 @@ 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
|
+
|
|
79
|
+
## [0.44.0] - 2026-10-08
|
|
80
|
+
|
|
81
|
+
### Added
|
|
82
|
+
|
|
83
|
+
- LLM context edits are visible: each batch of stubs that reaches the prompt gets a ✂ row in its step, in the web
|
|
84
|
+
and the terminal (`✂ forgot 2 outputs, stubbed 1 stale read · frees ~4.1k tokens (paid off)`), whose web hover
|
|
85
|
+
lists the outputs, a stale stub's reason, a forget's note and kept lines, and the cost. After a reload a stubbed
|
|
86
|
+
output's tool row carries a ✂ mark (`✂ stubbed: superseded by a later read · ~1.0k tokens`, `✂ forgotten: <note>`,
|
|
87
|
+
`✂ forget staged`). The stream has a new event, `llm_context_edited`. See
|
|
88
|
+
[docs/configuration.md](docs/configuration.md) ("LLM context: what you see (✂)").
|
|
89
|
+
|
|
90
|
+
### Fixed
|
|
91
|
+
|
|
92
|
+
- Under an `llm_context.budget_tokens` budget without the forget layer (strategy `none` or `stale`), the model's
|
|
93
|
+
`[CONTEXT: …]` line said "of the context window" for a percentage of the budget; it now reads "about 62% of the
|
|
94
|
+
context budget (64k tokens) is in use", and its top bucket no longer asks the model to "summarize aggressively"
|
|
95
|
+
("context budget critical — avoid large outputs and re-reads, delegate broad work to subagents").
|
|
96
|
+
|
|
11
97
|
## [0.43.0] - 2026-10-08
|
|
12
98
|
|
|
13
99
|
### Added
|
|
@@ -2008,7 +2094,9 @@ and long-lived sessions.
|
|
|
2008
2094
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
2009
2095
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
2010
2096
|
|
|
2011
|
-
[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
|
|
2099
|
+
[0.44.0]: https://github.com/dm1try/samagotchi/compare/v0.43.0...v0.44.0
|
|
2012
2100
|
[0.43.0]: https://github.com/dm1try/samagotchi/compare/v0.42.0...v0.43.0
|
|
2013
2101
|
[0.42.0]: https://github.com/dm1try/samagotchi/compare/v0.41.0...v0.42.0
|
|
2014
2102
|
[0.41.0]: https://github.com/dm1try/samagotchi/compare/v0.40.0...v0.41.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/configuration.md
CHANGED
|
@@ -900,6 +900,24 @@ models:
|
|
|
900
900
|
- `/stats` counts the re-prefilled tokens each applied batch costs; `script/llm_context_bench.rb --strategy
|
|
901
901
|
forget_outputs` replays the offer on stored sessions (docs/internals/llm-context-bench.md).
|
|
902
902
|
|
|
903
|
+
## LLM context: what you see (✂)
|
|
904
|
+
|
|
905
|
+
Every edit is shown; the session keeps the originals.
|
|
906
|
+
|
|
907
|
+
- **A ✂ row** for each batch of stubs that reaches the prompt (`stale`'s, a `forget_outputs` call's, or the ones
|
|
908
|
+
staged until a turn's end), in the step it came in, in the web and the terminal alike:
|
|
909
|
+
`✂ forgot 2 outputs, stubbed 1 stale read · frees ~4.1k tokens (paid off)`. It says why it went now (`paid off`,
|
|
910
|
+
`the context is nearly full`, `at turn end`; nothing under `next_request`) and how many stubs stay staged. In the
|
|
911
|
+
web, its hover lists each output (its id, tool and path or command), a stale stub's reason, a forget's note once
|
|
912
|
+
with the lines it keeps, the tokens it frees and the tail the server reads again. A batch held back, the warm-up's
|
|
913
|
+
own copy and a restore show no row (the `forget_outputs` row says what a restore did). A reload puts the rows back
|
|
914
|
+
where they were (`cards.json`, its own cap of 20, apart from hook notices).
|
|
915
|
+
- **A ✂ mark** on a tool row after a reload, above its output, when the model is sent a stub for it:
|
|
916
|
+
`✂ stubbed: superseded by a later read · ~1.0k tokens`, `✂ forgotten: <note>`, `✂ forgotten with t41` (the
|
|
917
|
+
other outputs of the call whose row carries the note), `✂ forget staged`, and `· lines 12-40 kept`.
|
|
918
|
+
The running turn's rows get theirs on the next reload.
|
|
919
|
+
- Tokens are estimates (chars/4), hence the `~`.
|
|
920
|
+
|
|
903
921
|
## LLM context: a session's own strategy
|
|
904
922
|
|
|
905
923
|
A session can set its own `llm_context` strategy, apply rule and budget, which come before the model's, its host's
|
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
|
|
|
@@ -36,7 +36,10 @@ both loops):
|
|
|
36
36
|
|
|
37
37
|
- `llm_context.budget_tokens` (or a model's or host's `llm_context_budget_tokens`): the buckets count against the
|
|
38
38
|
budget instead of the window, the smaller of the two. The event's `window_tokens` and the status line's percentage
|
|
39
|
-
are of it too, and so is `payoff`'s top bucket.
|
|
39
|
+
are of it too, and so is `payoff`'s top bucket. Without the forget layer the model's line names it, `[CONTEXT:
|
|
40
|
+
about 62% of the context budget (64k tokens) is in use (…)]` (the window when that is the smaller), and its top
|
|
41
|
+
bucket reads "context budget critical — avoid large outputs and re-reads, delegate broad work to subagents": no
|
|
42
|
+
"summarize", as nothing in the model's hands shrinks the context.
|
|
40
43
|
- The forget layer replaces the guidance with tiered offers of `forget_outputs`, each line a readout,
|
|
41
44
|
`[CONTEXT: ~52k/64k tokens in use (bucket=60plus). …]`: the readout alone in the guided buckets below the top two
|
|
42
45
|
(CLM: how-to at low pressure makes a model wipe everything); "Finish the unit of work in flight, then tidy once
|
data/docs/internals/engine.md
CHANGED
|
@@ -298,6 +298,7 @@ Turn events (the main ones; `Events` in `events.rb` keeps the shared sets):
|
|
|
298
298
|
| `:generation_cancelled` | the turn was cancelled mid-loop | `{iteration, reason, stopped_by}` |
|
|
299
299
|
| `:empty_answer_retry` | an empty, cut or malformed answer is asked again | `{iteration, attempt, of, finish_reason, thinking_chars, stopped_by?, malformed?}` |
|
|
300
300
|
| `:steer_cut` | a steer cut the generation | `{iteration, source}` |
|
|
301
|
+
| `:llm_context_edited` | the apply rule applied a batch of LLM context edits (a request, a forget_outputs call, turn end; never the warm-up) | `{moment, why, freed_tokens, tail_tokens, staged, text, groups}` (`LLMContextNotice`) |
|
|
301
302
|
| `:pending_input_merged` | queued input joined the conversation | `{iteration, count, content, steers?, answer}` |
|
|
302
303
|
| `:tool_dispatch_started` / `:tool_dispatch_completed` | around a batch | `{iteration, call_count}` |
|
|
303
304
|
| `:tool_call_started` | before a call runs | see ToolRunner |
|
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.
|
|
@@ -16,7 +16,8 @@ module Samagotchi
|
|
|
16
16
|
# iteration started before it (a before_tool_call hook's notice comes
|
|
17
17
|
# before its call's row, so the web puts it above row calls + 1). The
|
|
18
18
|
# loop's "asking again" row (:empty_answer_retry, after an empty or cut
|
|
19
|
-
# answer)
|
|
19
|
+
# answer), a steer's cut row (:steer_cut) and a ✂ row (:llm_context_edited,
|
|
20
|
+
# LLMContextNotice) are kept the same way, and so is a question the turn asked
|
|
20
21
|
# (:question_requested), with how it was answered or cancelled, so a
|
|
21
22
|
# reload draws the resolved card where it was.
|
|
22
23
|
#
|
|
@@ -28,9 +29,10 @@ module Samagotchi
|
|
|
28
29
|
# as /btw) goes after that turn: its prompt is already in the history.
|
|
29
30
|
# A failed turn leaves no prompt, so its cards stay before the next one.
|
|
30
31
|
#
|
|
31
|
-
# Notices (hook notices, load warnings, "asking again" rows)
|
|
32
|
-
# (with questions) are capped apart, +capacity+ each, so a
|
|
33
|
-
# notices can't push out a card
|
|
32
|
+
# Notices (hook notices, load warnings, "asking again" rows), ✂ rows
|
|
33
|
+
# and cards (with questions) are capped apart, +capacity+ each, so a
|
|
34
|
+
# burst of notices can't push out a card, nor a long turn's ✂ rows a
|
|
35
|
+
# hook's notice. An open question (an approval too) is
|
|
34
36
|
# never pushed out: the oldest other card or answered question goes.
|
|
35
37
|
#
|
|
36
38
|
# With a +path+ (the Bridge's: FILE in the session's folder) the store
|
|
@@ -47,11 +49,14 @@ module Samagotchi
|
|
|
47
49
|
class CardStore
|
|
48
50
|
CAPACITY = 20
|
|
49
51
|
NOTICE_TYPES = %i[hook_notice guardrail_warning empty_answer_retry steer_cut].freeze
|
|
52
|
+
# Capped apart from the notices.
|
|
53
|
+
LLM_CONTEXT_TYPES = %i[llm_context_edited].freeze
|
|
54
|
+
LLM_CONTEXT_FIELDS = %i[type moment why freed_tokens tail_tokens staged text groups].freeze
|
|
50
55
|
FILE = "cards.json"
|
|
51
56
|
# The events after which the file is written again: the ones that
|
|
52
57
|
# change an entry or the turn count.
|
|
53
|
-
SAVED_ON = (%i[card hook_notice empty_answer_retry steer_cut question_requested question_answered
|
|
54
|
-
question_relay turn_failed] + Events::TURN_KEPT).freeze
|
|
58
|
+
SAVED_ON = (%i[card hook_notice empty_answer_retry steer_cut llm_context_edited question_requested question_answered
|
|
59
|
+
question_cancelled question_relay turn_failed] + Events::TURN_KEPT).freeze
|
|
55
60
|
|
|
56
61
|
# What a session's saved file lists, as a store started from it lists
|
|
57
62
|
# (no turn running): the web's cards for a session no worker runs.
|
|
@@ -167,6 +172,7 @@ module Samagotchi
|
|
|
167
172
|
.compact.merge(in_turn: false, turns: @turns_done))
|
|
168
173
|
when :empty_answer_retry then add_turn_row(event.slice(:type, :attempt, :of, :stopped_by, :malformed)) if @running
|
|
169
174
|
when :steer_cut then add_turn_row(event.slice(:type, :source)) if @running
|
|
175
|
+
when :llm_context_edited then add_turn_row(Marshal.load(Marshal.dump(event.slice(*LLM_CONTEXT_FIELDS)))) if @running
|
|
170
176
|
when :question_requested
|
|
171
177
|
add_turn_row({ type: :question, pending_question: Marshal.load(Marshal.dump(event[:pending_question])) }) if @running
|
|
172
178
|
when :question_answered then resolve_question(event[:id], answer: event[:answer])
|
|
@@ -236,15 +242,20 @@ module Samagotchi
|
|
|
236
242
|
|
|
237
243
|
def push(entry)
|
|
238
244
|
@entries << entry
|
|
239
|
-
|
|
240
|
-
kind = @entries.select { |e|
|
|
245
|
+
cap = cap_class(entry)
|
|
246
|
+
kind = @entries.select { |e| cap_class(e) == cap }
|
|
241
247
|
return if kind.size <= @capacity
|
|
242
248
|
|
|
243
249
|
evict = kind.find { |e| !open_question?(e) }
|
|
244
250
|
@entries.delete_at(@entries.index { |e| e.equal?(evict) }) if evict
|
|
245
251
|
end
|
|
246
252
|
|
|
247
|
-
|
|
253
|
+
# Which cap +entry+ counts against: :notice, :llm_context or :card.
|
|
254
|
+
def cap_class(entry)
|
|
255
|
+
return :llm_context if LLM_CONTEXT_TYPES.include?(entry[:type])
|
|
256
|
+
|
|
257
|
+
NOTICE_TYPES.include?(entry[:type]) ? :notice : :card
|
|
258
|
+
end
|
|
248
259
|
|
|
249
260
|
def open_question?(entry)
|
|
250
261
|
entry[:type] == :question && !entry.key?(:answer) && !entry[:cancelled]
|
|
@@ -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]
|
|
@@ -4,7 +4,8 @@ module Samagotchi
|
|
|
4
4
|
class Bridge
|
|
5
5
|
# The events a running turn shows as rows of its own (not text, thinking
|
|
6
6
|
# or a tool call): a hook's notice, the loop asking again after an empty
|
|
7
|
-
# or cut answer, a
|
|
7
|
+
# or cut answer, a batch of LLM context edits (✂), a question and how it
|
|
8
|
+
# was answered. A UI that joins the
|
|
8
9
|
# turn mid-way (TurnAccumulator's "notice" part) and one that reloads a
|
|
9
10
|
# finished turn (CardStore) get the event back with the fields listed
|
|
10
11
|
# here, and draw it with the handler that drew it live, where it was.
|
|
@@ -13,6 +14,7 @@ module Samagotchi
|
|
|
13
14
|
hook_notice: %i[hook text level fallback_for],
|
|
14
15
|
empty_answer_retry: %i[iteration attempt of stopped_by malformed],
|
|
15
16
|
steer_cut: %i[iteration source],
|
|
17
|
+
llm_context_edited: %i[moment why freed_tokens tail_tokens staged text groups],
|
|
16
18
|
question_requested: %i[pending_question],
|
|
17
19
|
question_answered: %i[id answer],
|
|
18
20
|
question_cancelled: %i[id reason]
|
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
|