samagotchi 0.154.1 → 0.156.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -1
- data/bin/chi +1 -1
- data/docs/architecture.md +12 -7
- data/docs/bundles.md +21 -0
- data/docs/cli.md +1 -1
- data/docs/guardrails.md +2 -2
- data/docs/hooks.md +1 -1
- data/docs/internals/anthropic.md +5 -5
- data/docs/internals/attached-context.md +9 -6
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/block-renderers.md +8 -1
- data/docs/internals/engine.md +642 -831
- data/docs/internals/inline-widgets.md +136 -0
- data/docs/internals/llm-context-forget.md +8 -3
- data/docs/internals/objectives.md +8 -6
- data/docs/internals/participants.md +5 -5
- data/docs/internals/prompt-caching.md +13 -6
- data/docs/internals/remote-sessions.md +3 -1
- data/docs/internals/web-frontend.md +1 -1
- data/docs/internals/working-set.md +2 -1
- data/docs/plugins.md +2 -0
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +4 -4
- data/docs/system-one.md +3 -2
- data/docs/testing.md +1 -1
- data/lib/samagotchi/block_renderers.rb +5 -0
- data/lib/samagotchi/bootstrap/config_writer.rb +1 -1
- data/lib/samagotchi/broadcast_command.rb +3 -3
- data/lib/samagotchi/bundle_command/check.rb +1 -1
- data/lib/samagotchi/bundle_command/try/sandbox.rb +1 -1
- data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
- data/lib/samagotchi/bundles/inline-widgets/manifest.yml +11 -0
- data/lib/samagotchi/bundles/inline-widgets/plugin.rb +76 -0
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/child_move.rb +7 -51
- data/lib/samagotchi/children_status.rb +1 -1
- data/lib/samagotchi/cli/parent_wait.rb +1 -1
- data/lib/samagotchi/config.rb +17 -112
- data/lib/samagotchi/context_command.rb +1 -2
- data/lib/samagotchi/context_window.rb +3 -3
- data/lib/samagotchi/desktop/macos.rb +1 -1
- data/lib/samagotchi/engine/context_judge.rb +21 -119
- data/lib/samagotchi/engine/guardrails.rb +4 -2
- data/lib/samagotchi/engine/messages.rb +9 -6
- data/lib/samagotchi/engine/models.rb +5 -2
- data/lib/samagotchi/engine/objectives.rb +30 -80
- data/lib/samagotchi/engine/participants.rb +15 -10
- data/lib/samagotchi/engine/plugins.rb +3 -1
- data/lib/samagotchi/engine/questions.rb +1 -1
- data/lib/samagotchi/engine/side_requests.rb +26 -13
- data/lib/samagotchi/engine/toolkit.rb +35 -191
- data/lib/samagotchi/engine/turn_end.rb +1 -1
- data/lib/samagotchi/engine/turn_helpers.rb +16 -39
- data/lib/samagotchi/engine/verify.rb +7 -143
- data/lib/samagotchi/engine.rb +166 -98
- data/lib/samagotchi/engine_peers.rb +1 -1
- data/lib/samagotchi/event_fanout.rb +18 -12
- data/lib/samagotchi/guardrail_wiring.rb +83 -82
- data/lib/samagotchi/guardrails/gate.rb +41 -40
- data/lib/samagotchi/guardrails/pair_boundary.rb +2 -1
- data/lib/samagotchi/guardrails/review_boundary.rb +2 -1
- data/lib/samagotchi/guardrails/targets.rb +28 -11
- data/lib/samagotchi/guardrails_command.rb +1 -1
- data/lib/samagotchi/host_config.rb +72 -1
- data/lib/samagotchi/host_model.rb +2 -6
- data/lib/samagotchi/host_registry.rb +1 -1
- data/lib/samagotchi/inline_widgets.rb +325 -0
- data/lib/samagotchi/kernel_loop.rb +215 -313
- data/lib/samagotchi/llm/anthropic_wire.rb +2 -2
- data/lib/samagotchi/llm/backend.rb +3 -1
- data/lib/samagotchi/llm/chat_loop.rb +87 -49
- data/lib/samagotchi/llm/native_backend.rb +3 -2
- data/lib/samagotchi/llm_context_apply.rb +54 -1
- data/lib/samagotchi/llm_context_forget.rb +5 -3
- data/lib/samagotchi/llm_context_notice.rb +1 -1
- data/lib/samagotchi/llm_context_strategy.rb +7 -9
- data/lib/samagotchi/llm_context_view.rb +9 -0
- data/lib/samagotchi/model_binding.rb +3 -3
- data/lib/samagotchi/model_price.rb +4 -10
- data/lib/samagotchi/model_profile.rb +6 -6
- data/lib/samagotchi/model_settings.rb +37 -2
- data/lib/samagotchi/note_command.rb +1 -2
- data/lib/samagotchi/objectives_audit.rb +4 -3
- data/lib/samagotchi/participants.rb +2 -2
- data/lib/samagotchi/plugin/context.rb +1 -1
- data/lib/samagotchi/plugin/prompt_sections.rb +7 -0
- data/lib/samagotchi/plugin/sessions.rb +2 -5
- data/lib/samagotchi/plugin/tool_spec.rb +5 -3
- data/lib/samagotchi/project_env.rb +1 -1
- data/lib/samagotchi/question_desk.rb +1 -1
- data/lib/samagotchi/reasoning_history.rb +3 -3
- data/lib/samagotchi/reminder_carry.rb +1 -1
- data/lib/samagotchi/reprefill_meter.rb +45 -0
- data/lib/samagotchi/review_command.rb +1 -1
- data/lib/samagotchi/revise_anchor.rb +1 -1
- data/lib/samagotchi/sampling_settings.rb +3 -3
- data/lib/samagotchi/self_report.rb +7 -7
- data/lib/samagotchi/session.rb +50 -7
- data/lib/samagotchi/session_access/local.rb +7 -5
- data/lib/samagotchi/session_access/remote.rb +17 -3
- data/lib/samagotchi/session_archive_command.rb +2 -2
- data/lib/samagotchi/session_chain.rb +7 -6
- data/lib/samagotchi/session_continue.rb +6 -5
- data/lib/samagotchi/session_delete_command.rb +1 -1
- data/lib/samagotchi/session_manager/delivery.rb +254 -0
- data/lib/samagotchi/session_manager/emptiness.rb +138 -0
- data/lib/samagotchi/session_manager/lifecycle.rb +274 -0
- data/lib/samagotchi/session_manager/listing.rb +189 -0
- data/lib/samagotchi/session_manager/removal.rb +190 -0
- data/lib/samagotchi/session_manager/spawn.rb +242 -0
- data/lib/samagotchi/session_manager/worker_command.rb +157 -0
- data/lib/samagotchi/session_manager.rb +26 -1540
- data/lib/samagotchi/session_metrics.rb +1 -1
- data/lib/samagotchi/sessions_command.rb +1 -1
- data/lib/samagotchi/share_command.rb +1 -1
- data/lib/samagotchi/starting_marker.rb +65 -0
- data/lib/samagotchi/steer_cut_after.rb +3 -3
- data/lib/samagotchi/system_one/recorder.rb +49 -0
- data/lib/samagotchi/system_prompt.rb +37 -30
- data/lib/samagotchi/terminal_ui/exit_lines.rb +1 -1
- data/lib/samagotchi/terminal_ui.rb +2 -6
- data/lib/samagotchi/testing/boundary.rb +7 -0
- data/lib/samagotchi/testing/bundle.rb +7 -5
- data/lib/samagotchi/testing/install.rb +3 -2
- data/lib/samagotchi/testing/sessions.rb +1 -1
- data/lib/samagotchi/thinking.rb +3 -3
- data/lib/samagotchi/token_usage.rb +1 -1
- data/lib/samagotchi/tool_activity.rb +15 -7
- data/lib/samagotchi/tool_declarations.rb +6 -6
- data/lib/samagotchi/tool_output_cut.rb +17 -0
- data/lib/samagotchi/tool_response.rb +4 -3
- data/lib/samagotchi/tool_runner.rb +96 -102
- data/lib/samagotchi/toolkit_binding.rb +40 -14
- data/lib/samagotchi/toolkit_command.rb +1 -1
- data/lib/samagotchi/tools/deferred_tools.rb +17 -21
- data/lib/samagotchi/tools/delegate.rb +10 -5
- data/lib/samagotchi/tools/delegate_result.rb +4 -4
- data/lib/samagotchi/tools/index_hint.rb +14 -0
- data/lib/samagotchi/tools/registry.rb +64 -44
- data/lib/samagotchi/tools/send_note.rb +4 -6
- data/lib/samagotchi/tools/tool_index.rb +150 -102
- data/lib/samagotchi/tools/tool_surface.rb +187 -0
- data/lib/samagotchi/turn_context.rb +68 -0
- data/lib/samagotchi/turn_stages/context_judge.rb +172 -0
- data/lib/samagotchi/turn_stages/objectives.rb +150 -0
- data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +257 -0
- data/lib/samagotchi/turn_stages/verify_anchors.rb +168 -0
- data/lib/samagotchi/turn_stages/warmup.rb +82 -0
- data/lib/samagotchi/turn_state.rb +46 -4
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +3 -3
- data/lib/samagotchi/web/app.rb +15 -8
- data/lib/samagotchi/web/markdown_renderer.rb +39 -14
- data/lib/samagotchi/web/message_display.rb +1 -1
- data/lib/samagotchi/web/participant_access.rb +2 -2
- data/lib/samagotchi/web/public/app.js +5 -1
- data/lib/samagotchi/web/public/block_host.js +74 -199
- data/lib/samagotchi/web/public/block_renderers.css +2 -0
- data/lib/samagotchi/web/public/block_source.js +7 -4
- data/lib/samagotchi/web/public/block_watch.js +256 -0
- data/lib/samagotchi/web/public/composer_fold.js +3 -0
- data/lib/samagotchi/web/public/index.html +28 -9
- data/lib/samagotchi/web/public/inline_widgets.js +146 -0
- data/lib/samagotchi/web/public/owner_verify.js +1 -1
- data/lib/samagotchi/web/public/participant.js +1 -1
- data/lib/samagotchi/web/public/revise_stack.js +1 -1
- data/lib/samagotchi/web/public/stage_watch.js +6 -2
- data/lib/samagotchi/web/public/text_roots.js +1 -1
- data/lib/samagotchi/web/public/verify_target.js +1 -1
- data/lib/samagotchi/web/public/widget_picks.js +130 -0
- data/lib/samagotchi/web/public/widget_walk.js +230 -0
- data/lib/samagotchi/web/public/widgets.css +70 -0
- data/lib/samagotchi/web/session_access_routes.rb +3 -3
- data/lib/samagotchi/web/session_control.rb +3 -3
- data/lib/samagotchi/web/session_create.rb +3 -3
- data/lib/samagotchi/web/session_hub.rb +1 -1
- data/lib/samagotchi/web/session_show.rb +3 -3
- data/lib/samagotchi/web/session_summary.rb +3 -4
- data/lib/samagotchi/worker.rb +55 -27
- data/lib/samagotchi/worker_boot.rb +184 -0
- data/lib/samagotchi/worker_signals.rb +1 -1
- data/lib/samagotchi/worker_task_wakes.rb +2 -2
- metadata +29 -1
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Inline widgets: chi-ui blocks the page draws
|
|
2
|
+
|
|
3
|
+
A model can write a ```` ```chi-ui ```` fence in an answer: YAML, `type:` first, a **picks** (rows of options to choose
|
|
4
|
+
from, which fill the reply) or a **walk** (a scenario stepped through). Core draws it, not a bundle: the server parses
|
|
5
|
+
the YAML once and the page draws from the parsed data. Where it doesn't draw (the terminal, `web.markdown` off, a
|
|
6
|
+
refused block) the YAML reads as text. The server half (the parse) and the page half (the drawers) are below.
|
|
7
|
+
|
|
8
|
+
What tells the model the blocks exist is the shipped inline-widgets bundle
|
|
9
|
+
([bundles.md](../bundles.md#the-inline-widgets-bundle)): two prompt sections, `picks` and `walk`, each with an example
|
|
10
|
+
its spec holds to a strict parse. Without it nothing here changes, and blocks already in a history still draw.
|
|
11
|
+
|
|
12
|
+
## The path
|
|
13
|
+
|
|
14
|
+
1. `Web::MarkdownRenderer#fences` walks the `code_block` nodes, as for block renderers' `data-lang`
|
|
15
|
+
([block-renderers.md](block-renderers.md), The path), keeping each one's language and its literal
|
|
16
|
+
(`string_content`, the source between the fences, also inside a list item or a blockquote) as a `Fence`.
|
|
17
|
+
2. After sanitize's walk, `mark_languages` pairs them with the `<pre>`s by position and, for a `chi-ui` one, calls
|
|
18
|
+
`InlineWidgets.parse(literal)`. The block gets `data-widget` (the JSON of the data with its `type`) or
|
|
19
|
+
`data-widget-error` (the one-line reason), and `data-widget-lenient` when the lenient pass was needed. A count
|
|
20
|
+
that differs sets nothing, as for `data-lang`.
|
|
21
|
+
3. Every HTML path goes through `render`, so answers, cards, delegate reports and `/api/render` all carry the
|
|
22
|
+
attributes; which of them draw is the page's choice (answer texts only).
|
|
23
|
+
4. `chi-ui` is `BlockRenderers::RESERVED`: a bundle's `block_renderers:` can't claim it, so the page never has two
|
|
24
|
+
drawers for one block.
|
|
25
|
+
|
|
26
|
+
**Safety.** The JSON is an attribute value: Nokogiri escapes its quotes, and `<`, `>` and `&` in it are `\u` escapes
|
|
27
|
+
(`JSON_MARKUP`), so the HTML holds no model markup even as attribute text. The page draws its strings as text.
|
|
28
|
+
|
|
29
|
+
## The parse (`Samagotchi::InlineWidgets`)
|
|
30
|
+
|
|
31
|
+
Pure Ruby, no web, no Engine. `parse(source)` returns a `Result`: `ok?`, `type`, `data` (string keys, JSON-ready),
|
|
32
|
+
`lenient?`, `error`, and `widget_json`.
|
|
33
|
+
|
|
34
|
+
- **Strings stay strings.** The YAML is never loaded: `Tree` reads Psych's node tree and takes each scalar's own text.
|
|
35
|
+
YAML 1.1 would make `- no` false (a real eval block has one), `id: 1.10` the float 1.1, and a date a `Date` that
|
|
36
|
+
`safe_load` refuses. A plain empty value, `~` or `null` is absent.
|
|
37
|
+
- **Plain data only.** An anchor, an alias or a tag (`!ruby/object`, `!!str`) is refused, as is a second document, a
|
|
38
|
+
key that isn't a scalar, a root that isn't a mapping, text that isn't valid UTF-8, and nesting past 8 levels
|
|
39
|
+
(`MAX_DEPTH`; the deepest legal block nests 6). A stack overflow is rescued too, as the backstop: it is no
|
|
40
|
+
`StandardError`, so it would escape `render` and break every render of the history.
|
|
41
|
+
- **Strict first, then the lenient pass.** Only a block Psych can't parse gets `Lenient.quote` and a second parse.
|
|
42
|
+
The pass is eval 0013's (`~/.claude/plans/evals/0013-inline-widgets/scripts/validate.rb`): the free-text values
|
|
43
|
+
(`title text why does sees behind when recommended`) and the list items that aren't mappings go in double quotes
|
|
44
|
+
unless they are one clean scalar already. It mends the traps models fell into: a colon in a value
|
|
45
|
+
(`does: Asked: ...`), a colon in an option (`- B: keep 1 h`), a value starting with a quoted word
|
|
46
|
+
(`text: "x" is called`). It is line by line, so a YAML error's line still points at the source. It can't mend a
|
|
47
|
+
comma in a flow list (`options: [A allow, B deny project, allow system]` is three options, so the recommended one
|
|
48
|
+
isn't one of them: eval round 1's failures), and a block that parses strictly into the wrong shape isn't retried.
|
|
49
|
+
- **Folds.** A block that parses strictly can still have an option or a free-text value that YAML read as a mapping
|
|
50
|
+
of one pair of scalars (`- fix now: drop the command`). Where text is wanted (an option, `title`, `text`, `why`,
|
|
51
|
+
`does`, `sees`, `behind`, `when`, `recommended`), that mapping is taken as the text written: the source slice from
|
|
52
|
+
the key to the value's end when both sit on one line, else `key: value`. The Result is then `lenient?`. A mapping
|
|
53
|
+
of several pairs, or one holding a list or a mapping, stays an error, and so does one where an id, an actor, a
|
|
54
|
+
`next` or a `goto` goes. This is a mend, not a second lenient pass (`Tree#folds`, `Schema#text`).
|
|
55
|
+
- **Normalised.** Ids are strings, unknown keys are dropped, optional values are absent rather than null, and text is
|
|
56
|
+
stripped. The page never sees model junk.
|
|
57
|
+
|
|
58
|
+
### The schemas
|
|
59
|
+
|
|
60
|
+
| | Required | Optional | Rules |
|
|
61
|
+
|---|---|---|---|
|
|
62
|
+
| picks | `rows`: rows of `id`, `text`, `options` (2-4 texts) | `title`; on a row `recommended`, `why` | ids unique; a row's options unique; `recommended` is one of the options |
|
|
63
|
+
| walk | `title`, `actors` (2-5 texts), `steps`: steps of `id`, `actor`, `does` or `sees` | on a step `behind`, `next`, `cases` (each `when` + `goto`) | ids unique, none of them `end`; actors unique; `actor` is one of the actors; `next` names a step or is `end`, and not the step itself; every `goto` names a step |
|
|
64
|
+
|
|
65
|
+
Eval round 1's `branches:` (the same shape as `cases:`) is read as `cases`.
|
|
66
|
+
|
|
67
|
+
**`next: end`** ends the walk at that step: a branch that ends early, such as a success step written before the
|
|
68
|
+
error step it must not run into. The normalised JSON keeps `"next": "end"`, and the page shows the step as the last
|
|
69
|
+
one (its "end" tag, then "Start over"). A `goto` can't be `end`. In YAML:
|
|
70
|
+
|
|
71
|
+
```yaml
|
|
72
|
+
- id: s4
|
|
73
|
+
actor: user
|
|
74
|
+
sees: the saved form
|
|
75
|
+
next: end
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### The caps
|
|
79
|
+
|
|
80
|
+
The source is at most 16 KB, checked before the parse. After it: a picks has at most 12 rows, a walk at most 30 steps
|
|
81
|
+
and 2-5 unique actors, and every string is at most 300 characters. An error repeats at most 40 characters of a bad
|
|
82
|
+
`type`, and never a tag.
|
|
83
|
+
|
|
84
|
+
## The page (`inline_widgets.js`)
|
|
85
|
+
|
|
86
|
+
`createWidgetDrawer` is a core drawer of `block_watch.js` (its header has the contract), wired in `app.js` ahead of the
|
|
87
|
+
bundles' frame drawer through `createBlockHost`'s `drawers`. So a `chi-ui` block gets the watcher's paths: a reload, a
|
|
88
|
+
session switch, a turn's end, Revise's clones, the stage's hand-off to `#history`.
|
|
89
|
+
|
|
90
|
+
- **What it draws.** A `<pre>` with `data-widget` gets a light-DOM `<figure class="block-drawn widget">` right before it,
|
|
91
|
+
holding a `.widget-host` and the shared `.block-toggle` (block_source.js `setSourceShown`, the same icon; the
|
|
92
|
+
figure's `data-toggle-picture` makes its other label "Show the widget"). The `<pre>` stays, hidden: the DOM is the state, copy.js gives it its `.code-wrap`
|
|
93
|
+
copy button (it copies the YAML, which is the answer's text), and the toggle shows it. The toggle is in the light DOM
|
|
94
|
+
because a click in a shadow root reaches the page's delegated listeners retargeted at the host.
|
|
95
|
+
- **Shadow DOM.** The host has an open shadow root holding the widget, with `widgets.css` adopted
|
|
96
|
+
(`adoptedStyleSheets`): the page fetches it once into a constructable `CSSStyleSheet` (`loadWidgetSheet`), and `draw`
|
|
97
|
+
returns a Promise that places the widget once it is there, so no widget flashes unstyled and the view held
|
|
98
|
+
(`holdingView`) measures the styled one. A failed fetch draws unstyled and the next draw tries again. Its words are then out of the bubble's `textContent`, which the shown-texts dedupe (`seedSeenFromDom`), copy's
|
|
99
|
+
fallback and an annotation's quote read, as with a renderer's `<img>`-only figure. chi's `:root` tokens reach it as
|
|
100
|
+
inherited custom properties, so light and dark follow the page. Playwright's locators pierce an open shadow root;
|
|
101
|
+
`page.evaluate`'s DOM calls don't. Whatever asks `document.activeElement` whether a field has the focus looks
|
|
102
|
+
through shadow roots (`composer_fold.js editable`, `stage_watch.js`), or typing in a note opens a folded composer.
|
|
103
|
+
- **A clone redraws.** `cloneNode` doesn't copy a shadow root: a Revise clone (`revise_switch.js viewOf`) arrives with
|
|
104
|
+
the figure and an empty host, which `needsDraw` sees, and `draw` fills it again with a fresh widget and its own
|
|
105
|
+
listeners. A widget's position and picks live in the widget, so a clone or a rewrite starts over (accepted for v1).
|
|
106
|
+
- **A refused block** (`data-widget-error`) stays source under `failBlock`'s muted `.block-note` ("Couldn't draw this
|
|
107
|
+
chi-ui block: …", an attribute CSS shows, a click copies it). Bad JSON or an unknown `type` fails the same way, from
|
|
108
|
+
the watcher. A `chi-ui` `<pre>` with neither attribute (an older server) is left alone.
|
|
109
|
+
- **walk** (`widget_walk.js`): the swimlane (one column per actor, one row per step; the current, seen and a hovered
|
|
110
|
+
case's target marked; a click jumps), the step card (actor, `does`, "You see", the dashed "Behind it"), a fork's
|
|
111
|
+
"Which case?" buttons, Back / Next / Start over, the path taken, and ← → while the widget has the focus
|
|
112
|
+
(a move that removes or hides the focused button leaves the focus on the widget; a click on the current step is no
|
|
113
|
+
move). `walkRoutes`
|
|
114
|
+
resolves where a step goes: its `next` (`end`: nowhere), else the step below; a fork's `cases` as written, a case whose `when` is no
|
|
115
|
+
condition (round 1's `always`) reading as the usual path, alone it is the step's next. At a phone's width the
|
|
116
|
+
swimlane scrolls sideways inside the widget; the page doesn't.
|
|
117
|
+
- **picks** (`widget_picks.js`): one row per item, its options as chips (`aria-pressed`), the recommended one marked
|
|
118
|
+
and picked to start with, a "+ note" per row, and "Fill reply".
|
|
119
|
+
|
|
120
|
+
**The only way out is composer text.** Fill reply builds `id: option (note) · …` from the picked rows (a row with no
|
|
121
|
+
pick is left out) and hands it to app.js's `fillComposer`, after a typed draft on a new line (`withDraft`; the draft
|
|
122
|
+
is kept). It never sends: the owner reads the text and sends it or not. A walk has no way out at all. The model's
|
|
123
|
+
strings reach the page through `textContent` and attributes only, never `innerHTML`, so a step reading
|
|
124
|
+
`<img src=x onerror=…>` shows as those characters.
|
|
125
|
+
|
|
126
|
+
**What doesn't draw.** The participant page (`participant.js` wires no block host), `web.markdown` off (no
|
|
127
|
+
`data-lang`), a live answer while it streams (its YAML shows until the turn's end re-renders it as markdown), and
|
|
128
|
+
anything outside `TEXTS`: cards, question replies, the asking slot, delegate reports. The terminal shows the YAML.
|
|
129
|
+
|
|
130
|
+
## The fixtures
|
|
131
|
+
|
|
132
|
+
`spec/fixtures/inline_widgets/` holds eval 0013's 78 real blocks (47 from round 2, 31 from round 1), one file each,
|
|
133
|
+
and `manifest.yml`, the outcome (ok, lenient or error) the eval's `check()` gave each when they were copied, but for
|
|
134
|
+
r2-deepseek-picks-review.2: its options are one-pair mappings, an error in the eval, and a fold (lenient) here.
|
|
135
|
+
`spec/inline_widgets_spec.rb` checks every one gets it: all 47 of round 2's draw, and round 1's errors are the
|
|
136
|
+
comma splits.
|
|
@@ -29,7 +29,7 @@ lets the model forget its own tool outputs. The pieces:
|
|
|
29
29
|
those steps, counted with the forget's own: `protect_steps + 1`) unless the forget keeps lines of it; a keep
|
|
30
30
|
outside the output's lines, of a preview, or holding all of it; an output of `SMALL` (80) chars or less; an output
|
|
31
31
|
with an edit already; an unknown id. What passes is saved as a staged `LLMContextEdit` of kind `:forget` (`by:
|
|
32
|
-
"model"`, the note, `keep`), and the apply rule runs at once as at a request (`KernelLoop#apply_llm_context!`), so
|
|
32
|
+
"model"`, the note, `keep`), and the apply rule runs at once as at a request (`LLMContextApply.apply!`, through `KernelLoop#apply_llm_context!`), so
|
|
33
33
|
the result can say whether the batch goes with the next request or waits for the turn's end. `LLMContextApply`'s
|
|
34
34
|
protect_steps hold covers stale's stubs only: a forget was checked when it was made.
|
|
35
35
|
- **Restore** takes the forget off the entry (`LLMContextEdit.remove`, a new edits Hash); the next request sends the
|
|
@@ -38,8 +38,13 @@ lets the model forget its own tool outputs. The pieces:
|
|
|
38
38
|
- **Which edits are sent** is one predicate, `LLMContextView#sends?(edit)`: applied, and its kind one of the view's
|
|
39
39
|
layers or `:drop`. The view stubs by it and `WorkingSet` decides "still sent" by it. The view's layers are
|
|
40
40
|
`LLMContextStrategy::LAYERS` (`stale`, `forget`), the strategy's words, not `LLMContextEdit::KINDS`: whatever a
|
|
41
|
-
caller passes as the strategy, an edit kind no strategy names never becomes a layer that `
|
|
42
|
-
hand to `LLMContextApply.run!`.
|
|
41
|
+
caller passes as the strategy, an edit kind no strategy names never becomes a layer that `LLMContextApply.apply!` would
|
|
42
|
+
hand to `LLMContextApply.run!`. Both loops build their view with `LLMContextView.for(llm_context)` (the turn's
|
|
43
|
+
strategy, none without one) and apply with `LLMContextApply.apply!(conversation, llm_context, moment:, context:,
|
|
44
|
+
warmup:, emit:, model:)`, module functions that hold no loop state: the run, its `llm_context_apply` log line (with
|
|
45
|
+
the run's model) and its ✂ row (`LLMContextNotice`; none for the warm-up's copy). `KernelLoop#llm_context_view` and
|
|
46
|
+
`#apply_llm_context!` are one-line defaults over them (the native run's settings), `ChatLoop`'s the same over its
|
|
47
|
+
turn's. Under none the view has no layers and, unless an entry holds a drop
|
|
43
48
|
(`LLMContextEdit.drop_on?`, one scan), `#messages` returns the Array it was given.
|
|
44
49
|
- **The user's layer, always on**: an edit of kind `:drop` (`by: "user"`, the working set's drop:
|
|
45
50
|
[working-set.md](working-set.md#steering-drop-and-undrop)) is sent under every strategy, none included, as
|
|
@@ -116,9 +116,9 @@ Bridge's `TurnAccumulator#current_messages`) leaves tool calls out. A move shows
|
|
|
116
116
|
|
|
117
117
|
`action: propose` (`change` add, reword, done or drop; `id`; `text`; `note`, why) checks the change against the list
|
|
118
118
|
(`Objectives::Proposal#refusal`: its line through `Objectives::Edit`, the edit the card's Apply runs, so the tool refuses
|
|
119
|
-
what Apply would) and hands it to `Engine#propose_objective
|
|
120
|
-
as
|
|
121
|
-
failed one drops them
|
|
119
|
+
what Apply would) and hands it to `Engine#propose_objective` (`TurnStages::Objectives#propose`), which keeps it until the turn is over,
|
|
120
|
+
as `TurnStages::ContextJudge`'s cards are kept. `run_turn`'s ensure (`Engine#show_objective_proposals`, the cards `TurnStages::Objectives#finally` gives) shows them only for a turn that completed: a canceled or
|
|
121
|
+
failed one drops them, so its Apply card never outlives a turn whose call a
|
|
122
122
|
rollback has taken out of the history, and nothing carries over to the next turn. A card is between turns (no
|
|
123
123
|
`in_turn`), so it writes no `pending_card.json`, the stage doesn't say it waits, and it keeps its buttons over a
|
|
124
124
|
restart (`CardStore` takes them only from a card shown in a turn). The same change twice in a turn is one card. A P3
|
|
@@ -139,7 +139,7 @@ any call. An add's text can't split on ` | `. The body names the line, for a ter
|
|
|
139
139
|
|
|
140
140
|
`Objectives.view` (`List#to_view`) is what the page draws: the items, each with its last move, and `pending`, whether
|
|
141
141
|
the newest list is a file still waiting. The session read (`objectives`, no key with no list) and the live
|
|
142
|
-
`:objectives_updated` carry it. `Engine#announce_objectives` emits when the view differs from the last: after an edit,
|
|
142
|
+
`:objectives_updated` carry it. `Engine#announce_objectives` (under the event lock, `TurnStages::Objectives#announce` giving the event) emits when the view differs from the last: after an edit,
|
|
143
143
|
once a list note is in (`add_context_note`), at a turn's end (its moves) and after a rollback between turns. A session
|
|
144
144
|
with no list emits nothing. Between two tabs that is all it takes. A participant gets neither: the event is in
|
|
145
145
|
`ParticipantView::DROPPED`.
|
|
@@ -259,7 +259,9 @@ turn isn't on the page until the turn's end (`announce_objectives`), so its tap
|
|
|
259
259
|
|
|
260
260
|
## System 1 in audit
|
|
261
261
|
|
|
262
|
-
`ObjectivesAudit`, off unless `objectives.system_one: audit`.
|
|
262
|
+
`ObjectivesAudit`, off unless `objectives.system_one: audit`. `TurnStages::Objectives` asks it: `incoming` at an owner's
|
|
263
|
+
turn's start (a person's prompt only), `outgoing` after the end event of a completed answer, `note` for a note added
|
|
264
|
+
between turns (docs/internals/engine.md, "Turn stages"). See [system-one.md](../system-one.md#objectives-in-audit).
|
|
263
265
|
|
|
264
266
|
## Not in this version
|
|
265
267
|
|
|
@@ -269,4 +271,4 @@ Taking it out: old notes stay readable framed bubbles and keep being sent, and t
|
|
|
269
271
|
stay as history; with a kit no longer declaring the tool, they are ordinary saved calls. The card's bundle uninstalls. The
|
|
270
272
|
page's pieces are the `objectives_*.js` modules and `objectives.css`, wired in a few lines of `app.js`; without them a list note
|
|
271
273
|
falls back to the default bubble once `noteBubbleTemplate`'s first line goes. Check this is `objectives_check.rb`, its line in the command and
|
|
272
|
-
the `check` key; the audit is `objectives_audit.rb` and three calls in `
|
|
274
|
+
the `check` key; the audit is `objectives_audit.rb` and three calls in `TurnStages::Objectives`. Old `objective_checks.jsonl` files and saved runs are then read by nothing.
|
|
@@ -214,10 +214,10 @@ the list's `mode` (the panel's words) and the grant's stamp: `Grant#mode` / the
|
|
|
214
214
|
turns) stays review until the owner invites them again.
|
|
215
215
|
|
|
216
216
|
A turn's mode is `ParticipantMode.for_turn(session, entry)`: `:pair` only for a stamped entry in a pair session.
|
|
217
|
-
The worker decides it once at the turn's start (`GuardrailWiring#begin_turn` →
|
|
217
|
+
The worker decides it once at the turn's start (`GuardrailWiring#begin_turn` → `TurnStart#mode`, into the turn's `TurnContext#participant_mode`, from the entry
|
|
218
218
|
`SessionParticipants#entry_for` reads), and everything in the turn reads that: the stamp on the opening message
|
|
219
219
|
(`participant_mode`), the boundary in `GuardrailWiring#checks`, `ToolRunner#unreached_group_refusal` (through
|
|
220
|
-
`
|
|
220
|
+
the call's `TurnContext`: a review turn's boundary words the refusal; a pair turn gets the owner's), the
|
|
221
221
|
mid-turn stop and the cap notice's words. The system prompt's line is the pair one once an active participant is
|
|
222
222
|
stamped pair (`SessionParticipants#prompt_mode`); the participant page's `viewer.mode` is their own grant's
|
|
223
223
|
(`ParticipantMode.live_for`).
|
|
@@ -323,7 +323,7 @@ and `ctx.turn_author` names it ([hooks](../hooks.md#participants-turns)).
|
|
|
323
323
|
their $2.00 review cap: their comments are refused until you raise it (chi share cap ID p_… AMOUNT)." (a working
|
|
324
324
|
session's: "Alex reached their $2.00 cap: their turns stop and are refused until you raise it (…)"). A review
|
|
325
325
|
turn's spend counts from its end, so a review turn can finish past the cap. A pair turn stops mid-turn: before each
|
|
326
|
-
generation (`KernelLoop#generation_guard`, both loops) `Engine#guard_participant_spend` asks the worker's check
|
|
326
|
+
generation (`KernelLoop::Services#generation_guard`, both loops) `Engine#guard_participant_spend` asks the worker's check
|
|
327
327
|
(`Worker#participant_turn_stop`: `metrics.spend(author)` counts the running turn's finished generations) and, past
|
|
328
328
|
the cap, stops the turn on the plugins' stop path (`HookRoutes#stop_turn`: "stopped the turn: Alex reached their
|
|
329
329
|
$2.00 cap"), with the owner's notice once per cap. A generation's cost is known only when it ends, so a pair turn
|
|
@@ -469,7 +469,7 @@ fields: a field added to an event later reaches nobody until it is named there.
|
|
|
469
469
|
its final assistant reply (`AnswerTail`: the turn's last assistant message with text) contains the quote. Matching is case-sensitive, collapses whitespace, and strips common
|
|
470
470
|
Markdown presentation (links, headings, list/blockquote prefixes, emphasis and code delimiters) from both sides;
|
|
471
471
|
this matches the rendered text the page selects without treating different casing as the same claim. The quote may
|
|
472
|
-
also be in the answer as the pages show it (`
|
|
472
|
+
also be in the answer as the pages show it (`TurnStages::VerifyAnchors#quote_in?`, `Redact.text`: the session's
|
|
473
473
|
folder relative, home `~`, secret shapes `[redacted]`): a participant's page has only that text, and the owner's
|
|
474
474
|
page quotes it the same way (`owner_claim.js` `opts()`, `normalizeClaim`; `spec/fixtures/redact_cases.json` keeps the two
|
|
475
475
|
sides alike), so a claim naming a path is taken on both. A fresh child
|
|
@@ -510,7 +510,7 @@ fields: a field added to an event later reaches nobody until it is named there.
|
|
|
510
510
|
`pair` only for a grant stamped to act in a working session whose worker runs pair turns, else `review`). Every
|
|
511
511
|
turn of a participant's child is theirs, whoever starts it (the owner typing into it, a continue, a reminder):
|
|
512
512
|
`VerifyFreshOrigin#origin_for` sets the author in `Engine#begin_turn` and in the worker, so their boundary
|
|
513
|
-
(`GuardrailWiring#
|
|
513
|
+
(`GuardrailWiring#begin_turn` reads the saved mode, never a lookup), their System 1 skip, their once-only asks to
|
|
514
514
|
the owner and the stamps on their messages follow. The child has no `participants.json`: its participant lookups
|
|
515
515
|
(`Session#participants_source_id`) read the parent's, so a revoke, an expiry or the parent's archive refuses the
|
|
516
516
|
child's next turn and stops a pair turn mid-turn, as in the parent; their audit lines go to the parent's
|
|
@@ -73,11 +73,15 @@ right-hand column happens.
|
|
|
73
73
|
- The tools layer is shared **per toolkit** (configuration.md, "Toolkits"): sessions on the same toolkit share it,
|
|
74
74
|
and a chain's links keep theirs. A session's toolkit is saved when it is created and never resolved again from
|
|
75
75
|
config. A change (`/toolkit`, `ctx.use_toolkit`) is applied only at a turn's start (`Engine#apply_toolkit!`,
|
|
76
|
-
beside `apply_staged_tools!`, then `reset!`): the chat loop builds `tools:` per request
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
beside `apply_staged_tools!`, then `reset!`): the chat loop builds `tools:` per request from the turn's
|
|
77
|
+
`Tools::ToolSurface` (built once in `Engine#generate` from the Registry, under the layers the turn's prompt was
|
|
78
|
+
built with), so its requests declare what the turn's prompt does, and a mid-turn change shapes only the next
|
|
79
|
+
turn's surface. The native declarations, the not-loaded line and the kit prompt come from a surface too: each
|
|
80
|
+
`SystemPrompt` build reads the Registry once as `ToolSurface.for(registry, layers:)` (its `declared`, `left_out`
|
|
81
|
+
and `index_only`, its toolkit's prompt), so the three agree and match the turn's (engine.md, "ToolSurface"). Under `full` nothing is filtered and no line is added: the bytes are what they were before
|
|
82
|
+
toolkits.
|
|
79
83
|
- A toolkit suggestion (`toolkit.suggest.mode: on`) is set before `apply_toolkit!` of the turn it was asked on
|
|
80
|
-
(`
|
|
84
|
+
(`TurnStages::ToolkitSuggestion#settle` → `ToolkitBinding#suggest`), and no request goes out before that, so on a
|
|
81
85
|
session's first turn it costs nothing. Its **deferred switch** (a later prompt after `undecided`, within
|
|
82
86
|
`toolkit.suggest.defer_turns` / `defer_tokens`) is engine-side, so `/toolkit`'s CacheConfirm doesn't apply: it
|
|
83
87
|
changes the declared tools at the front of the request, and the server re-reads **system prompt + tools +
|
|
@@ -171,7 +175,7 @@ right-hand column happens.
|
|
|
171
175
|
The next turn's prompt differs from the turn's last request at its tail: the previous answer's thinking is
|
|
172
176
|
stripped (the models' specs require it). A hybrid model on llama.cpp can only resume from a restore point, so the
|
|
173
177
|
next turn prefills from well before the end, more with every turn. `PromptWarmup` moves that work into the time
|
|
174
|
-
the user reads: after a completed turn, `
|
|
178
|
+
the user reads: after a completed turn, `TurnStages::Warmup#after` formats the next prompt up to where the new
|
|
175
179
|
user message starts (`KernelLoop#warmup_prompt`) and sends it in a thread (`Client#warm_up`: native
|
|
176
180
|
`/completion`, `n_predict: 1`, not streamed, no retries, log purpose `warmup`, then a `model warmup` line with
|
|
177
181
|
`cached=`, `prefilled=` and `prefill_ms=`).
|
|
@@ -218,7 +222,10 @@ is prefilled once. The native and the chat loop use the same plan.
|
|
|
218
222
|
- The debug log's `generation_completed` line: `prompt=` (the request's prompt tokens), `cached=` (read from the
|
|
219
223
|
server's cache), `cache_write=` (written to it, where the provider reports writes) and `reprefill=` (of the last
|
|
220
224
|
request's prompt on the same model through the same host, what was prefilled again; a `/model` to the same bare
|
|
221
|
-
model on another host is the switch's prefill, not a re-prefill). The
|
|
225
|
+
model on another host is the switch's prefill, not a re-prefill). The baseline is the Engine's `ReprefillMeter`
|
|
226
|
+
(`reprefill_meter.rb`), one for both loops: the last request whichever loop sent it, keyed by the model and the
|
|
227
|
+
host's client (`KernelLoop#client`, `ChatLoop#client`), so a switch to another host and back starts over on the
|
|
228
|
+
way back; the turn-end warm-up resets it (`KernelLoop#warmed_up!`). The `http` line of a
|
|
222
229
|
remote Claude or Messages request shows `cache=on` (or `cache=1h`) when it carried breakpoints.
|
|
223
230
|
- The warm-up logs `model warmup` with `cached=`, `prefilled=` and `prefill_ms=`.
|
|
224
231
|
- `/stats`: "cached N (P%)", "cache writes" and "re-prefilled" for the session.
|
|
@@ -121,7 +121,9 @@ the session commands*. Keep that table in step with `SessionAccessRoutes`.
|
|
|
121
121
|
- **Refusals.** `SessionAccessRoutes#refusal_response` (and the page routes that raise) send
|
|
122
122
|
`{error, detail, message, …fields}`: `detail` is the page's wording, `message` is the exception's own text, and the
|
|
123
123
|
fields are those the class is built from (`session_id`, `reason`, `ids`, `busy_id`, `version`).
|
|
124
|
-
`Remote#raise_refusal` rebuilds the class that Local's callee raises: `
|
|
124
|
+
`Remote#raise_refusal` rebuilds the class that Local's callee raises: `Session::NotFound` for a `404 not_found`
|
|
125
|
+
whose detail is `no session X` (or, from chi web 0.155.0 and older, `Session not found: X`), a plain
|
|
126
|
+
`ArgumentError` for another `not_found`, `Session::AmbiguousId`,
|
|
125
127
|
`SessionManager::OwnedByTUI` / `DeleteRefused` / `ArchiveRefused` / `RestartRefused`, `SessionContinue::Refused`,
|
|
126
128
|
and `ModelProfile::MissingModel` for `invalid_model` (no model there, or `--model`'s host unknown there: "could not
|
|
127
129
|
start a session: …", exit 1, as here).
|
|
@@ -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`, `card_stamps.js` and the `objectives_*.js` modules are new modules for that reason. The last
|
|
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`, `block_watch.js`, `card_stamps.js`, `inline_widgets.js`, `widget_walk.js`, `widget_picks.js` and the `objectives_*.js` modules are new modules for that reason. The last four `block_*` draw fenced blocks (`block_watch.js` finds and watches them, `block_host.js` is its frame drawer for a bundle's renderers): [block-renderers.md](block-renderers.md). `inline_widgets.js` is its core drawer for `chi-ui` blocks, drawing `widget_walk.js`'s and `widget_picks.js`'s widgets in a shadow root styled by `widgets.css`: [inline-widgets.md](inline-widgets.md).
|
|
28
28
|
|
|
29
29
|
## Two pages, two models
|
|
30
30
|
|
|
@@ -102,7 +102,8 @@ when it has outputs and none is `sent`. An edit-only item, a failed run's file o
|
|
|
102
102
|
pending: [Steering: add and unadd](#steering-add-and-unadd)) (`Engine#emit_working_set`, `EventFanout`'s):
|
|
103
103
|
- after each `:generation_started`, in `build_stream_event_handler` (`EventFanout`), from the running turn's
|
|
104
104
|
conversation (`Engine#live_conversation`, read on the turn's thread) under the turn's strategy
|
|
105
|
-
(
|
|
105
|
+
(`TurnContext#settings`' `llm_context`; `EventFanout#working_set_llm_context`: the running turn's, the last turn's
|
|
106
|
+
until it has its own). Both loops start a generation after `LLMContextApply.apply!`, so the set is
|
|
106
107
|
what that request sends;
|
|
107
108
|
- once in `Engine#end_turn`, after `replace_session_messages` and before the end event, from the kept messages (the
|
|
108
109
|
turn-end apply has run by then);
|
data/docs/plugins.md
CHANGED
|
@@ -1387,6 +1387,8 @@ block_renderers:
|
|
|
1387
1387
|
it refuses with "No memories to build".
|
|
1388
1388
|
- The first installed bundle (by name) with a language draws it. A bundle whose `requires_chi` this chi doesn't meet
|
|
1389
1389
|
draws nothing.
|
|
1390
|
+
- `chi-ui` is reserved: chi draws it itself (inline widgets, [internals/inline-widgets.md](internals/inline-widgets.md)),
|
|
1391
|
+
so a manifest that lists it is refused.
|
|
1390
1392
|
- **What it needs of the page:** `web.markdown` on (a default-off setting, and the commonmarker gem). With it off a
|
|
1391
1393
|
fence has no language in the page and nothing draws, though a hint in the bundle's prompt section still reaches
|
|
1392
1394
|
the model: say so in the bundle's README.
|
data/docs/releasing.md
CHANGED
|
@@ -13,7 +13,7 @@ can run the whole release; the user approves the notes before the tag and the
|
|
|
13
13
|
and `rake release:check` enforce it). The system bundle upgrades itself when
|
|
14
14
|
chi starts.
|
|
15
15
|
- **Every other shipped bundle** (btw, check-in, guardrails, known-names,
|
|
16
|
-
loop-guard, mcp, skills, slow-commands, agents-md, cost-cap, work-cap, source-links, github-pr, coordinator, claude-peer, codex-peer, ruby, model-notes, decision-card, web-search, and the profiles core and dev) has its own semver in its
|
|
16
|
+
loop-guard, mcp, skills, slow-commands, agents-md, cost-cap, work-cap, source-links, github-pr, coordinator, claude-peer, codex-peer, ruby, model-notes, decision-card, inline-widgets, web-search, and the profiles core and dev) has its own semver in its
|
|
17
17
|
`manifest.yml` and, when it needs a newer chi, a `requires_chi:` line. They never upgrade by themselves: users run
|
|
18
18
|
`chi update` (all of them, with the gem) or `chi bundle upgrade NAME`. So:
|
|
19
19
|
- a change to a bundle's files bumps that bundle's `version:`
|
data/docs/sessions.md
CHANGED
|
@@ -6,7 +6,7 @@ Sessions are plain files — no DB. Each session is `~/.local/state/samagotchi/s
|
|
|
6
6
|
|
|
7
7
|
**Verify verdicts:** a web Verify turn stamps its opening user message with `verify: {turn_id, quote}` (the selected source turn and quote; metadata only, never added to the prompt). When that turn completes and the final assistant reply's last non-empty line is a valid `Verdict: confirmed|refuted|unclear — <reason>`, the session file appends `{anchor: {turn_id, quote}, verdict, reason, by, check_turn_id, at}` to its optional `verdicts` array. `by` is the turn's author (nil for the owner), `check_turn_id` names the Verify turn, and `at` is its timestamp. The array is append-only; older files without it have no saved verdicts. An `unclear` verdict can be checked again by **Verify fresh** (a delegate child that gets only the claim; see *Delegating*): its verdict is appended to the parent's `verdicts` with `fresh: true`, `of` (the verdict it checked again) and `child_id`, from the parent's `fresh_verdicts/` dir, which its worker takes between turns. Verdicts are display state: none enters a prompt.
|
|
8
8
|
|
|
9
|
-
**Revise stacks:** a web Revise turn (POST /turn `revises: {turn_id}`, the answer it rewrites) stamps its opening message (its prompt) with `revises: {turn_id}` at the turn's end, only when the turn completed with an answer (`AnswerTail`): a failed, canceled or empty turn revised nothing (`
|
|
9
|
+
**Revise stacks:** a web Revise turn (POST /turn `revises: {turn_id}`, the answer it rewrites) stamps its opening message (its prompt) with `revises: {turn_id}` at the turn's end, only when the turn completed with an answer (`AnswerTail`): a failed, canceled or empty turn revised nothing (`TurnStages::VerifyAnchors#stamp_opening`). A draft no prompt carries (a stale page, a rolled-back turn) is no target: no stamp. A draft a stamped turn already revised is re-targeted to its stack's top, the newest version, so a stack is a chain `v1 ← v2 ← v3`. Like `verify`, it is metadata only, never added to the prompt (no change to a request's bytes); chi web's history passes it on the opening message (and on a wake turn's note), and `:turn_completed` carries the stamped `revises` (the participant view's too). The history also marks a Verify turn's opening `verify: true`, and so does its live end event (`:turn_completed`'s `verify: true`, with its `turn_id`): the page offers no Revise on its answer. The web page draws the stacks from these stamps (docs/cli.md, Revise). Like a Verify turn's, a Revise message waits for the running turn's end (`SessionInbox.turn_end_data?`; the Bridge takes either as `delivery: queue`, so it never cuts and its ack and `:turn_enqueued` say it queues), is sent only to a worker whose `bridge.json` names the `revise` feature (else 409 `revise_unavailable`), and a failed turn gives it back (`:prompt_restored`'s `revises`). Participants can't Revise: chi web and the Bridge drop it from their turns. Both anchors travel as one `TurnAnchors` inside chi; on the wire and in an input file each keeps its own key (`verify`, `revises`).
|
|
10
10
|
|
|
11
11
|
**Input files:** a message for a session's worker waits in `input/<timestamp>.json` (`SessionInbox.write_input`: `prompt`, `client_id`, `enqueued_id`, and `images`, `no_interrupt` and `delivery` when they apply; `delivery` is written only for `"queue"`). While a turn runs, the worker merges them at each step boundary in name order, except a queued one or one with images: those wait for the turn's end and run one turn each, in name order, and a file after them still merges (`Worker#pending_input_drain`). Such a file, with a prompt, from the user, `chi send` or a delegating parent, also ends a running `task_wait` or `delegate_result` and hands a running `execute` over as a task after 3 s (`SessionInbox.steer_waiting?`, a peek the worker's turn polls through `Engine#input_waiting?`; never in a participant's turn): the file stays for the drain. A queue is sent only to a worker whose `bridge.json` names the `delivery` feature; an older one would steer it, so `SessionManager.deliver_turn` refuses it (409 `delivery_unavailable`), and any other 4xx the Bridge answers is a refusal too, never a fallback file (except a 413, a body over its size limit: that one still goes in as a file, its `delivery` kept). A session command recognized in a file runs without a turn; its outcome is kept at `input/settled/<enqueued_id>` (`completed`, `failed` for a command error, `canceled` with `cancel_reason` `busy` or `dropped` for one refused mid-turn or dropped as the worker left; `origin: command`) so `chi send --wait` ends instead of waiting for a turn that will not run, its detail saying `the command failed` or `the command was refused (busy)` rather than a turn's words. **Reply files:** a turn that ends with visible text leaves its reply in `output/<timestamp>.txt`, or `output/<timestamp>-<enqueued_id>.txt` when the turn ran an input file with an id (`SessionInbox.write_output`), and the session's `last_turn` keeps that `enqueued_id`; so a wait for one queued message (`chi send --queue --wait`, `ReplyWait`'s `turn:`) finds its own turn's reply or end, never another turn's. The names still sort by time, and every reader takes `*.txt`.
|
|
12
12
|
|
|
@@ -23,7 +23,7 @@ Sessions are plain files — no DB. Each session is `~/.local/state/samagotchi/s
|
|
|
23
23
|
|
|
24
24
|
A session is deleted if **expired by age OR overflow by count** (unless `keep_status` or the live-owner guard: a session a worker or `chi` still has open is never pruned). A session's `status` is its turn state (`idle`/`running`), not whether a worker is alive. Orphan dirs without a `*.json` are never deleted, except skeleton-only ones (see *Empty sessions*). Deletion removes both `*.json` and sidecar dir atomically. Set both `DAYS=0` and `MAX=0` to retain forever.
|
|
25
25
|
|
|
26
|
-
**Worker idle exit:** a background worker (plain `chi`, Web UI sessions) exits after `SAMAGOTCHI_SESSION_IDLE_EXIT_MINUTES` (`session.idle_exit_minutes`, default `30`, `0`=never) with no turn running or queued, no client on its stream (an open web tab or attached terminal keeps it), no reminder registered, no background task it watches for a [finish wake](internals/background-tasks.md#finish-wake) still running or waiting for its wake turn (with `session.task_wakes: wake`; not one whose turn you stopped) and no continue offer (the step-limit question) waiting for an answer. It removes `bridge.json` and frees `owner.lock`; the next prompt or `--attach` wakes a new worker (`WorkerIdleExit`, `
|
|
26
|
+
**Worker idle exit:** a background worker (plain `chi`, Web UI sessions) exits after `SAMAGOTCHI_SESSION_IDLE_EXIT_MINUTES` (`session.idle_exit_minutes`, default `30`, `0`=never) with no turn running or queued, no client on its stream (an open web tab or attached terminal keeps it), no reminder registered, no background task it watches for a [finish wake](internals/background-tasks.md#finish-wake) still running or waiting for its wake turn (with `session.task_wakes: wake`; not one whose turn you stopped) and no continue offer (the step-limit question) waiting for an answer. It removes `bridge.json` and frees `owner.lock`; the next prompt or `--attach` wakes a new worker (`WorkerIdleExit`, `WorkerBoot.run_session_loop`). As its Bridge stops, the worker marks the lock leaving (`OwnerLock#mark_leaving`: `leaving_at`, and `leave_wait`, its `recap.timeout`), since it keeps the lock while it writes the recap on leave: an `--attach` or `--resume` meanwhile (here, or a remote one through chi web's wake) prints `(waiting for the session to finish its recap…)` (`LaunchMode::RECAP_WAIT`) and waits for the release, up to that budget plus `OwnerLock::LEAVE_MARGIN` (5 s), before starting the new worker (`OwnerLock.await_release`). A lock held by a worker that isn't leaving (a live one, a stuck one) is left as before. A client can also ask the worker to exit right away (Bridge `POST /session/:id/exit`; `/exit` in an attached terminal does, `/detach` doesn't): the same rules apply without the timeout (even with `0`), and the asker's own stream doesn't count. The reply names what keeps it up (for background tasks, which ones and how to stop them). When every hold is one the user can stop — reminders, a watched task still running, a delegate with a live worker — the 409 also carries `holds: [{kind:, items: [{id:, label:, detail:}]}]` (a reminder's detail is its schedule, `every 20m`/`every 1h`, `, due now` when it is already due), and an attached terminal asks what to do with them; its "stop them and exit" posts `{"stop": "all"}`, which clears the reminders, stops those tasks (`TaskRuntime.stop_task`, as `task_stop`) and stops those children (`SessionManager.stop_session`) before leaving, re-checking first that nothing the user can't stop appeared meanwhile (`HoldStop`). A delegate counts only for that ask: it never holds the idle exit, and an exit that deletes or archives the session does not ask about it. The session's status stays as it was, not `stopped`. With `"restart": true` (a worker whose sidecar lists the `restart` feature) the worker hands the session to a new worker on the newest chi installed: it leaves, then starts its successor once its lock is free, keeping even an empty session and writing no recap. Clients' streams don't hold a restart (a web tab and an attached terminal reconnect to the new worker); what would be lost does: a turn running or queued, a continue offer, a question or approval waiting, a `/btw` (or another anytime command) still running, a delegate's approval relay open or settled in the last 10 minutes, and background tasks this conversation started that still run (`WorkerIdleExit#hold_for_restart`; 409 `held` with `reason`). Reminders go along: as it leaves, the worker writes them to `<id>/reminders.json` (0600: `session_id`, `written_at`, and each reminder's id, name, description, interval and next fire as wall-clock time; `ReminderCarry`), and its successor reads and deletes the file as it starts, before its idle scheduler runs. A restored reminder keeps its id and schedule; one that came due meanwhile fires once at once, and one further ahead than its interval (a clock that jumped) is due one interval from now. The file counts only for the same session within 2 minutes of `written_at`, and not once the session is stopped: otherwise it is deleted unread (log `reminders_carry_dropped`, at warn unless the session was stopped). A stop that lands while the worker leaves for a restart wins: the worker writes no file, and no successor starts (`WorkerBoot.run_session_loop`, log `restart_dropped`); an archive deletes the file of each session it archives. Once an exit or restart is taken (claimed before a stop-all stops anything), the first one decides how the worker leaves: a later request of the same kind is answered as that exit, one of another kind (a stop-all from another terminal after a restart, chi web's "Restart workers" after a `/exit`) gets 409 `held` with `reason` `leaving_exit`, `leaving_delete` or `leaving_restart`, and a restart says it was refused rather than "restarted". A write that fails keeps the worker up with its reminders (log `reminders_carry_failed`), and the caller reports no new worker. A reminder already due when the restart is asked runs its turn first, so the restart's wait (10 s) may report no new worker yet though the hand-off follows. A worker from before this still refuses a restart for its reminders (409 `reminders`). Only an asked restart carries them: the fallback's TERM (a worker that can't be asked, below), a stop, an idle exit (which reminders hold anyway) and a crash drop them as before. What lives only in the worker's memory goes: the event history a late client replays, the kept `/btw` answers, a plugin's in-memory state.
|
|
27
27
|
|
|
28
28
|
**Empty sessions:** a session nothing happened in is deleted when it is left, so `chi` → `/exit` leaves no trace. Empty means: no messages, no turn tried (a failed turn leaves a `last_prompt` and keeps it), no memory used, no pending question, nothing in `input/`, `notes/` or `images/`, nothing in its dir beyond the worker's skeleton, mode `assist`, and the default model. A session on another model (`/model`, `--model`) counts as prepared and stays (`SessionManager.discardable?`). The worker checks as it idle-exits or leaves on a client's request and deletes it once its lock is free, checking again first. The attached `/exit` then says `Detached; the session is empty: it will be discarded if nothing arrives before its worker stops.` (a note or prompt landing first keeps it). The retention sweep catches those killed first: empty sessions over an hour old with no live owner, whatever `DAYS`/`MAX`, and skeleton-only dirs with no `*.json`. `session.keep_empty: true` (env `SAMAGOTCHI_SESSION_KEEP_EMPTY`) turns all of it off.
|
|
29
29
|
|
|
@@ -141,7 +141,7 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
|
|
|
141
141
|
| `parent_id` | The session that delegated this one or forked it (`ctx.sessions.fork`); `null` otherwise. See [Delegating](#delegating). | `SessionManager.spawn_session`; `ChildMove` writes a `parent.json` beside it instead of this file |
|
|
142
142
|
| `scratch` | A `chi scratch` session, deleted when its terminal leaves. | creation |
|
|
143
143
|
| `last_turn` | How the last turn ended (`LastTurn`), for a parent's wait and the web's notifications. | the Engine, at a turn's end |
|
|
144
|
-
| `verdicts` | Verdict cards recorded by completed Verify turns, in append order. | `
|
|
144
|
+
| `verdicts` | Verdict cards recorded by completed Verify turns, in append order. | `TurnStages::VerifyAnchors#record_verdict`, `VerifyFresh` |
|
|
145
145
|
| `delegate` | Started by the `delegate` tool (a fork has a `parent_id` but isn't one). | `Tools::Delegate` via `spawn_session` |
|
|
146
146
|
| `llm_context` | The session's own LLM context values (strategy, apply rule, budget), over the model's; `null` with none. | creation (`--llm-context`), `/llm-context`; copied by a fork and a continue, not a delegate child |
|
|
147
147
|
| `continues` | The previous link of a chain; `null` otherwise. See [Session chains](#session-chains). | `SessionContinue` |
|
|
@@ -345,7 +345,7 @@ The title names the session wherever its first message did: `chi sessions list`
|
|
|
345
345
|
|
|
346
346
|
A session can **continue** an earlier one: "the next run of the same routine" (a daily coordinator, say, continued once a day or several times). The new session's `continues` field names the previous link; the chain is derived from it, never stored: follow `continues` back for the earlier links, forward for the later ones. It is not a parent: archiving one link leaves the next alone, and lists fold delegates, not links. A link is continued at most once, so a chain has no forks and its latest link is the one nobody continues.
|
|
347
347
|
|
|
348
|
-
`
|
|
348
|
+
`SessionContinue.run` makes the next link:
|
|
349
349
|
|
|
350
350
|
- **What it copies:** the previous link's folder, its model (the name it was typed as, so an alias holds) unless the continue names one (`--model`, the web's `model`), and its own LLM context values. A model given is checked like a new session's (a host config.yml doesn't have refuses the continue, nothing archived). An inherited model whose host can't run it any more (gone from config.yml, `enabled: false`, an alias naming another host) **falls back to the default model** with a warning, `gone:org/model can't run (unknown host 'gone' in model …); started on <default>`, decided before the archive; with no default model configured the continue is refused. The carry note doesn't mention the model. The new link carries no transcript beyond the note's last answer, so a different model, or another provider, starts clean there. Its preview is the previous link's (until its first message is saved), unless it starts with a message. Its title (`/title`) is the previous link's, with or without a first message.
|
|
351
351
|
- **The carry note:** the new link starts with a context note from `session chain` (shown in its transcript as `note from session chain`), queued before its worker spawns, so its first turn sees it: `This session continues 3fa2c1d0 (2026-10-08 09:12), the previous link of its chain. Its recap, as of 2026-10-08 18:02:` and the previous link's recap (`recap.json`; cut at 8000 characters). No recap (fewer user turns than a recap needs, recaps off, none written in time): `… It left no recap.` Then `Its last answer:` and the previous link's last answer, read after the recap wait, so its exact shas, paths and next steps survive: a longer one keeps its last 4000 characters, after a `…` (a wrap-up closes on what comes next). There is none when the newest bubble was a step that asked for tools, when a later turn failed or was cut before it answered, and on a review link (a continue doesn't copy the grants, so the next link's participants could differ); `session.carry_last_answer: false` leaves it out of every note. No other transcript. The note never makes a continue fail: it stays within the 16 KiB a note may hold, the answer taking what the head and recap leave, and a session file that can't be read gives no answer. An idle new link holding only the note is kept, not discarded as empty.
|
data/docs/system-one.md
CHANGED
|
@@ -10,8 +10,9 @@ was sent and what came of it. The runs don't leave the machine; a hosted System
|
|
|
10
10
|
## The common layer
|
|
11
11
|
|
|
12
12
|
`SystemOne::Client` asks. `SystemOne::Recording` wraps it where a use builds its
|
|
13
|
-
client (`
|
|
14
|
-
|
|
13
|
+
client (`SystemOne::Recorder#wrap`, over the recorder `Engine#system_one_recorder` builds
|
|
14
|
+
for each call: the Engine's runs and its model then; `Engine#system_one_recording` for the
|
|
15
|
+
guardrails): the same duck type, the same answers and the same raises. A cancelled ask saves its run, then raises; a run that can't be saved is
|
|
15
16
|
one warn and never changes the answer. A use that needs the run's id at once passes it
|
|
16
17
|
(`client.ask(..., run_id: id)`); one client serves several threads.
|
|
17
18
|
|
data/docs/testing.md
CHANGED
|
@@ -260,7 +260,7 @@ any other file requires it, so a spec file run alone still finds its requires. E
|
|
|
260
260
|
| `reply_wait_looks.rb` | `ReplyWaitLooks`: counts a `ReplyWait`'s polls, so a step waits for the looks instead of a delay |
|
|
261
261
|
| `stamps.rb` | *helper*: `next_stamp`, distinct ordered times for the lib's `now:` seams; the pinned history clock |
|
|
262
262
|
| `task_dirs.rb` | `with_task_dir`: a tasks' folder of its own, every task stopped and its recorders joined after |
|
|
263
|
-
| `test_kernel.rb` | `TestKernel`: `test_kernel` (a real `KernelLoop` to stub `#run` on) and `test_client` for Engine specs |
|
|
263
|
+
| `test_kernel.rb` | `TestKernel`: `test_kernel` (a real `KernelLoop` to stub `#run` on; its options are `KernelLoop::Services` fields), `attach_services` (swap some of a kernel's services), `run_context` and `test_client` for Engine specs |
|
|
264
264
|
| `thinking_off.rb` | shared context "thinking off": `SAMAGOTCHI_THINKING_LEVEL=off` for the example |
|
|
265
265
|
| `virtual_terminal.rb` | `VirtualTerminal`: just enough VT100 to check what a Screen leaves on the terminal |
|
|
266
266
|
| `waiting.rb` | *helper*: `SpecWaiting` (`wait_for`, `wait_until`, `join!`, `pop!`, …), the test kit's helpers ([Waiting and time](#waiting-and-time)) |
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Samagotchi
|
|
4
|
+
autoload :InlineWidgets, File.expand_path("inline_widgets", __dir__)
|
|
4
5
|
autoload :Log, File.expand_path("log", __dir__)
|
|
5
6
|
autoload :VERSION, File.expand_path("version", __dir__)
|
|
6
7
|
module MemoryBundle
|
|
@@ -34,6 +35,9 @@ module Samagotchi
|
|
|
34
35
|
# A language a block can carry: a fence's first word, downcased
|
|
35
36
|
# (MarkdownRenderer sets it as data-lang under this same pattern).
|
|
36
37
|
LANG = /\A[a-z0-9_+-]{1,32}\z/
|
|
38
|
+
# Languages core draws itself: a bundle's renderer for one would be a
|
|
39
|
+
# second drawer for the same block.
|
|
40
|
+
RESERVED = [InlineWidgets::LANG].freeze
|
|
37
41
|
|
|
38
42
|
module_function
|
|
39
43
|
|
|
@@ -60,6 +64,7 @@ module Samagotchi
|
|
|
60
64
|
unless lang.is_a?(String) && lang.match?(LANG)
|
|
61
65
|
raise Invalid, "block_renderers: lang #{lang.inspect} isn't a fence's language (lowercase letters, digits, _ + -; at most 32)"
|
|
62
66
|
end
|
|
67
|
+
raise Invalid, "block_renderers: #{lang} is drawn by chi itself; a bundle can't render it" if RESERVED.include?(lang)
|
|
63
68
|
|
|
64
69
|
load = data["load"]
|
|
65
70
|
unless load.is_a?(Array) && !load.empty? && load.all? { |file| file.is_a?(String) && file.match?(MemoryBundle::Manifest::SCRIPT_FILE) }
|
|
@@ -304,7 +304,7 @@ module Samagotchi
|
|
|
304
304
|
|
|
305
305
|
def written_ok?(real, expected, name)
|
|
306
306
|
parse(File.read(real)) == expected &&
|
|
307
|
-
|
|
307
|
+
HostConfig.all(env: @env, path: real).key?(name.downcase)
|
|
308
308
|
rescue StandardError
|
|
309
309
|
false
|
|
310
310
|
end
|
|
@@ -348,11 +348,11 @@ module Samagotchi
|
|
|
348
348
|
# The full id +given+ names (an id or the start of one), or nil after
|
|
349
349
|
# saying why there is none.
|
|
350
350
|
def resolve_session(given)
|
|
351
|
-
id = Session.
|
|
352
|
-
Session.load(id, state_dir: @state_dir) #
|
|
351
|
+
id = Session.resolve!(given, state_dir: @state_dir)
|
|
352
|
+
Session.load(id, state_dir: @state_dir) # a file that won't load is refused here too
|
|
353
353
|
id
|
|
354
354
|
rescue ArgumentError => e
|
|
355
|
-
error_line("chi broadcast: #{e.
|
|
355
|
+
error_line("chi broadcast: #{e.message}")
|
|
356
356
|
nil
|
|
357
357
|
rescue SystemCallError => e
|
|
358
358
|
error_line("chi broadcast: session #{given} can't be read: #{e.message}")
|
|
@@ -421,7 +421,7 @@ module Samagotchi
|
|
|
421
421
|
end
|
|
422
422
|
end
|
|
423
423
|
{ "init" => registered.inits, "toolkit" => registered.toolkits, "tool source" => registered.tool_sources,
|
|
424
|
-
"peer source" => registered.peer_sources }.each do |what, names|
|
|
424
|
+
"peer source" => registered.peer_sources, "prompt section" => registered.prompt_sections }.each do |what, names|
|
|
425
425
|
names.each { |name| add(:info, :registered, plugin, "#{what} #{name}") }
|
|
426
426
|
end
|
|
427
427
|
end
|
|
@@ -130,7 +130,7 @@ module Samagotchi
|
|
|
130
130
|
|
|
131
131
|
# Stops what the run started that outlives its child chi: the
|
|
132
132
|
# workers of its delegates (they leave the child's process tree,
|
|
133
|
-
#
|
|
133
|
+
# WorkerBoot.detach_from_spawner) and its tasks (a group of
|
|
134
134
|
# their own each, reparented once the child exits). Found by their
|
|
135
135
|
# records in the sandbox, so nothing outside it: a session's lock
|
|
136
136
|
# held by a group leader, a running task's record whose pid still
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: dev
|
|
3
|
-
version: 0.7.
|
|
3
|
+
version: 0.7.8
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Developer tools"
|
|
6
6
|
trust_level: reviewed
|
|
7
|
-
requires_chi: ">= 0.
|
|
7
|
+
requires_chi: ">= 0.155.0"
|
|
8
8
|
includes:
|
|
9
9
|
- known-names
|
|
10
10
|
- mcp
|
|
@@ -18,3 +18,4 @@ includes:
|
|
|
18
18
|
- ruby
|
|
19
19
|
- model-notes
|
|
20
20
|
- decision-card
|
|
21
|
+
- inline-widgets
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: inline-widgets
|
|
3
|
+
version: 0.1.0
|
|
4
|
+
scope: system
|
|
5
|
+
description: "Teaches the model the chi-ui blocks chi web draws: picks (rows of options that fill your reply) and walk (a scenario stepped through)"
|
|
6
|
+
trust_level: reviewed
|
|
7
|
+
plugin:
|
|
8
|
+
file: plugin.rb
|
|
9
|
+
sha256: sha256:24ad0c3f0ffb592826b4a7389111d76723e9f28ea4d94ab63abda7986d3311bb
|
|
10
|
+
# chi.prompt_section is 0.132.0's; the web draws chi-ui blocks only from the next release
|
|
11
|
+
requires_chi: ">= 0.155.0"
|