samagotchi 0.154.0 → 0.154.1

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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -1
  3. data/docs/cli.md +9 -7
  4. data/docs/internals/bridge.md +18 -5
  5. data/docs/internals/engine.md +1 -1
  6. data/docs/internals/prompt-caching.md +3 -1
  7. data/docs/internals/web-frontend.md +2 -2
  8. data/docs/sessions.md +1 -1
  9. data/docs/testing.md +15 -0
  10. data/lib/samagotchi/answer_text.rb +1 -2
  11. data/lib/samagotchi/bridge/card_places.rb +56 -7
  12. data/lib/samagotchi/bridge/card_store.rb +24 -9
  13. data/lib/samagotchi/bridge.rb +9 -0
  14. data/lib/samagotchi/bridge_client.rb +9 -5
  15. data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
  16. data/lib/samagotchi/bundles/btw/manifest.yml +2 -2
  17. data/lib/samagotchi/bundles/btw/plugin.rb +1 -1
  18. data/lib/samagotchi/bundles/coordinator/manifest.yml +2 -2
  19. data/lib/samagotchi/bundles/coordinator/plugin.rb +3 -2
  20. data/lib/samagotchi/bundles/github-pr/manifest.yml +2 -2
  21. data/lib/samagotchi/bundles/github-pr/plugin.rb +3 -3
  22. data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
  23. data/lib/samagotchi/bundles/skills/plugin.rb +3 -2
  24. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  25. data/lib/samagotchi/desktop/macos/ChiRunner.swift +1 -1
  26. data/lib/samagotchi/engine_snapshots.rb +9 -8
  27. data/lib/samagotchi/idle_recap.rb +3 -1
  28. data/lib/samagotchi/kernel_loop.rb +4 -3
  29. data/lib/samagotchi/llm/chat_loop.rb +3 -1
  30. data/lib/samagotchi/session_manager.rb +8 -13
  31. data/lib/samagotchi/session_retention.rb +24 -4
  32. data/lib/samagotchi/steer.rb +12 -1
  33. data/lib/samagotchi/stream_event.rb +22 -0
  34. data/lib/samagotchi/system_prompt.rb +52 -36
  35. data/lib/samagotchi/terminal_ui/attached_view.rb +0 -9
  36. data/lib/samagotchi/terminal_ui/input_support.rb +16 -15
  37. data/lib/samagotchi/terminal_ui/join_replay.rb +16 -12
  38. data/lib/samagotchi/terminal_ui/reline_seam.rb +0 -10
  39. data/lib/samagotchi/terminal_ui/status_row.rb +0 -7
  40. data/lib/samagotchi/tool_runner.rb +2 -1
  41. data/lib/samagotchi/turn_flow.rb +7 -2
  42. data/lib/samagotchi/version.rb +1 -1
  43. data/lib/samagotchi/web/app.rb +3 -1
  44. data/lib/samagotchi/web/message_display.rb +7 -0
  45. data/lib/samagotchi/web/public/card_stamps.js +27 -0
  46. data/lib/samagotchi/web/public/cards_place.js +5 -3
  47. data/lib/samagotchi/web/public/composer_mode.js +1 -1
  48. data/lib/samagotchi/web/public/format.js +3 -2
  49. data/lib/samagotchi/web/public/turn_html.js +8 -4
  50. data/lib/samagotchi/web/session_access_routes.rb +8 -0
  51. data/lib/samagotchi/web/session_show.rb +4 -0
  52. data/lib/samagotchi/worker.rb +8 -5
  53. data/lib/samagotchi.rb +0 -1
  54. metadata +3 -2
  55. data/lib/samagotchi/pending_input_queue.rb +0 -55
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 43fce920e24b60bc0bb9b5211dbc894f272e4f82ea55e6568698b533083f0c66
4
- data.tar.gz: 5af718499f366587969cef9318c16e271fa343a69362f1ba5ed227992d2ab4d8
3
+ metadata.gz: 9533994f61b5607aef4d2dc6318cd6e885a0331c163643bd06d3c16f5e6f03c0
4
+ data.tar.gz: b768811c50bb587fcf4d16696ab69b857c75168067fd521270d871954169874e
5
5
  SHA512:
6
- metadata.gz: 5f3a52420f92d4335f0c156cd208b5b40d8b952a692861d2ca0e6672caea3d66bb033f6e04f0c3288fe6415ae48c63d8489ef8be6cd24c57086861b3117c7746
7
- data.tar.gz: d9a8a9c3cc79549bf476f43900669ac68e212ed419c76ec362b8e9483d937eb70537f00b9d63a3e904417205dd42db069662e13127ee3cadf710b18bdc4fa058
6
+ metadata.gz: 2064776b06d42184107924bc44391f46681d755437e2c98a2e7c24205e900aba33bd8d3d792c7cea4dd6bdbf2813870395112db7315dbec33f92e198267c4ddc
7
+ data.tar.gz: a826305fa133e9ca4d1bd62e112ca7464fcf932f91292dc4112286e7f8f501c167fc18ccfeeb12dd24bec649e24636c810a715417cac45f454a19b4ba56f2b05
data/CHANGELOG.md CHANGED
@@ -8,6 +8,19 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.154.1] - 2026-11-07
12
+
13
+ ### Fixed
14
+
15
+ - chi web refuses a scratch session's page and stream (409 `scratch`, in the words every other client gets) when a tab is opened at its id by hand, and a scratch worker counts only its terminal's stream: such a tab no longer keeps the worker up after its terminal is killed.
16
+ - A card in chi web comes back where it was after a reload: the page finds the turn it came after (or `/continue no`'s reason line) by its id instead of counting the turns drawn after it, so the page and the worker can no longer disagree on the count.
17
+ - A Qwen model's turn that was asked to finish an open tool call no longer splits in two after a reload of chi web: the request to finish it is part of the turn, drawn as nothing, as it was live.
18
+ - chi web starting just as `chi scratch` did no longer deletes the new scratch session before its worker takes it (the worker crashed with "Session not found" and the terminal said the Bridge did not start in time): the retention sweep, `chi sessions prune` and `clean` leave an unowned scratch session alone for 30 s after its last save.
19
+ - A terminal joining a session (`chi --attach`, `--resume`) shows the last turn's own prompt instead of a Qwen model's request to finish an open tool call, with the input merged into that turn on its `input>` lines; the idle recap no longer counts either as a turn of its own.
20
+ - A turn no longer fails when a plugin changes its tools or prompt section (or a profile change drops the system prompt) at the moment the system prompt is built.
21
+
22
+ Update with `chi update` (btw 0.1.5, coordinator 0.8.1, github-pr 0.6.4, skills 0.1.10: comments only; the desktop helper rebuilds once).
23
+
11
24
  ## [0.154.0] - 2026-11-07
12
25
 
13
26
  ### Changed
@@ -5759,7 +5772,8 @@ and long-lived sessions.
5759
5772
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
5760
5773
  hotkey panel that send selected text or the clipboard to your sessions.
5761
5774
 
5762
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.154.0...HEAD
5775
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.154.1...HEAD
5776
+ [0.154.1]: https://github.com/dm1try/samagotchi/compare/v0.154.0...v0.154.1
5763
5777
  [0.154.0]: https://github.com/dm1try/samagotchi/compare/v0.153.0...v0.154.0
5764
5778
  [0.153.0]: https://github.com/dm1try/samagotchi/compare/v0.152.0...v0.153.0
5765
5779
  [0.152.0]: https://github.com/dm1try/samagotchi/compare/v0.151.0...v0.152.0
data/docs/cli.md CHANGED
@@ -460,7 +460,7 @@ invalid option (exit 1).
460
460
  | `--llm-context-budget TOKENS` | This session's own context budget: `64000`, `64k` (4k to 10M), or `off` (wins over the model's budget). |
461
461
  | `--no-interrupt` | Raise the tool-call limit to 1000 iterations for long tasks. |
462
462
  | `--no-default-input` | Skip the default input (`SAMAGOTCHI_DEFAULT_INPUT`) typed into a new session's first prompt. |
463
- | `-v`, `--verbose` | Log at debug level (raw LLM responses, tool call/result payloads). Attached, the terminal shows this session's debug records above the prompt as they come, and a worker this chi starts logs at debug too; a worker that was already running keeps its own level (a one-line note says so). With `--non-interactive`, every log record goes to stderr too. This machine only (exit 2 with `--remote`). |
463
+ | `-v`, `--verbose` | Log at debug level (raw LLM responses, tool call/result payloads). Attached, the terminal shows this session's debug records above the prompt as they come, and a worker this chi starts logs at debug too; a worker that was already running keeps its own level (a one-line note says so). `chi web -v` logs at debug too (its `/api/` requests among them) and so does every worker it starts from then on (a new session, a wake, a restart: they all take `SAMAGOTCHI_LOG_LEVEL=debug`, `Config.cli_env`), into the same log; a worker already running keeps its level. With `--non-interactive`, every log record goes to stderr too. This machine only (exit 2 with `--remote`). |
464
464
  | `--version` | Print `chi <version>` and exit (`chi self` shows it with the paths). |
465
465
 
466
466
  Every setting in the config registry (`lib/samagotchi/config.rb`) that exposes a CLI
@@ -537,13 +537,15 @@ refused. Its first line says it is a scratch session.
537
537
  session (`Scratch session deleted.`). `/archive` is refused. The lines typed
538
538
  are not added to the prompt history. With `-p … --non-interactive` it is one
539
539
  turn in this process, and the session is deleted as the run ends.
540
- - The worker deletes the session too once its terminal is gone (no stream
541
- left for about 25 s, twice the terminal's reconnect retries, so a dropped
542
- stream that reconnects keeps it; or none came within 30 s), so a closed
543
- terminal window or a `kill -9` of the terminal leaves nothing: a turn still
544
- running then is cancelled first. It refuses `chi sessions restart`.
540
+ - The worker deletes the session too once its terminal is gone (no terminal
541
+ stream left for about 28 s, twice the terminal's reconnect retries, so a
542
+ dropped stream that reconnects keeps it; or none came within 30 s), so a
543
+ closed terminal window or a `kill -9` of the terminal leaves nothing: a turn
544
+ still running then is cancelled first. Another client's stream never keeps
545
+ it. It refuses `chi sessions restart`.
545
546
  - It never shows in `chi web`, and only its terminal uses it: `chi send`, the
546
- web's routes, `chi --resume` and `--attach` refuse it (exit 1), in one
547
+ web's routes (a tab opened at its id too: its page and stream answer 409
548
+ `scratch`), `chi --resume` and `--attach` refuse it (exit 1), in one
547
549
  wording: "that's a chi scratch session: only the terminal that started it
548
550
  uses it (a leftover one is deleted at the next sweep: chi sessions clean)".
549
551
  A session left behind when both died is marked `"scratch": true` in its
@@ -16,7 +16,7 @@ rescues its own errors: an observer must never break the running turn.
16
16
  | `RingBuffer` | `ring_buffer.rb` | The last `ring_capacity` (256) raw events by `event_seq`, in memory, for a reconnect's replay. The capture observer feeds it. |
17
17
  | `TurnAccumulator` | `turn_accumulator.rb` | The turn in progress, folded from events. |
18
18
  | `CardStore` | `card_store.rb` | The last cards and notices, saved in `cards.json`, each anchored to its turn. |
19
- | `CardPlaces` | `card_places.rb` | Pure: where `CardStore`'s entries sit in the messages the page draws (`turns_since`). |
19
+ | `CardPlaces` | `card_places.rb` | Pure: where `CardStore`'s entries sit in the messages the page draws (`turns_since`, and the `opening_id` stamp). |
20
20
  | `PendingCard` | `pending_card.rb` | `pending_card.json` `{id:, bundle:}`: the running turn's open card with actions (check-in's Nudge), written and removed with that card, so chi web's hub, which watches files and not workers, can notify. |
21
21
 
22
22
  Per connection there is an `SSEWriter` (with a `BoundedQueue`); `ClientEvent` and `EventId` are pure helpers. The
@@ -57,7 +57,9 @@ the "asking again" and steer-cut rows, ✂ rows and questions (with how they wer
57
57
  the page's `TURN_PROMPT` and the recap count) after the last one whose id is in the log at or before the entry's, else from
58
58
  the first one whose id is later in the log, else 0. The log only orders; the messages count. So a turn with no opening (a
59
59
  reminder, a Continue, a failed or rolled-back one, a prompt stopped before the model) moves nothing, a wake's note does, and
60
- an opening with no id (`/continue no`'s reason, Qwen's recovery line) counts as on the page. Nothing orders such an opening
60
+ an opening with no turn id (`/continue no`'s reason, which carries an `opening_id` of its own instead, `TurnFlow#abort_continue!`)
61
+ counts as on the page. KernelLoop's Qwen recovery line (kind `recovery`, `Steer.recovery?`) is a row of its turn, no opening:
62
+ the page draws nothing for it (`MessageDisplay`), as live. Nothing orders such an opening
61
63
  against an entry, so the store reads the session's messages (its `messages:` lambda) when an entry is shown between turns, and
62
64
  at the turn's end for what a running turn showed that isn't its own: `past_idless` is how many of them, right after the opening
63
65
  it counts from, the entry already came after, and they don't count for it. A `!rollback` usually only shortens that run, and the count
@@ -66,7 +68,15 @@ the "asking again" and steer-cut rows, ✂ rows and questions (with how they wer
66
68
  anchored to T2, now counts from T1 and reads above the reason and the first card). That rare inversion is accepted. A turn's own rows don't read it: they count such a line, which sits below them. `current` is a card of the running turn. A card that
67
69
  comes mid-turn but isn't the turn's (an anytime command's, such as `/btw`) goes after that turn (`during`), and so does a
68
70
  between-turns notice. A card with an earlier card's id replaces it in place and keeps its anchor. A `list` with no messages
69
- (`?cards=1`'s bodies, `chi bundle try`'s trace) has no `turns_since`.
71
+ (`?cards=1`'s bodies, `chi bundle try`'s trace) has no `turns_since` and no `opening_id`.
72
+ - **The stamp.** Next to `turns_since`, an entry lists `opening_id` (`CardPlaces#opening_of`): the id of the opening its count
73
+ reaches, the one it comes after: a prompt's `turn_id` (a wake note's too), or `/continue no`'s reason line's `opening_id`. It is
74
+ resolved at `list` time from the same anchor (an entry's own turn often has no bubble: a Continue, a failed or rolled-back turn),
75
+ so the stamp and the count always name one place, and the page finds it by the bubble (`card_stamps.js`: `data-turn-id`, or `data-opening-id` on a wake's note or a reason line), not by counting
76
+ bubbles. An entry shown after reason lines also saves which one it came after (`after_opening`, beside `past_idless`): read
77
+ first while that line is still in the run, so a later release can drop the count. No stamp (the page counts `turns_since`) for
78
+ an entry before every opening, after a line an older chi wrote with no id, or from a version 1 file (`base_since`). A rollback
79
+ keeps today's rule: a rolled-back turn's cards name the opening before it.
70
80
  - **A turn that leaves nothing** is read, not told: an in-turn entry not `current` whose turn has no opening in the messages
71
81
  (a `!rollback`, a failed turn with nothing kept, a prompt stopped before the model) lists as a between-turns entry at its
72
82
  place, a card without its actions and a hook's notice without its step; its other rows, having no step to sit in, aren't
@@ -84,7 +94,9 @@ the "asking again" and steer-cut rows, ✂ rows and questions (with how they wer
84
94
  is what chi web reads for a session no worker runs. The file is `{version: 2, turn_log:, entries:}`. The log is pruned at
85
95
  a turn's start and end and at an idle show (a running turn's rows reuse the start's, as finding it walks the messages) to start at the opening the earliest anchor counts from (all of it while an entry has none, or for a store with no
86
96
  `messages:`), so a restart reads an entry as the live store did; an old card therefore keeps it whole. `seed` drops an entry
87
- whose `turn_id`, `base_since` or `past_idless` isn't an id or a non-negative integer. Load warnings
97
+ whose `turn_id`, `after_opening`, `base_since` or `past_idless` isn't an id or a non-negative integer. A file 0.154.0 wrote
98
+ (no `after_opening`) lists the same stamps, from its counts; an older chi reading this one keeps `after_opening` (it seeds
99
+ entries whole) and lists it, unused. Load warnings
88
100
  are the worker's own (each start announces them) and an unanswered question waits for no one: neither is saved or seeded. An
89
101
  entry from an earlier worker lists with `earlier: true`; one shown during a turn comes back **without its actions** (their
90
102
  turn no longer runs), while a between-turns card keeps its buttons.
@@ -152,7 +164,8 @@ The Bridge exposes:
152
164
  `?from_seq=` (a plain `event_seq` is still accepted); a `: ping` heartbeat keeps idle proxies alive;
153
165
  too-old reconnects, and cursors from another worker's epoch, receive a `reset` marker carrying
154
166
  `session_state_snapshot`. `?snapshot=1` joins with a snapshot frame instead of a replay;
155
- `?client_id=` names whose stream it is (`Bridge#open_streams_except`, used by `POST /exit`).
167
+ `?client_id=` names whose stream it is (`Bridge#open_streams_except`, used by `POST /exit`; `Bridge#open_streams_of`,
168
+ by a scratch worker, which counts only `tui:` streams: a web tab's, through chi web, names none).
156
169
  Each event goes out as clients get it (`Bridge::ClientEvent.project`, in `SSEWriter#write_event`, live
157
170
  and replayed alike); `generation_chunk`, `tool_call_started` and `turn_completed` are projected to the
158
171
  contracts below. The ring retains raw events for replay and `?raw=1`.
@@ -176,7 +176,7 @@ Stopped before the model was asked (prepare_turn returned false):
176
176
  **Degrading.** Work that has a fallback when it fails (a prompt section left out, an empty list from the Bridge) runs
177
177
  in `Degrade.call(area, event, fallback:, rescue:) { … }` (`degrade.rb`): same rescue class and fallback as a bare rescue,
178
178
  plus one WARN in the debug log per process for each `[area, event]` (DEBUG after; nothing on stderr). A bare `rescue`
179
- whose body is only a literal (`nil`, `false`, `[]`, `{}`, `""`, `0`) is flagged in lib by the `Samagotchi/SilentRescue`
179
+ whose body is only a literal (`nil`, `false`, `[]`, `{}`, `""`, `0`, or an array of only those: `[nil, nil]`) is flagged in lib by the `Samagotchi/SilentRescue`
180
180
  cop; a silence that is right (a client gone, a routine probe, Log's own internals) carries `# degrade-ok: why`.
181
181
 
182
182
  ## The model↔tool loop
@@ -67,7 +67,9 @@ right-hand column happens.
67
67
  the loop, the thinking level, the turn's LLM context layers; a `/thinking` change builds under another key). All
68
68
  of them are dropped only on `@prompt_builder.reset!`: a model switch (`Engine#switch_model!`), a plugin's
69
69
  `tools_changed!` or `prompt_changed!` (`Engine#prompt_changed!`), a profile change, or a toolkit change
70
- (`Engine#apply_toolkit!`). A per-turn rebuild would break everything below it.
70
+ (`Engine#apply_toolkit!`). A per-turn rebuild would break everything below it. `reset!` swaps in a new cache
71
+ (no lock; it runs on plugin and Bridge threads, and inside a build through the profile lookup): a build it lands
72
+ in returns the same bytes but is not kept, so the next build reads the sources again.
71
73
  - The tools layer is shared **per toolkit** (configuration.md, "Toolkits"): sessions on the same toolkit share it,
72
74
  and a chain's links keep theirs. A session's toolkit is saved when it is created and never resolved again from
73
75
  config. A change (`/toolkit`, `ctx.use_toolkit`) is applied only at a turn's start (`Engine#apply_toolkit!`,
@@ -24,7 +24,7 @@ page. A factory dep a new version stops passing is the same hazard the other way
24
24
  out of `send_turn.js`, `session_actions.js` and `lists_view.js`); a page loaded before keeps all its old modules
25
25
  until a reload, so the risk is a mixed cache, not an open tab. `answer_fold.js`, `details_motion.js`,
26
26
  `turn_range.js`, `history_reveal.js`, `latest.js`, `session_payload.js`, `method_api.js`, `session_store.js`,
27
- `session_fields.js`, `feature_gates.js`, `revise_stack.js`, `revise_switch.js`, `shell_command.js`, `stale_params.js`, `popover_modal.js`, `more_sheet.js`, `hash_intent.js` (what the hash asks for: session, all, start or keep), `worker_features.js`, `version_notices.js`, `owner_claim.js`, `owner_verify.js`, `composer_mode.js`, `strip_hidden.js`, `block_open.js`, `text_roots.js`, `block_source.js`, `block_frame.js`, `block_host.js` and the `objectives_*.js` modules are new modules for that reason. The last three `block_*` draw a bundle's fenced blocks: [block-renderers.md](block-renderers.md).
27
+ `session_fields.js`, `feature_gates.js`, `revise_stack.js`, `revise_switch.js`, `shell_command.js`, `stale_params.js`, `popover_modal.js`, `more_sheet.js`, `hash_intent.js` (what the hash asks for: session, all, start or keep), `worker_features.js`, `version_notices.js`, `owner_claim.js`, `owner_verify.js`, `composer_mode.js`, `strip_hidden.js`, `block_open.js`, `text_roots.js`, `block_source.js`, `block_frame.js`, `block_host.js`, `card_stamps.js` and the `objectives_*.js` modules are new modules for that reason. The last three `block_*` draw a bundle's fenced blocks: [block-renderers.md](block-renderers.md).
28
28
 
29
29
  ## Two pages, two models
30
30
 
@@ -51,7 +51,7 @@ change.
51
51
  | **`store.current`** (`session_state.js`: `emptySessionState` / `sessionStateFrom`; per field: `session_fields.js FIELDS`) | the open session's fields, about 40 (info bar, chips, verdicts, timing, worker, stream cursor) | `store.update` / `setStatus` from app.js, `stream_handlers.js`, `send_turn.js` (`firstPreview`, `owner`), `session_actions.js` (`status`), `owner_verify.js` (`verdicts`, `verifyFresh`); `forgetSession`'s `replaceCurrent`. Two writers in place, on purpose (D8 in `FIELDS`): live_timing.js's `activeTurn`, verdict_fresh.js's running list | the store runs the field's redraws (`FIELDS[key].redraws`) |
52
52
  | **`store.sessions`** (the hub's projection) | every card | `store.update({sessions})` from app.js: `openSessionEvents` (`applySessionEvent`), `refresh` (the list fallback), `addCreatedSession`, `applyView` (re-sorted), `replaceSelectedCard` (`setLiveStatus` through `setStatus`, `readTurnEnd`, `setUsedMemories`), the stream's close (`withoutWorker`); `session_actions.js` (`setSessions`) | the store: `lists` and `infoBar`. **Always a new array**; an entry is replaced, never changed in place. `followSelectedSession` copies the open one's owner and worker (`fieldsFromCard`) and, when not streaming, its status (`setStatus(…, "hub")`) into `store.current` |
53
53
  | **Factory closures**: the `create*` factories (popovers, panels, views) | each component's own state: open-for, busy, error, its own fetched list | the factory | an `onChange` hook back into app.js, or a getter the factory reads at draw time |
54
- | **The history DOM** | the conversation as drawn: which turn a bubble belongs to (`data-turn-id`, `data-answer-turn`), what opens a turn (`TURN_PROMPT`, `bubbles.js`: a prompt or a wake's note, the same turns `Steer.turn_opening?` counts for a card's or the recap's `turns_since`), kept command bubbles, which texts were shown, unnamed badges, folded answers and long prompts (`.folded`), Revise stamps (`data-revises` on a prompt; a Verify turn's `data-verify`) and the stacks' folded versions (`.stack-hidden`, `.stack-stub.open`); the switcher's version on view is `revise_stack.js`'s own (a copy `.stack-view` before the top answer, the top `.stack-swapped`; never stored) | `bubbles.js`, `cards_place.js`, `question_cards.js`, `turn_block.js`/`stage_view.js`, `answer_fold.js`, `revise_stack.js`, `stream_handlers.js` (a live `turn_completed`'s `revises` and `verify` onto its prompt) | read back as state: `turn_range.js turnElements` (one walker for a turn's elements), `keptCommandBubbles`, `seedSeenFromDom`, `nameUnnamedBadges`/`nameVerdictCards`, `answer_fold.js` and `revise_stack.js` (their observers decorate every path that fills `#history`; the stacks are `stacks()` of the stamps, read again on each change, never a store field), `history_reveal.js revealInHistory` (the one way a jump unfolds what hides its target) |
54
+ | **The history DOM** | the conversation as drawn: which turn a bubble belongs to (`data-turn-id`, `data-answer-turn`), what opens a turn (`TURN_PROMPT`, `bubbles.js`: a prompt or a wake's note, the same turns `Steer.turn_opening?` counts for a card's or the recap's `turns_since`), an opening's id for a card's stamp (`data-turn-id` on a prompt, `data-opening-id` on a wake's note or `/continue no`'s reason line; `card_stamps.js` places a snapshot's card after the stamped opening, by its count only without one), kept command bubbles, which texts were shown, unnamed badges, folded answers and long prompts (`.folded`), Revise stamps (`data-revises` on a prompt; a Verify turn's `data-verify`) and the stacks' folded versions (`.stack-hidden`, `.stack-stub.open`); the switcher's version on view is `revise_stack.js`'s own (a copy `.stack-view` before the top answer, the top `.stack-swapped`; never stored) | `bubbles.js`, `cards_place.js`, `question_cards.js`, `turn_block.js`/`stage_view.js`, `answer_fold.js`, `revise_stack.js`, `stream_handlers.js` (a live `turn_completed`'s `revises` and `verify` onto its prompt) | read back as state: `turn_range.js turnElements` (one walker for a turn's elements), `keptCommandBubbles`, `seedSeenFromDom`, `nameUnnamedBadges`/`nameVerdictCards`, `answer_fold.js` and `revise_stack.js` (their observers decorate every path that fills `#history`; the stacks are `stacks()` of the stamps, read again on each change, never a store field), `history_reveal.js revealInHistory` (the one way a jump unfolds what hides its target) |
55
55
  | **localStorage** (`storage.js` `readStored`/`writeStored`, guarded) | per-browser choices only | the module that owns the choice | read at start (see Storage keys) |
56
56
  | **SSE** | see Streams | the server | owner page: events go into the DOM directly; participant page: an event marks the data stale |
57
57
 
data/docs/sessions.md CHANGED
@@ -110,7 +110,7 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
110
110
 
111
111
  - New sessions set `test_run:true` when `SAMAGOTCHI_ENV=test` or `RACK_ENV=test` or `CI` is set (explicit flag, `metadata_version` 2). Old sessions without the flag load as `test_run:false`.
112
112
  - Test runs are tagged and obey the same retention. `chi sessions clean` deletes every test session whatever its age (`--days N`: only those older than N days); a live worker or a `keep_status` status still keeps one. `chi sessions prune --test-only` applies the usual age and count rules to test sessions only.
113
- - A `chi scratch` session (`"scratch": true`) is deleted when its terminal leaves (its worker deletes it too once its last client is gone); one left behind when both died is deleted by the next sweep, `prune` or `clean`, whatever its age or status, once nobody owns it. `list` shows it as `[scratch]` (every text form; `scratch: true` in `--format json`); `chi web` never shows one. See [CLI: Scratch sessions](cli.md#scratch-sessions).
113
+ - A `chi scratch` session (`"scratch": true`) is deleted when its terminal leaves (its worker deletes it too once its last client is gone); one left behind when both died is deleted by the next sweep, `prune` or `clean`, whatever its age or status, once nobody owns it and its file is 30 s old (`SessionRetention::SCRATCH_START_GRACE`, the worker start grace: `chi scratch` saves the session before the worker it spawns takes the owner lock). `list` shows it as `[scratch]` (every text form; `scratch: true` in `--format json`); `chi web` never shows one. See [CLI: Scratch sessions](cli.md#scratch-sessions).
114
114
  - For ad-hoc manual QA use `SAMAGOTCHI_ENV=test XDG_STATE_HOME=/tmp/chi-test-$USER chi ...` to isolate from real state; the flag also marks sessions that `chi web` or an attached `chi` spawn (their workers inherit the environment), so `clean` finds them if they land in the real state.
115
115
 
116
116
  ## Session file
data/docs/testing.md CHANGED
@@ -60,6 +60,21 @@ paths (replace them with placeholders), no PATH tools (the suite pins `rg` as fo
60
60
  that sets PATH to test the lookup is tagged `:real_rg`), no Ruby-version-dependent `inspect` (a Hash's or an
61
61
  exception's text changes between Ruby versions: write the string yourself).
62
62
 
63
+ The whole-turn goldens (`spec/engine_turn_golden_spec.rb`, `spec/fixtures/turn_goldens/`) run a real
64
+ `Engine#run_turn` (its own KernelLoop, ChatLoop, ToolRunner, Gate and GuardrailWiring) over `FakeProviderServer` on
65
+ the native (llama.cpp `/completion`, Qwen tool calls), `api: openai` and `api: anthropic` wires: a turn of two tool
66
+ iterations (a muted `memory_read` and a `read`, then a `write` a config rule asks about, allowed through the question
67
+ flow), a `before_turn` hook switching the model (the request names the new model with the old one's sampling), and a
68
+ review participant's turn whose `write` the review boundary refuses. Each golden pins every request (the chi-set
69
+ headers, the body in its key order, the native prompt as text), the turn's events (keys sorted; the context-status
70
+ estimates and clock values masked), each `before_tool_call` payload, the approval asked and the session's messages.
71
+ System 1, the toolkit suggestion, the context judge, the recap and the warm-up are off by config, so the order is the
72
+ turn's own. The arch P3 refactors (TurnContext, turn stages, ToolSurface) keep them byte-identical: a phase that
73
+ changes one stops and asks. An intended change elsewhere: `UPDATE_PROMPTS=1 bundle exec rspec
74
+ spec/engine_turn_golden_spec.rb`, then read `git diff spec/fixtures/turn_goldens/`; a failure writes what the run got
75
+ to `tmp/turn_goldens/<golden>.actual` and names the `diff` to run. Changes to the turn path (engine, kernel loop, chat loop,
76
+ tool runner, gate, guardrail wiring, system prompt, tool declarations, registry) pick it in `script/affected-specs`.
77
+
63
78
  The web specs run in plain node. One that loads lit-html, itself or through a module it imports (`turn_block.js`,
64
79
  `turn_html.js`, `card.js`, `copy.js`, `images.js`, `scope.js`, `command_view.js`, `diff_view.js`, `read_view.js`,
65
80
  `question_cards.js`, `stream_handlers.js`, `stage_view.js`, `bubbles.js`, `cards_place.js`, the chips, `info_bar.js`, `info_bar_model.js`, `lists_view.js`, `context_chips.js`, `families_view.js`,
@@ -13,7 +13,6 @@ module Samagotchi
13
13
  # thoughts and call markup inline: both hosts' thoughts come out, a thought
14
14
  # cut off before it closed too, then the calls and the wire tokens.
15
15
  module AnswerText
16
- QWEN_THINK = %r{<think>.*?</think>}m
17
16
  # The template opened the block, so the text starts inside it.
18
17
  TEMPLATE_OPENED_THINK = %r{\A.*?</think>}m
19
18
 
@@ -25,7 +24,7 @@ module Samagotchi
25
24
  text = text.to_s.scrub
26
25
  template_opened = !text.include?("<think>")
27
26
  gemma = ToolCallParser::Gemma.new(ModelProfile.gemma4)
28
- text = gemma.strip_thought(text).gsub(QWEN_THINK, "")
27
+ text = gemma.strip_thought(text).gsub(OutputFormatter::QWEN_THINK_RE, "")
29
28
  text = text.sub(TEMPLATE_OPENED_THINK, "") if template_opened
30
29
  text = ToolCallParser.strip_calls(gemma.strip_tool_calls(text), "<tool_call>", "</tool_call>")
31
30
  OutputFormatter.strip_markup(text) # drops a thought cut off before it closed
@@ -9,16 +9,22 @@ module Samagotchi
9
9
  # the recap share (Steer.turn_opening?); the store's turn log only orders
10
10
  # the ids. So a turn with no opening (a reminder, a Continue, a failed or
11
11
  # rolled-back one, a prompt stopped before the model) counts nothing, and
12
- # an opening with no id (/continue no's reason, Qwen's recovery line) counts
13
- # as it does on the page. Nothing orders such an opening against an entry, so
14
- # an entry says how many it came after (+past_idless+, #idless_after). Pure:
15
- # no Engine, no file.
12
+ # an opening with no turn id (/continue no's reason) counts as it does on
13
+ # the page. Nothing orders such an opening against an entry, so
14
+ # an entry says how many it came after (+past_idless+, #idless_after), and
15
+ # which one that is when it has an id (+after_opening+, #after_opening).
16
+ #
17
+ # The same place as a stamp (#opening_of): the id of the opening the entry
18
+ # comes after (a prompt's turn_id, a line's opening_id), which the page
19
+ # finds by its bubble, not by counting. Pure: no Engine, no file.
16
20
  class CardPlaces
17
21
  # @param turn_log [Array<String>] the turn ids, oldest first
18
22
  # @param messages [Array<Hash>] the messages the page draws
19
23
  def initialize(turn_log, messages)
20
24
  positions = turn_log.each_with_index.to_h
21
- @ids = Array(messages).select { |message| Steer.turn_opening?(message) }.map { |message| message[:turn_id] || message["turn_id"] }
25
+ openings = Array(messages).select { |message| Steer.turn_opening?(message) }
26
+ @ids = openings.map { |message| message[:turn_id] || message["turn_id"] }
27
+ @stamps = openings.map { |message| stamp_of(message) }
22
28
  @openings = @ids.size
23
29
  @opened = @ids.compact.to_set
24
30
  @places = @ids.map { |id| positions[id] }
@@ -45,13 +51,44 @@ module Samagotchi
45
51
  entry[:base_since].to_i + since
46
52
  end
47
53
 
54
+ # The opening +entry+ comes after, as #turns_since counts: its id (a
55
+ # prompt's turn_id, a line's opening_id), the page's stamp. nil when it
56
+ # comes before every opening, when that opening has no id (a line an
57
+ # older chi wrote), or for an entry from an older file (+base_since+):
58
+ # the page counts those (turns_since).
59
+ # @param entry [Hash] a store's entry: :turn_id, :past_idless, :after_opening, :base_since
60
+ # @return [String, nil]
61
+ def opening_of(entry)
62
+ return nil if entry[:base_since]
63
+
64
+ at_or_before = counted_from(entry[:turn_id])
65
+ return nil unless at_or_before
66
+
67
+ run = at_or_before..(at_or_before + idless_run(at_or_before))
68
+ # The line it saved it came after, while that line is in the run (as
69
+ # the count past it, the two save together); else the count's.
70
+ after = entry[:after_opening] && @stamps.index(entry[:after_opening])
71
+ @stamps[after && run.cover?(after) ? after : [at_or_before + entry[:past_idless].to_i, run.end].min]
72
+ end
73
+
74
+ # The id of the last opening with no turn id right after the one
75
+ # +turn_id+ counts from (#idless_after's run), nil when there is none or
76
+ # it has no id either.
77
+ # @return [String, nil]
78
+ def after_opening(turn_id)
79
+ at_or_before = counted_from(turn_id)
80
+ return nil unless at_or_before
81
+
82
+ run = idless_run(at_or_before)
83
+ run.positive? ? @stamps[at_or_before + run] : nil
84
+ end
85
+
48
86
  # The openings with no id right after the one +turn_id+ counts from: what
49
87
  # an entry shown now has come after. A rollback can only shorten the run, so
50
88
  # a count read back against it never eats a later turn.
51
89
  # @return [Integer]
52
90
  def idless_after(turn_id)
53
- position = @positions.fetch(turn_id, -1)
54
- at_or_before = position >= 0 ? @last_at_or_before[position] : nil
91
+ at_or_before = counted_from(turn_id)
55
92
  at_or_before ? idless_run(at_or_before) : 0
56
93
  end
57
94
 
@@ -70,6 +107,18 @@ module Samagotchi
70
107
 
71
108
  private
72
109
 
110
+ # The last opening whose turn is at or before +turn_id+'s in the log
111
+ # (an index into the openings), nil for none.
112
+ def counted_from(turn_id)
113
+ position = @positions.fetch(turn_id, -1)
114
+ position >= 0 ? @last_at_or_before[position] : nil
115
+ end
116
+
117
+ def stamp_of(message)
118
+ id = message[:turn_id] || message["turn_id"] || message[:opening_id] || message["opening_id"]
119
+ id.is_a?(String) && !id.empty? ? id : nil
120
+ end
121
+
73
122
  def idless_run(opening)
74
123
  @ids.drop(opening + 1).take_while(&:nil?).size
75
124
  end
@@ -26,7 +26,9 @@ module Samagotchi
26
26
  # holds the id of the turn it belongs to or follows (+turn_id+: the running
27
27
  # turn's, else the last one's, nil before any) and lists with +turns_since+,
28
28
  # the turns drawn after it, counted from the messages the page draws
29
- # (CardPlaces, with the turn log the store keeps), and +current+ for a card
29
+ # (CardPlaces, with the turn log the store keeps), +opening_id+, the same
30
+ # place as a stamp (the id of the opening it comes after, which the page
31
+ # finds by its bubble; none where only the count can say), and +current+ for a card
30
32
  # of the turn running now. A card that isn't the turn's but comes while one
31
33
  # runs (an anytime command's, such as /btw) goes after that turn: its prompt
32
34
  # is already in the history. Listed against messages, an in-turn entry of a
@@ -70,7 +72,7 @@ module Samagotchi
70
72
  SAVED_ON = (%i[turn_started card hook_notice empty_answer_retry steer_cut llm_context_edited question_requested question_answered
71
73
  question_cancelled question_relay] + Events::TURN_END).freeze
72
74
  # What only the store reads: never listed.
73
- INTERNAL_FIELDS = %i[turn_id continued base_since past_idless during after_failed command].freeze
75
+ INTERNAL_FIELDS = %i[turn_id continued base_since past_idless after_opening during after_failed command].freeze
74
76
 
75
77
  # What a session's saved file lists, as a store started from it lists
76
78
  # (no turn running): the web's cards for a session no worker runs.
@@ -118,7 +120,8 @@ module Samagotchi
118
120
  # them (a body re-read, a trace): no turns_since:, nothing to tell a turn
119
121
  # that left nothing by.
120
122
  # @return [Array<Hash>] oldest first: the card's (or notice's) fields
121
- # plus current:, turns_since: with +messages+, earlier: true for one an
123
+ # plus current:, turns_since: and opening_id: (CardPlaces#opening_of,
124
+ # when it has one) with +messages+, earlier: true for one an
122
125
  # earlier worker saved, during: true for a card that came while the
123
126
  # running turn runs but isn't that turn's (it goes after the running
124
127
  # turn's prompt), and after_failed: true for one shown after the last
@@ -141,6 +144,8 @@ module Samagotchi
141
144
 
142
145
  out = shown.except(*INTERNAL_FIELDS)
143
146
  out[:turns_since] = places.turns_since(entry) if places
147
+ stamp = places&.opening_of(entry)
148
+ out[:opening_id] = stamp if stamp
144
149
  out[:current] = current
145
150
  out[:during] = true if entry[:during]
146
151
  out[:after_failed] = true if entry[:after_failed] && entry[:turn_id] == @failed_turn
@@ -255,7 +260,7 @@ module Samagotchi
255
260
  # A hand-edited or foreign file's entry goes, rather than break every later
256
261
  # listing of the session.
257
262
  def sound_place?(entry)
258
- return false unless entry[:turn_id].nil? || entry[:turn_id].is_a?(String)
263
+ return false unless [entry[:turn_id], entry[:after_opening]].all? { |id| id.nil? || id.is_a?(String) }
259
264
 
260
265
  entry.values_at(:base_since, :past_idless).compact.all? { |count| count.is_a?(Integer) && !count.negative? }
261
266
  end
@@ -315,8 +320,8 @@ module Samagotchi
315
320
  shown = @entries.select { |entry| entry[:turn_id] == @running_turn && !entry[:in_turn] }
316
321
  return if shown.empty? || !@messages
317
322
 
318
- run = CardPlaces.new(@turn_log, @messages.call).idless_after(@running_turn)
319
- shown.each { |entry| entry[:past_idless] = run if run.positive? }
323
+ places = CardPlaces.new(@turn_log, @messages.call)
324
+ shown.each { |entry| note_idless(entry, places) }
320
325
  end
321
326
 
322
327
  # The entry's anchor: the running turn, else the last one.
@@ -331,7 +336,7 @@ module Samagotchi
331
336
  # button's command replaced, which nobody typed).
332
337
  old = @entries[index]
333
338
  @entries[index] = card.except(:command).merge(updated: true, **old.slice(:in_turn, :turn_id, :during, :continued, :base_since,
334
- :past_idless, :after_failed, :command))
339
+ :past_idless, :after_opening, :after_failed, :command))
335
340
  else
336
341
  entry = card.merge(in_turn: event[:in_turn] ? true : false, turn_id: anchor)
337
342
  entry[:during] = true if @running_turn && !event[:in_turn]
@@ -393,8 +398,18 @@ module Samagotchi
393
398
  def note_past_idless(entry)
394
399
  return unless @messages
395
400
 
396
- run = CardPlaces.new(@turn_log, @messages.call).idless_after(entry[:turn_id])
397
- entry[:past_idless] = run if run.positive?
401
+ note_idless(entry, CardPlaces.new(@turn_log, @messages.call))
402
+ end
403
+
404
+ # How many openings with no turn id +entry+ came after, and the last
405
+ # one's own id when it has one (+after_opening+: its stamp, #list).
406
+ def note_idless(entry, places)
407
+ run = places.idless_after(entry[:turn_id])
408
+ return unless run.positive?
409
+
410
+ entry[:past_idless] = run
411
+ after = places.after_opening(entry[:turn_id])
412
+ entry[:after_opening] = after if after
398
413
  end
399
414
 
400
415
  # Which cap +entry+ counts against: :notice, :llm_context or :card.
@@ -196,6 +196,15 @@ module Samagotchi
196
196
  @mutex.synchronize { @open_streams - (client_id ? @streams_by_client[client_id] : 0) }
197
197
  end
198
198
 
199
+ # Streams held by the clients whose id starts with +prefix+
200
+ # (ClientId::TUI_PREFIX: the attached terminals, which a scratch worker
201
+ # counts, Worker#scratch_unattended?). A stream that named no client
202
+ # (a web tab's, through the chi web proxy) is none of them.
203
+ # @return [Integer]
204
+ def open_streams_of(prefix)
205
+ @mutex.synchronize { @streams_by_client.sum { |client_id, count| client_id.start_with?(prefix) ? count : 0 } }
206
+ end
207
+
199
208
  # Monotonic time of the last request, stream open or stream close. The
200
209
  # worker's idle exit counts from it. A connect that sends no request (the
201
210
  # sidecar liveness probe) doesn't count.
@@ -309,12 +309,16 @@ module Samagotchi
309
309
  # older worker's 404 for a route it lacks from a refused connect, a
310
310
  # timeout or a 500.
311
311
  # @return [Array(Integer, Object)] the status and the parsed JSON body
312
- # (nil when it isn't JSON); [nil, nil] when no reply came
312
+ # (nil when it isn't JSON); [nil, nil] when no reply came (a refused
313
+ # connect, a timeout, a cut or garbled reply), and when anything else
314
+ # failed, which is logged
313
315
  def get(path)
314
- response = get_response(path)
315
- [response.status, response.json]
316
- rescue StandardError
317
- [nil, nil]
316
+ Degrade.call(:bridge, "get_failed", fallback: [nil, nil], path: path) do
317
+ response = get_response(path)
318
+ [response.status, response.json]
319
+ rescue SystemCallError, IOError
320
+ [nil, nil]
321
+ end
318
322
  end
319
323
 
320
324
  # GET /session/:id/<path>, preserving the raw response and raising on I/O.
@@ -124,7 +124,7 @@ module Samagotchi
124
124
  end
125
125
 
126
126
  # The listing's placement fields: nothing to place in a one-turn run.
127
- LISTING_FIELDS = %w[turns_since current earlier after_failed label].freeze
127
+ LISTING_FIELDS = %w[turns_since opening_id current earlier after_failed label].freeze
128
128
 
129
129
  # The saved cards as the web reads them (CardStore.saved), with
130
130
  # string keys and values, as the JSON trace prints them.
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  name: btw
3
- version: 0.1.4
3
+ version: 0.1.5
4
4
  scope: system
5
5
  description: "/btw <question>: a side answer about the conversation, as a card in every UI, even mid-turn; Keep as session forks it into a child session"
6
6
  trust_level: reviewed
7
7
  plugin:
8
8
  file: plugin.rb
9
- sha256: sha256:6497129b06d1c3061c1d7d9d7a9c3dfdbcf790e8f2049addad0c26d24771015f
9
+ sha256: sha256:d4c0d6309308270a1430d64f9713e46c15d71ace1b068fefc22916aa8cc2d043
10
10
  requires_chi: ">= 0.143.0"
11
11
  # config.yml bundles: btw: (another key warns; an older chi ignores this)
12
12
  settings: [max_tokens, timeout]
@@ -15,7 +15,7 @@ class Plugin
15
15
  SYSTEM = "Answer the user's question about the conversation below, briefly. The user asks it on the side: " \
16
16
  "don't continue the conversation's task, and don't say what you would do next."
17
17
  # How many answers /btw keep can still fork: the last ones, for the
18
- # worker's (or the REPL's) life.
18
+ # worker's life.
19
19
  KEPT = 10
20
20
  TITLE_CHARS = 72
21
21
  EARLIER = "(About the conversation before the running turn.)"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: coordinator
3
- version: 0.8.0
3
+ version: 0.8.1
4
4
  scope: system
5
5
  description: "Coordinate parallel work: the coordinator skill (split the work, one worktree and child session per task, check each report, merges under a guardrail approval (the git-merge rule) or a question, a handoff memory to resume from), /coordinate <goal>, /coordinate resume, /coordinate end, /coordinate review (experimental: a sprint review session to share) and /children; the coordinator toolkit (lean + delegate, delegate_result, list_sessions, objectives) and the coding toolkit for build children (child + be concise)"
6
6
  trust_level: reviewed
@@ -10,7 +10,7 @@ files:
10
10
  description: "Coordinate parallel work: split it, delegate each task wait:false in its own worktree (cwd:), check each report, merge under the git-merge approval or ask the user; resume from a handoff_* memory"
11
11
  plugin:
12
12
  file: plugin.rb
13
- sha256: sha256:84535a86b754f3a12cdb2f2591027ea71bbf9ffb74734b1050899b9a4fde07ac
13
+ sha256: sha256:540dd6369342ea4dcbc6ba5ee17fb20d980b880df614a475668c70115e7ec6b8
14
14
  # the coordinator toolkit needs chi.toolkit and ctx.use_toolkit (0.66.0); the
15
15
  # coding toolkit (chi.toolkit prompt:) and the skill's delegate toolkit: need
16
16
  # the next chi.
@@ -16,8 +16,9 @@
16
16
  # (ctx.sessions.stop, by session id) and shows the card again.
17
17
  #
18
18
  # /coordinate <goal> asks the model, in this session, to follow the skill
19
- # for the goal. A REPL session (--no-shared) takes no messages: the request
20
- # is shown for the user to send.
19
+ # for the goal. A session a one-turn run holds (-p --non-interactive, or an
20
+ # older chi's terminal) takes no messages: the request is shown for the user
21
+ # to send.
21
22
  #
22
23
  # /coordinate resume picks up a coordinator handoff: the open handoff_*
23
24
  # memories of this project (an index line whose description doesn't start
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: github-pr
3
- version: 0.6.3
3
+ version: 0.6.4
4
4
  scope: system
5
5
  description: Proposes the branch's open GitHub pull request to a session, attached once you mention it (#N, the branch or its URL), and attaches 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 attach the PR at once instead, or do nothing
6
6
  trust_level: reviewed
@@ -11,7 +11,7 @@ needs:
11
11
  hint: brew install gh, then gh auth login
12
12
  plugin:
13
13
  file: plugin.rb
14
- sha256: sha256:82f8c76d38f314c3503eef3a5de0f8c0b20d0bb5bf4a3daebca43b60d00f8c7a
14
+ sha256: sha256:87678c39565a3d586b462d0449a27ee3da4a96f53ef5b352dea959256f6c1eda
15
15
  scripts:
16
16
  pr_context.rb: sha256:a883d2780e89c68c0d7b6d5e0fcb22c273f17bb167e79064e0e99db6bd2ee6f0
17
17
  context_providers:
@@ -29,11 +29,11 @@ require "samagotchi/git_env"
29
29
  # diff" (or "File at head") action the web's ref menu offers. The
30
30
  # after_turn hook links from a per-PR cache and never runs gh (the web holds the answer until the hooks are
31
31
  # done); before_turn, before_generation and the init task fill the cache
32
- # off the turn's thread (`gh api`), in a session worker only: the REPL and
33
- # -p --non-interactive show no web page while they run (ctx.frontend; a chi
32
+ # off the turn's thread (`gh api`), in a session worker only: -p
33
+ # --non-interactive shows no web page while it runs (ctx.frontend; a chi
34
34
  # without it counts as a worker), and a scratch session never fetches.
35
35
  # `bundles: github-pr: line_links:` false turns it off, "always" fetches in
36
- # the REPL and -p too (still not for a scratch session).
36
+ # -p --non-interactive too (still not for a scratch session).
37
37
  #
38
38
  # `bundles: github-pr: auto_attach:` says what the init task does with the
39
39
  # branch's open PR: `propose` (the default, above), `attach` (attached at
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  name: skills
3
- version: 0.1.9
3
+ version: 0.1.10
4
4
  scope: system
5
5
  description: "Skills (skill_<name> memories, the steps of a task done together): /skill save, list, show and diff; older versions kept with a short diff line on every update; a nudge when a followed skill's step fails"
6
6
  trust_level: reviewed
7
7
  plugin:
8
8
  file: plugin.rb
9
- sha256: sha256:781d943749d59ade0e24d04ba774449d31267eb8ca435835c36c67a3e523e632
9
+ sha256: sha256:8d72d29286d0807c18d2c37225455ca06d45f4f974f9335b76cc77916043838c
10
10
  requires_chi: ">= 0.143.0"
11
11
  # config.yml bundles: skills: (another key warns; an older chi ignores this)
12
12
  settings: [history_keep, nudge]
@@ -242,8 +242,9 @@ class Plugin
242
242
  # what was just done (the model has seen the commands; a side answer
243
243
  # wouldn't). The request runs as a turn; sent while a turn runs it joins
244
244
  # that turn at its next step, and the request says to finish the task
245
- # first. A REPL session (--no-shared) takes no messages: the request is
246
- # shown for the user to send.
245
+ # first. A session a one-turn run holds (-p --non-interactive, or an older
246
+ # chi's terminal) takes no messages: the request is shown for the user to
247
+ # send.
247
248
  def save(rest, ctx)
248
249
  words = rest.split
249
250
  scope = words.delete("--system") ? "system" : "project"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: samagotchi-system
3
- version: 0.154.0
3
+ version: 0.154.1
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:
@@ -199,7 +199,7 @@ final class ChiRunner {
199
199
  private static let recentFlags = ["--limit", "20"]
200
200
 
201
201
  /// The live rows, then up to +recentCount+ of the recent list that are
202
- /// neither live nor a REPL's; a failed recent list adds none.
202
+ /// neither live nor held by an owner (a one-turn run); a failed recent list adds none.
203
203
  private static func rows(live: [LiveSession], recent: Result<[LiveSession], ChiError>, recentCount: Int) -> [LiveSession] {
204
204
  let liveIds = Set(live.map(\.id))
205
205
  let stopped = ((try? recent.get()) ?? [])