samagotchi 0.153.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +42 -1
- data/README.md +2 -3
- data/bin/chi +23 -24
- data/docs/architecture.md +30 -33
- data/docs/broadcast.md +5 -5
- data/docs/bundles.md +9 -12
- data/docs/cli.md +82 -96
- data/docs/configuration.md +27 -26
- data/docs/context.md +3 -3
- data/docs/desktop.md +1 -1
- data/docs/guardrails.md +8 -10
- data/docs/hooks.md +9 -9
- data/docs/internals/attached-context.md +2 -2
- data/docs/internals/background-tasks.md +3 -4
- data/docs/internals/block-renderers.md +4 -0
- data/docs/internals/bridge.md +21 -8
- data/docs/internals/context-telemetry.md +1 -1
- data/docs/internals/engine.md +33 -7
- data/docs/internals/objectives.md +6 -5
- data/docs/internals/participants.md +7 -7
- data/docs/internals/prompt-caching.md +4 -2
- data/docs/internals/remote-sessions.md +7 -7
- data/docs/internals/web-frontend.md +2 -2
- data/docs/internals/web-push.md +1 -1
- data/docs/memory.md +2 -2
- data/docs/plugins.md +23 -28
- data/docs/sessions.md +24 -24
- data/docs/sub-agent.md +2 -2
- data/docs/system-one.md +1 -1
- data/docs/testing.md +31 -6
- data/lib/samagotchi/answer_text.rb +33 -0
- data/lib/samagotchi/bridge/card_places.rb +56 -7
- data/lib/samagotchi/bridge/card_store.rb +24 -9
- data/lib/samagotchi/bridge.rb +52 -32
- data/lib/samagotchi/bridge_client.rb +19 -14
- data/lib/samagotchi/broadcast/recipients.rb +2 -2
- data/lib/samagotchi/broadcast/triage.rb +1 -1
- data/lib/samagotchi/broadcast_command.rb +5 -5
- data/lib/samagotchi/bundle_command/try/command.rb +1 -1
- data/lib/samagotchi/bundle_command/try/run.rb +1 -1
- data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +2 -2
- data/lib/samagotchi/bundles/btw/manifest.yml +2 -2
- data/lib/samagotchi/bundles/btw/plugin.rb +1 -1
- data/lib/samagotchi/bundles/coordinator/manifest.yml +2 -2
- data/lib/samagotchi/bundles/coordinator/plugin.rb +3 -2
- data/lib/samagotchi/bundles/decision-card/manifest.yml +4 -4
- data/lib/samagotchi/bundles/decision-card/plugin.rb +1 -9
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
- data/lib/samagotchi/bundles/github-pr/manifest.yml +2 -2
- data/lib/samagotchi/bundles/github-pr/plugin.rb +3 -3
- data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/plugin.rb +3 -2
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +3 -4
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/child_move.rb +3 -3
- data/lib/samagotchi/commands/registry.rb +6 -6
- data/lib/samagotchi/composer_route.rb +4 -5
- data/lib/samagotchi/config.rb +14 -12
- data/lib/samagotchi/context_candidate_choice.rb +1 -1
- data/lib/samagotchi/context_candidates.rb +4 -3
- data/lib/samagotchi/continue_offer.rb +1 -1
- data/lib/samagotchi/debug_log.rb +1 -1
- data/lib/samagotchi/degrade.rb +75 -0
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +3 -3
- data/lib/samagotchi/engine/cards.rb +87 -0
- data/lib/samagotchi/engine/context_judge.rb +138 -0
- data/lib/samagotchi/engine/events.rb +131 -0
- data/lib/samagotchi/engine/guardrails.rb +87 -0
- data/lib/samagotchi/engine/hooks.rb +119 -0
- data/lib/samagotchi/engine/messages.rb +124 -0
- data/lib/samagotchi/engine/model_settings.rb +127 -0
- data/lib/samagotchi/engine/models.rb +261 -0
- data/lib/samagotchi/engine/objectives.rb +116 -0
- data/lib/samagotchi/engine/participants.rb +86 -0
- data/lib/samagotchi/engine/plugins.rb +208 -0
- data/lib/samagotchi/engine/questions.rb +98 -0
- data/lib/samagotchi/engine/recap.rb +130 -0
- data/lib/samagotchi/engine/session.rb +131 -0
- data/lib/samagotchi/engine/side_requests.rb +135 -0
- data/lib/samagotchi/engine/steering.rb +134 -0
- data/lib/samagotchi/engine/toolkit.rb +269 -0
- data/lib/samagotchi/engine/turn_end.rb +180 -0
- data/lib/samagotchi/engine/turn_helpers.rb +160 -0
- data/lib/samagotchi/engine/verify.rb +153 -0
- data/lib/samagotchi/engine.rb +62 -2646
- data/lib/samagotchi/engine_snapshots.rb +9 -8
- data/lib/samagotchi/grants.rb +575 -0
- data/lib/samagotchi/guardrail_wiring.rb +2 -2
- data/lib/samagotchi/guardrails/context.rb +2 -3
- data/lib/samagotchi/guardrails/parent_approvals.rb +2 -2
- data/lib/samagotchi/guardrails/parent_policy.rb +2 -2
- data/lib/samagotchi/hook_routes.rb +3 -3
- data/lib/samagotchi/idle_recap.rb +7 -6
- data/lib/samagotchi/idle_reminders.rb +3 -4
- data/lib/samagotchi/iteration_limit.rb +2 -2
- data/lib/samagotchi/kernel_loop.rb +13 -13
- data/lib/samagotchi/launch_mode.rb +13 -32
- data/lib/samagotchi/level_setting_command.rb +3 -2
- data/lib/samagotchi/llm/chat_loop.rb +3 -1
- data/lib/samagotchi/llm_context_command.rb +3 -2
- data/lib/samagotchi/log.rb +7 -7
- data/lib/samagotchi/log_line.rb +1 -1
- data/lib/samagotchi/log_path.rb +1 -1
- data/lib/samagotchi/note_command.rb +1 -1
- data/lib/samagotchi/note_delivery.rb +18 -8
- data/lib/samagotchi/objectives.rb +24 -3
- data/lib/samagotchi/objectives_check.rb +8 -27
- data/lib/samagotchi/objectives_command.rb +4 -69
- data/lib/samagotchi/objectives_edit.rb +87 -0
- data/lib/samagotchi/output_formatter.rb +7 -1
- data/lib/samagotchi/owner_lock.rb +7 -1
- data/lib/samagotchi/parent_answers_command.rb +5 -4
- data/lib/samagotchi/participant_exposure.rb +42 -0
- data/lib/samagotchi/participant_mode.rb +1 -1
- data/lib/samagotchi/participant_name.rb +1 -1
- data/lib/samagotchi/participants.rb +17 -17
- data/lib/samagotchi/participants_watch.rb +1 -1
- data/lib/samagotchi/plugin/api.rb +3 -2
- data/lib/samagotchi/plugin/context.rb +21 -7
- data/lib/samagotchi/plugin/peer.rb +2 -1
- data/lib/samagotchi/plugin/sessions.rb +6 -4
- data/lib/samagotchi/plugin_tasks.rb +1 -2
- data/lib/samagotchi/private_files.rb +29 -0
- data/lib/samagotchi/project_env.rb +2 -2
- data/lib/samagotchi/question_desk.rb +8 -7
- data/lib/samagotchi/recap_command.rb +2 -1
- data/lib/samagotchi/recap_setup.rb +1 -1
- data/lib/samagotchi/recap_store.rb +1 -1
- data/lib/samagotchi/redact.rb +86 -0
- data/lib/samagotchi/reminder_queue.rb +2 -2
- data/lib/samagotchi/remote_attach_command.rb +1 -1
- data/lib/samagotchi/reply_wait/poll.rb +2 -2
- data/lib/samagotchi/review_start.rb +1 -1
- data/lib/samagotchi/run_options.rb +6 -6
- data/lib/samagotchi/self_report.rb +42 -39
- data/lib/samagotchi/send_command/busy.rb +1 -1
- data/lib/samagotchi/send_command.rb +4 -1
- data/lib/samagotchi/session.rb +11 -2
- data/lib/samagotchi/session_access/contract.rb +3 -3
- data/lib/samagotchi/session_access/local.rb +9 -9
- data/lib/samagotchi/session_access/remote.rb +17 -7
- data/lib/samagotchi/session_access.rb +4 -4
- data/lib/samagotchi/session_archive_command.rb +2 -2
- data/lib/samagotchi/session_chain.rb +8 -6
- data/lib/samagotchi/session_commands.rb +45 -48
- data/lib/samagotchi/session_continue.rb +1 -1
- data/lib/samagotchi/session_delete_command.rb +4 -4
- data/lib/samagotchi/session_manager.rb +67 -49
- data/lib/samagotchi/session_metrics.rb +2 -2
- data/lib/samagotchi/session_model_settings.rb +3 -2
- data/lib/samagotchi/session_participants.rb +1 -1
- data/lib/samagotchi/session_retention.rb +27 -7
- data/lib/samagotchi/sessions_command.rb +4 -4
- data/lib/samagotchi/share_command.rb +10 -10
- data/lib/samagotchi/steer.rb +13 -2
- data/lib/samagotchi/stream_event.rb +22 -0
- data/lib/samagotchi/system_prompt.rb +154 -145
- data/lib/samagotchi/task_watch.rb +1 -1
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +72 -10
- data/lib/samagotchi/terminal_ui/attached_loop/composer.rb +12 -9
- data/lib/samagotchi/terminal_ui/attached_loop/exit_commands.rb +16 -0
- data/lib/samagotchi/terminal_ui/attached_loop/questions.rb +2 -3
- data/lib/samagotchi/terminal_ui/attached_loop.rb +17 -8
- data/lib/samagotchi/terminal_ui/attached_view.rb +1 -11
- data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -5
- data/lib/samagotchi/terminal_ui/formatting.rb +7 -7
- data/lib/samagotchi/terminal_ui/input_support.rb +17 -22
- data/lib/samagotchi/terminal_ui/join_replay.rb +16 -12
- data/lib/samagotchi/terminal_ui/line_reader.rb +2 -2
- data/lib/samagotchi/terminal_ui/live_region.rb +2 -2
- data/lib/samagotchi/terminal_ui/log_tail.rb +135 -0
- data/lib/samagotchi/terminal_ui/one_shot_sink.rb +2 -2
- data/lib/samagotchi/terminal_ui/plain_surface.rb +1 -1
- data/lib/samagotchi/terminal_ui/question_prompt.rb +4 -5
- data/lib/samagotchi/terminal_ui/reline_seam.rb +2 -12
- data/lib/samagotchi/terminal_ui/status_row.rb +6 -14
- data/lib/samagotchi/terminal_ui.rb +142 -1387
- data/lib/samagotchi/title_command.rb +2 -2
- data/lib/samagotchi/tool_runner.rb +15 -16
- data/lib/samagotchi/toolkit_command.rb +2 -1
- data/lib/samagotchi/tools/delegate.rb +3 -1
- data/lib/samagotchi/tools/delegate_cursor.rb +3 -3
- data/lib/samagotchi/tools/delegate_wait.rb +1 -1
- data/lib/samagotchi/tools/objectives_tool.rb +2 -2
- data/lib/samagotchi/tools/send_note.rb +2 -1
- data/lib/samagotchi/turn_endings.rb +2 -2
- data/lib/samagotchi/turn_flow.rb +11 -7
- data/lib/samagotchi/update_hint.rb +2 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +42 -51
- data/lib/samagotchi/web/message_display.rb +7 -0
- data/lib/samagotchi/web/participant_access.rb +2 -2
- data/lib/samagotchi/web/participant_review.rb +5 -5
- data/lib/samagotchi/web/participant_stream_filter.rb +8 -8
- data/lib/samagotchi/web/participant_view.rb +2 -82
- data/lib/samagotchi/web/participants_admin.rb +1 -1
- data/lib/samagotchi/web/public/block_host.js +35 -8
- data/lib/samagotchi/web/public/block_renderers.css +9 -5
- data/lib/samagotchi/web/public/card_stamps.js +27 -0
- data/lib/samagotchi/web/public/cards_place.js +5 -3
- data/lib/samagotchi/web/public/chains.js +2 -2
- data/lib/samagotchi/web/public/claim_link.js +4 -4
- data/lib/samagotchi/web/public/composer_mode.js +1 -1
- data/lib/samagotchi/web/public/format.js +4 -3
- data/lib/samagotchi/web/public/turn_html.js +8 -4
- data/lib/samagotchi/web/push/store.rb +1 -1
- data/lib/samagotchi/web/session_access_routes.rb +13 -4
- data/lib/samagotchi/web/session_control.rb +12 -8
- data/lib/samagotchi/web/session_create.rb +1 -1
- data/lib/samagotchi/web/session_hub.rb +2 -2
- data/lib/samagotchi/web/session_show.rb +6 -2
- data/lib/samagotchi/web/session_summary.rb +1 -1
- data/lib/samagotchi/worker.rb +103 -9
- data/lib/samagotchi/worker_link.rb +3 -3
- data/lib/samagotchi/working_set_command.rb +3 -2
- data/lib/samagotchi.rb +0 -1
- metadata +31 -7
- data/lib/samagotchi/pending_input_queue.rb +0 -55
- data/lib/samagotchi/terminal_ui/between_turns.rb +0 -138
- data/lib/samagotchi/terminal_ui/repl_input.rb +0 -152
- data/lib/samagotchi/web/grants.rb +0 -575
- data/lib/samagotchi/web/participant_exposure.rb +0 -44
- data/lib/samagotchi/web/private_files.rb +0 -31
data/docs/guardrails.md
CHANGED
|
@@ -23,7 +23,7 @@ The UI shows the tool line first, then the approval under it.
|
|
|
23
23
|
|
|
24
24
|
## Ask
|
|
25
25
|
|
|
26
|
-
The
|
|
26
|
+
The attached TUI and the web show what would run, where and why:
|
|
27
27
|
|
|
28
28
|
```
|
|
29
29
|
Approve tool call?
|
|
@@ -65,8 +65,7 @@ card comes back through `ApprovalFacts.from_h` (string or symbol keys, missing f
|
|
|
65
65
|
|
|
66
66
|
Who answers:
|
|
67
67
|
|
|
68
|
-
-
|
|
69
|
-
- A shared session's worker (plain `chi` and `chi -p …` while `session.shared` is on, the default): any attached TUI or web page. With none attached,
|
|
68
|
+
- A session's worker (plain `chi`, `chi -p …`, the web): any attached TUI or web page. With none attached,
|
|
70
69
|
the approval waits (in the session file) and shows on attach.
|
|
71
70
|
- `-p … --non-interactive`: nobody; the call is denied ("No one to approve it
|
|
72
71
|
(non-interactive run)").
|
|
@@ -134,7 +133,7 @@ others come back to the parent's model with the block below. A dismissal
|
|
|
134
133
|
always passes (the hook decides what no answer means): the strategy is a
|
|
135
134
|
convention for an honest parent, not a barrier. This section is
|
|
136
135
|
the path for other agents, and for a chi parent that can't host the relay (a
|
|
137
|
-
`--
|
|
136
|
+
`--non-interactive` one-shot): there the model gets the question
|
|
138
137
|
block and the rules below. With the relay, the same rules still hold for a
|
|
139
138
|
parent model that answers its own relay card (a piped or marked `chi --attach`
|
|
140
139
|
on the parent): that answer is a parent agent's, checked by the parent and
|
|
@@ -197,10 +196,10 @@ value that isn't a level reads as `hard`.
|
|
|
197
196
|
config.yml's; `/parent-answers LEVEL` sets the session's own, `/parent-answers
|
|
198
197
|
default` drops it (config.yml's applies). Anyone may tighten it, a parent too
|
|
199
198
|
(`chi send ID -m "/parent-answers hard"`). Loosening it (toward `soft`, or
|
|
200
|
-
`default`) is yours alone: the line must come from you at
|
|
199
|
+
`default`) is yours alone: the line must come from you at an attached
|
|
201
200
|
terminal or the web composer. chi refuses it (`loosening parent_answers is the
|
|
202
201
|
user's: …`) from `chi send` (a parent's or yours: the worker can't tell them
|
|
203
|
-
apart), from a piped or marked `chi --attach
|
|
202
|
+
apart), from a piped or marked `chi --attach`, from `chi -p
|
|
204
203
|
--non-interactive`, from a delegate's follow-up and from a plugin; `chi answer`
|
|
205
204
|
has no command route. `--parent-answers` on `--attach`/`--resume` is ignored,
|
|
206
205
|
never sent as the command.
|
|
@@ -277,7 +276,7 @@ no running turn, pending question or approval, queued prompt, or running
|
|
|
277
276
|
background task; when one exists, stop it explicitly and then send a prompt.
|
|
278
277
|
|
|
279
278
|
A parent may also type its answer into chi itself: a piped `chi --attach ID`
|
|
280
|
-
or `printf '2\n' | chi
|
|
279
|
+
or `printf '2\n' | chi -p …`. So chi treats an answer as a
|
|
281
280
|
parent's, held to the same setting, when its input isn't a terminal or an
|
|
282
281
|
agent marker is set: `CLAUDECODE` (Claude Code), `AI_AGENT`, `CODEX_THREAD_ID`
|
|
283
282
|
(Codex CLI), or `SAMAGOTCHI_PARENT_SESSION`,
|
|
@@ -1327,9 +1326,8 @@ allows), and how many got no judgment. `--threshold` overrides
|
|
|
1327
1326
|
for this run; `--dry-run` judges and prints without writing a line. Without a
|
|
1328
1327
|
client (`mode: off`, or no `host`) it exits 2 saying what to set.
|
|
1329
1328
|
|
|
1330
|
-
|
|
1331
|
-
questions per session,
|
|
1332
|
-
`cards.json` at all, so its asks are not replayable.
|
|
1329
|
+
A limit on what the replay can see: `CardStore` keeps only the last 20
|
|
1330
|
+
questions per session, so older asks are not replayable.
|
|
1333
1331
|
|
|
1334
1332
|
## The guardrails bundle
|
|
1335
1333
|
|
data/docs/hooks.md
CHANGED
|
@@ -97,14 +97,14 @@ class Watchful
|
|
|
97
97
|
def call(event)
|
|
98
98
|
case event[:type]
|
|
99
99
|
when :after_generation
|
|
100
|
-
# One line in the
|
|
100
|
+
# One line in the attached TUI and the web ("<bundle>> text",
|
|
101
101
|
# or "hook> text" for a config hook); level: :warn colours it.
|
|
102
102
|
# fallback_for: :display marks a line a UI that renders the answer's
|
|
103
103
|
# display may leave out (below).
|
|
104
104
|
event[:notify].call("the model repeated itself", level: :warn)
|
|
105
105
|
when :before_tool_call
|
|
106
|
-
# A single-select question through the question flow (
|
|
107
|
-
# TUI, web); returns {selected: [...], freeform:, selected_indices:}
|
|
106
|
+
# A single-select question through the question flow (the attached
|
|
107
|
+
# TUI, the web); returns {selected: [...], freeform:, selected_indices:}
|
|
108
108
|
# or nil when there is no one to ask (--non-interactive), the
|
|
109
109
|
# question was dismissed, or the options (or effects) were invalid.
|
|
110
110
|
# effects: says what an option does for a parent agent's answer (see
|
|
@@ -156,7 +156,7 @@ that a UI that already shows it can leave the line out. The main value is
|
|
|
156
156
|
links), for a UI that doesn't render that display. The hook never names a UI;
|
|
157
157
|
each decides by what it renders. The web with markdown on (`web.markdown` and
|
|
158
158
|
the commonmarker gem) leaves such a line out, live and after a reload; with
|
|
159
|
-
markdown off it shows, as it does in the
|
|
159
|
+
markdown off it shows, as it does in the attached TUI and
|
|
160
160
|
`chi -p`, which print the answer as text (in the web its http(s) URLs are
|
|
161
161
|
links, as in any hook's notice line). The log keeps the line with
|
|
162
162
|
`fallback_for=display`. `:editor` marks a line that names a file the viewer
|
|
@@ -304,8 +304,8 @@ end
|
|
|
304
304
|
kept), on a live turn and after a reload. Its copy button copies the
|
|
305
305
|
display text. The page learns about it from an `answer_display` event
|
|
306
306
|
that comes after `turn_completed` (the hooks run after the turn ended).
|
|
307
|
-
- **
|
|
308
|
-
and
|
|
307
|
+
- **The terminal** (the attached TUI, a `--non-interactive` one-shot) has printed the answer by then
|
|
308
|
+
and does not change it; use `event[:notify]` for something it should show.
|
|
309
309
|
|
|
310
310
|
A plugin gets the same from `chi.on(:after_turn) { |event, ctx| event[:present].call { … } }`.
|
|
311
311
|
|
|
@@ -569,7 +569,7 @@ the voter's own (a guard that wants the model to retry a corrected call:
|
|
|
569
569
|
`Retry with "…".`).
|
|
570
570
|
|
|
571
571
|
`event[:context]` is `{cwd:, repo_root:, branch:, session_id:, interface:, origin:}`
|
|
572
|
-
(`interface` is `:
|
|
572
|
+
(`interface` is `:worker` or `:non_interactive`). `event[:targets]` is
|
|
573
573
|
what the call acts on, resolved as the tools resolve it:
|
|
574
574
|
`{command:, paths:, cwd:, repo_root:, outside_repo:, git_dirs:}`. `outside_repo`
|
|
575
575
|
is measured from the session's repo, not the call's `cwd:`; `git_dirs` lists where
|
|
@@ -855,8 +855,8 @@ qualified ref:
|
|
|
855
855
|
|
|
856
856
|
**Where `{repo}` and `{host}` come from.** Without a named group that took
|
|
857
857
|
part, they come from `git remote get-url <remote>` run in the session's
|
|
858
|
-
working directory (the worker's;
|
|
859
|
-
current directory). `remote:` picks the remote, default `origin` — a fork
|
|
858
|
+
working directory (the worker's; a `--non-interactive` one-shot's is the
|
|
859
|
+
terminal's current directory). `remote:` picks the remote, default `origin` — a fork
|
|
860
860
|
sets `remote: upstream`. Git applies `insteadOf` rewrites and includes, and a
|
|
861
861
|
worktree reports its main repo's remote. The URL forms understood are
|
|
862
862
|
`https://`, `http://`, `ssh://`, `git://` (credentials and port dropped) and
|
|
@@ -125,11 +125,11 @@ name (or whose hint is its URL) is attached, *declined* while the declined marke
|
|
|
125
125
|
"PR", branch names ≥ 6 chars with `/ - _`, exact URLs, fenced code blanked). Only `OwnerPrompt` texts count
|
|
126
126
|
(`owner_texts`), so a participant's message, a steer, a note or a reminder never attaches anything; a review
|
|
127
127
|
session skips all of it.
|
|
128
|
-
- **Where it runs.** `before_turn` (from `Worker#run_prompt` before the absorb, and
|
|
128
|
+
- **Where it runs.** `before_turn` (from `Worker#run_prompt` before the absorb, and a `--non-interactive` one-shot's turn start, `TerminalUI`): attach
|
|
129
129
|
each proposed candidate the owner's prompts so far mention (`attach` writes a session `Source` through
|
|
130
130
|
`source_for`: a URL resolves through `ContextProviders`, else the candidate's own `cmd`), then `first_fetch` them
|
|
131
131
|
together within `FIRST_FETCH_SECONDS` (10) so the note lands in this turn; cut short, the fetch records nothing and
|
|
132
|
-
the poller tries soon. `after_propose` does the same for a late proposal.
|
|
132
|
+
the poller tries soon. `after_propose` does the same for a late proposal. A one-shot has no absorber running, so
|
|
133
133
|
`deliver_notes` runs one.
|
|
134
134
|
- Forks and continues take no candidates.
|
|
135
135
|
|
|
@@ -36,8 +36,7 @@ Behavior:
|
|
|
36
36
|
ends the wait when a person's message waits for the turn's next step (`Engine#input_waiting?`, run_turn's
|
|
37
37
|
`input_waiting:` probe through the kernel's `PeerView`): in a worker an input file that the next boundary merges
|
|
38
38
|
from the user, `chi send` or a delegating parent (`SessionInbox.steer_waiting?`; never a queued, image or
|
|
39
|
-
participant's one, never a plugin's steer or a child's report, never during a participant's turn)
|
|
40
|
-
(`--no-shared`) a line in its steering queue. The probe only peeks: the drain still merges the message at the
|
|
39
|
+
participant's one, never a plugin's steer or a child's report, never during a participant's turn). The probe only peeks: the drain still merges the message at the
|
|
41
40
|
next boundary. The result says `note: a message from the user is waiting; the task is STILL RUNNING.`, how to wait
|
|
42
41
|
again or stop it (`TaskWait::MESSAGE_WAITING_NOTE`), and `waited: Ns`; the task keeps running and stays watched
|
|
43
42
|
(below). A task that finished meanwhile gives its finished result, as after a Stop. A `chi -p` turn has no probe. The same probe ends a blocking `delegate_result` (and `delegate`'s wait) early; see [Sessions: Delegating](../sessions.md).
|
|
@@ -51,7 +50,7 @@ Behavior:
|
|
|
51
50
|
|
|
52
51
|
A task the model leaves running (it ends its turn: "I'll check when it's finished") is reported when it ends, so the result doesn't wait for the user's next message. `session.task_wakes` (env `SAMAGOTCHI_SESSION_TASK_WAKES`): `wake` (default), `queue` or `off` (no watch, as before).
|
|
53
52
|
|
|
54
|
-
- **The watch** (`TaskWatch`, `task_watch.rb`) is the Worker's (`WorkerTaskWakes`, `worker_task_wakes.rb`, which writes the notes; the Worker runs the wake turn), in memory:
|
|
53
|
+
- **The watch** (`TaskWatch`, `task_watch.rb`) is the Worker's (`WorkerTaskWakes`, `worker_task_wakes.rb`, which writes the notes; the Worker runs the wake turn), in memory: a `--non-interactive` one-shot has none, and a crash loses it (those tasks behave as before). The Worker's Engine observer feeds it the raw `:tool_call_completed` events, the input `Bridge#own_task_ids` reads too. A `task_create` result and an `execute` hand-over add the task (`TaskRuntime.created_id`: a `task_id:` the command printed doesn't count), the hand-over's limit kept for the note. What shows the model the end removes it: a `task_wait` with a final status, or with `wait_result: pattern_matched` (the model chose its own "done" marker); a `task_get` with a final status; any `task_stop`. `task_list` and a `read` of `output.log` don't: a model that read the log still gets the wake once. Tasks a tool starts inside an anytime command (`/btw` and the like) reach the same observer, so they are watched too.
|
|
55
54
|
- **The idle tick**: with something watched, each idle pass (after delegate reports, before due reminders) refreshes the watched records (`TaskRuntime.get_record`, one `ps` per running task) and takes the finished ones, keeping only the ids the conversation still holds (`TaskRuntime.created_ids_in`: a `!rollback` or a restored turn may have dropped the result that started one; an LLM context strategy can't, since its stale and forget stubs are drawn in the request, never written over the saved result). A note that can't be written puts its task back for the next pass. An empty watch costs nothing. A task that finishes during a turn and is still unseen when the turn ends wakes on the next idle pass. One the user stopped with the web's stop-task button is dropped without a note.
|
|
56
55
|
- **The note** is a context note under its own header (`ContextNote::TASK_HEADER`; `TaskFinishNote`), `note_id: task-<id>`, `source: task <id8>`:
|
|
57
56
|
|
|
@@ -70,6 +69,6 @@ A task the model leaves running (it ends its turn: "I'll check when it's finishe
|
|
|
70
69
|
- **A plain note instead** (the same text without the last two lines, no turn start), written at once, with `queue`, the budget spent (the UIs hear `N finished background tasks noted; the model reads them with your next message` once per human input, the flag shared with the other wakes), wakes paused, a continue offer waiting, input queued, an exit asked for, or a **note-only** task: one started or waited on in a turn the user stopped (reasons `user`, `manual`, `ctrl_c`), since a wake telling the model to carry on would undo that Stop. A prompt's turn takes the finished ones as plain notes first.
|
|
71
70
|
- **Failure**: a wake turn that fails and kept nothing, or is stopped and restored, turns its note back into the plain one (`WorkerInbound#unmark_wake`, as for a context wake) and pauses wakes until the user writes.
|
|
72
71
|
- **Idle exit**: with `wake`, a watched task holds the worker's idle exit (`WorkerIdleExit`'s `:background_tasks`) until its wake is taken: while it runs, and once it finished until the next idle pass takes it (`TaskWatch#holding`, in memory, no `ps`), so one that ends between the pass's look and the exit's check still wakes the session. A still-running one holds `/exit` too (`TaskWatch#running`, read once per request by `WorkerIdleExit#request_hold`; the 409 lists the tasks, `{id, command, age_seconds}`, with a `hint` on stopping them, and the attached terminal names them and says to use the web's stop-task button or ask for `task_stop`), as reminders do. When the only holds are ones the user can stop, the 409 carries `holds` too and the terminal asks whether to stop them all (`HoldStop`, which stops these tasks the same way `task_stop` does). A note-only task holds neither (no wake would come for it), nor does one whose note, or the look for it (the conversation's task ids unreadable), failed 3 times (`TaskWatch::MAX_FAILURES`: it is still tried at each look, but can't keep the worker up for good). With `queue` nothing holds: nothing would wake for the end, so keeping the worker up isn't worth it. As the worker leaves (any exit but a stop, a signal or an `/exit --delete`), the tasks that finished since the last look are written as plain notes; a task that ends after the worker left gets no note (the model sees its end at its next look, as before). A forgotten dev server keeps the worker up until it is stopped; a crash of that server wakes the session.
|
|
73
|
-
- **The tool descriptions**: a worker's `execute` and `task_create` say "if you end your turn before it finishes, chi tells you when it does" (left out under `off` and in
|
|
72
|
+
- **The tool descriptions**: a worker's `execute` and `task_create` say "if you end your turn before it finishes, chi tells you when it does" (left out under `off` and in a `--non-interactive` one-shot), fixed when the worker builds its Engine.
|
|
74
73
|
|
|
75
74
|
Work that needs a model, not a shell command, goes to a child chi session instead: `delegate` / `delegate_result` have the same shape (start, then wait) and return only the child's final reply; see [Sessions: Delegating](../sessions.md#delegating).
|
|
@@ -30,6 +30,10 @@ model. The TUI is untouched: it shows a fence as source.
|
|
|
30
30
|
clone arrives with both. On failure the `<pre>` stays, marked `data-block-failed`, with one muted line: an attribute CSS
|
|
31
31
|
shows, never markup, and not in the bubble's `textContent`, which the page's dedupe and an annotation read (`copy.js`'s rule). CSS content can't be selected, so a click on the line copies its words.
|
|
32
32
|
The toggle has an icon and an `aria-label`, no text (`copy.js`'s rule). A jump into the source shows it (`revealInHistory`).
|
|
33
|
+
For a screen reader: the hidden `<pre>` isn't read, so the `<img>`'s alt names the toggle that shows it ("mermaid
|
|
34
|
+
diagram, its source under the Show the source button"). The natural size, a click on the picture, is also a
|
|
35
|
+
`.block-zoom` button (icon, `aria-label`, `aria-pressed`) for the keyboard: before the toggle, shown on keyboard focus
|
|
36
|
+
only, and taking no pointer while hidden, so the mouse's view is as it was.
|
|
33
37
|
6. Cache: a `Map` on `lang + theme + source`, about 200 entries, holding the `data:` URL; a later `bubble.innerHTML` wipes the
|
|
34
38
|
figure and the observer redraws in the same pass. No `crypto.subtle`: it's undefined on a plain-http LAN address. A system
|
|
35
39
|
theme change redraws through the figure (a hidden `<pre>` never intersects).
|
data/docs/internals/bridge.md
CHANGED
|
@@ -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,
|
|
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.
|
|
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.
|
|
@@ -98,11 +110,11 @@ the "asking again" and steer-cut rows, ✂ rows and questions (with how they wer
|
|
|
98
110
|
`chi guardrails replay` reads. A plain upgrade doesn't reach this: a worker already running seeded before and keeps its own version 1
|
|
99
111
|
file, and a spawn runs the newest chi installed (`SessionManager.worker_command`), unless that activation raises LoadError: the boot
|
|
100
112
|
line then falls back to the spawner's or the home's pinned version, which can be older. Otherwise it takes an older process starting
|
|
101
|
-
on a session a newer one wrote: a downgrade, a chi home whose newest chi is older (`home_older_than_spawner`), or a
|
|
113
|
+
on a session a newer one wrote: a downgrade, a chi home whose newest chi is older (`home_older_than_spawner`), or a `bin/chi`
|
|
102
114
|
from an older gem or checkout on the same state dir. Nothing on this side stops
|
|
103
115
|
it: an older chi can't be changed, and having it read the file would mean writing fake `turns` counters into every file.
|
|
104
116
|
- **A killed turn needs nothing special.** `TurnCheckpoint` keeps a running turn's prompt in the session file, with its id from
|
|
105
|
-
the start (`Worker#run_engine_turn` settles it first), so it counts as any opening. A
|
|
117
|
+
the start (`Worker#run_engine_turn` settles it first), so it counts as any opening. A `--non-interactive` one-shot keeps no running turn in the
|
|
106
118
|
session, so a kill takes the prompt along and there's nothing to count.
|
|
107
119
|
|
|
108
120
|
## SSEWriter
|
|
@@ -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`.
|
|
@@ -401,7 +414,7 @@ for client discovery, with the Bridge's secret (see above).
|
|
|
401
414
|
**Worker link.** Every client asks `WorkerLink` (`worker_link.rb`) whether a session's worker can be reached, never
|
|
402
415
|
reading `bridge.json` or probing a port itself: `WorkerLink.resolve(session_dir, unlink:, owner:)` answers a typed
|
|
403
416
|
outcome. The rule is the owner lock plus the sidecar's `pid`; there is no port probe (a port can be reused by another
|
|
404
|
-
session's Bridge, a free lock is the kernel's proof the worker died). In order: no lock holder, or a
|
|
417
|
+
session's Bridge, a free lock is the kernel's proof the worker died). In order: no lock holder, or a one-shot's (kind `"tui"`): a
|
|
405
418
|
sidecar is `Stale(:lock_free)` (removed with `unlink: true`, only if it is still the file that was read and the lock,
|
|
406
419
|
read again just before, is still free: a worker that took it meanwhile is resolved as that worker), else `None`;
|
|
407
420
|
a holder with no kind yet: `Starting`; a leaving worker: `Leaving` (marked before its Bridge stops, on every way out:
|
|
@@ -55,7 +55,7 @@ session's first counted turn. A
|
|
|
55
55
|
hook's switch (`before_turn`) sends nothing: its turn's first request counts on the new model anyway.
|
|
56
56
|
|
|
57
57
|
A process that loads the session on another model than its last turn ran on (`Engine#session=`:
|
|
58
|
-
`SessionMetrics#last_model_ref` differs from `effective_model_ref`; a `--resume`'d
|
|
58
|
+
`SessionMetrics#last_model_ref` differs from `effective_model_ref`; a `--resume`'d one-shot with `--model`, a worker woken
|
|
59
59
|
after a `/model` with no turn since) drops the old window (`forget_model_reports!`) and calls
|
|
60
60
|
`announce_switched_context!(configured_only: true)`: the configured window only
|
|
61
61
|
(`EffectiveModel#configured_context_window`, no `/props` GET or model list, since a worker runs it before its Bridge
|
data/docs/internals/engine.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Engine: the core agent loop
|
|
2
2
|
|
|
3
3
|
The Engine (`lib/samagotchi/engine.rb`) runs a session's turns. It owns the turn lifecycle, the hooks and
|
|
4
|
-
plugins, and the model↔tool cycle, and it reports what happens as events, so every UI (
|
|
5
|
-
|
|
4
|
+
plugins, and the model↔tool cycle, and it reports what happens as events, so every UI (a worker behind the web
|
|
5
|
+
and the attached TUI, a `--non-interactive` one-shot) runs turns the same way without the Engine knowing how
|
|
6
6
|
they render.
|
|
7
7
|
|
|
8
8
|
```
|
|
@@ -120,7 +120,7 @@ StandardError in run_turn:
|
|
|
120
120
|
# error_kind, retryable, host and summary.
|
|
121
121
|
→ re-raised
|
|
122
122
|
|
|
123
|
-
Interrupt (Ctrl-C;
|
|
123
|
+
Interrupt (Ctrl-C; a one-shot only: a worker traps INT, see the signal row below):
|
|
124
124
|
→ if turn.ended: re-raised only
|
|
125
125
|
→ turn.controller.cancel!(:ctrl_c)
|
|
126
126
|
→ end_turn(turn, "canceled"): ctrl_c_messages keeps the loop's conversation, else turn.messages
|
|
@@ -173,6 +173,12 @@ Stopped before the model was asked (prepare_turn returned false):
|
|
|
173
173
|
→ LLM::ModelResult(text: "", canceled: true, cancellation_reason: reason)
|
|
174
174
|
```
|
|
175
175
|
|
|
176
|
+
**Degrading.** Work that has a fallback when it fails (a prompt section left out, an empty list from the Bridge) runs
|
|
177
|
+
in `Degrade.call(area, event, fallback:, rescue:) { … }` (`degrade.rb`): same rescue class and fallback as a bare rescue,
|
|
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`, or an array of only those: `[nil, nil]`) is flagged in lib by the `Samagotchi/SilentRescue`
|
|
180
|
+
cop; a silence that is right (a client gone, a routine probe, Log's own internals) carries `# degrade-ok: why`.
|
|
181
|
+
|
|
176
182
|
## The model↔tool loop
|
|
177
183
|
|
|
178
184
|
The Engine does not talk to models directly. `Engine#backend` picks the loop the effective model's host speaks:
|
|
@@ -426,7 +432,7 @@ what a resolve gives, all without a `/model`. Frozen, so any thread may hold one
|
|
|
426
432
|
- Never resolved (they hold on a broken resolve): `effective_model_ref`, `bare_model_name`,
|
|
427
433
|
`guardrail_model_name`, `sync_model_key!`, `served_model(probe: false)` (`HostRegistry#model_ref`/`#bare_name`).
|
|
428
434
|
- Readers on other threads: the Bridge (`/state`, `/stats`, the SSE reset, a steer's cut wait), the worker's
|
|
429
|
-
command thread, the recap thread and plugin threads (`session_model_recap_target`)
|
|
435
|
+
command thread, the recap thread and plugin threads (`session_model_recap_target`).
|
|
430
436
|
- `ModelChoice` (`ModelBinding#choice`) holds the name and the typed name as one value, so a reader on another
|
|
431
437
|
thread never pairs one model's name with another's typed name.
|
|
432
438
|
- `EffectiveModel.of_session` builds a saved session's (no worker): `Web::App`, `LLMContextStrategy.for_session`.
|
|
@@ -517,7 +523,7 @@ drain hands the message to the model at a boundary, or until the turn ends (`Ste
|
|
|
517
523
|
### The turn's sink (`on_event`) and the observers
|
|
518
524
|
|
|
519
525
|
`Engine#emit_event` (`EventFanout#emit_event`, `event_fanout.rb`, which also builds the loops' stream handler and
|
|
520
|
-
emits the working set) sends each turn event to the turn's sink (`on_event`,
|
|
526
|
+
emits the working set) sends each turn event to the turn's sink (`on_event`, a one-shot's renderer) and then to the
|
|
521
527
|
persistent observers (`Engine#subscribe`), which get a copy with a monotonic `event_seq`. The observers are the
|
|
522
528
|
metrics, the debug log, the thinking tails, and in a worker the Bridge, which streams to the web and the attached
|
|
523
529
|
TUI. Errors in the sink or an observer are caught; they never break the turn.
|
|
@@ -777,7 +783,7 @@ The verdict is one of:
|
|
|
777
783
|
- **allow**: the call runs.
|
|
778
784
|
- **deny**: that call's output is replaced with denial text; the rest of the batch still runs.
|
|
779
785
|
- **ask**: `Gate#settle_ask` first looks for a stored approval that covers the call. Otherwise
|
|
780
|
-
`Engine#request_approval` asks the user through the question flow (
|
|
786
|
+
`Engine#request_approval` asks the user through the question flow (attached TUI, web). A
|
|
781
787
|
`--non-interactive` run has no one to ask, so the call is denied. An approval wider than this once is stored.
|
|
782
788
|
|
|
783
789
|
docs/guardrails.md covers the rules and the approval scopes.
|
|
@@ -870,7 +876,27 @@ Paths are under `lib/samagotchi/`.
|
|
|
870
876
|
|
|
871
877
|
| File | Role |
|
|
872
878
|
|---|---|
|
|
873
|
-
| `engine.rb` | Engine:
|
|
879
|
+
| `engine.rb` | Engine: the wiring (`initialize`, the `build_*` methods, the kernel), the turn skeleton (`run_turn`, `begin_turn`, `prepare_turn`'s ordered steps, `generate`, `end_turn`) and `shutdown`; the rest is in its parts, below |
|
|
880
|
+
| `engine/steering.rb` | `Engine::SteeringPart`: steering the running turn (or the next), the drains, the cut, the activity clock, the cancel |
|
|
881
|
+
| `engine/side_requests.rb` | `Engine::SideRequestsPart`: ctx.ask_model's and Check this's side requests, their side usage, System 1's recording |
|
|
882
|
+
| `engine/recap.rb` | `Engine::RecapPart`: the idle recap job and `/recap`, the idle scheduler's start and stop |
|
|
883
|
+
| `engine/models.rb` | `Engine::ModelsPart`: the model's names, identity and notes, `/model`'s switch, the profile, the loop it runs on |
|
|
884
|
+
| `engine/model_settings.rb` | `Engine::ModelSettingsPart`: the session's model settings (SessionModelSettings) and the snapshots (EngineSnapshots), forwarded |
|
|
885
|
+
| `engine/events.rb` | `Engine::EventsPart`: observers, `#announce`, the event lock, held announcements, load events, the emit helpers |
|
|
886
|
+
| `engine/cards.rb` | `Engine::CardsPart`: `#show_card`, anytime commands and their threads |
|
|
887
|
+
| `engine/objectives.rb` | `Engine::ObjectivesPart`: the objectives' view, proposals and audit |
|
|
888
|
+
| `engine/messages.rb` | `Engine::MessagesPart`: the session messages API (out-of-turn edits, checkpoints, the live conversation) |
|
|
889
|
+
| `engine/plugins.rb` | `Engine::PluginsPart`: loading plugins, init tasks and staged tools, `Plugin::Host` |
|
|
890
|
+
| `engine/hooks.rb` | `Engine::HooksPart`: turn-scoped hooks, the hook runtime (HookRoutes, forwarded), reminders |
|
|
891
|
+
| `engine/guardrails.rb` | `Engine::GuardrailsPart`: the guardrails' readers and the first turn's load warnings |
|
|
892
|
+
| `engine/participants.rb` | `Engine::ParticipantsPart`: the turn's author, a pair turn's stop, the participants line |
|
|
893
|
+
| `engine/session.rb` | `Engine::SessionPart`: `#session=`, the state dir, the relay's view, the system prompt and its memory names |
|
|
894
|
+
| `engine/verify.rb` | `Engine::VerifyPart`: Verify and Revise anchors at a turn's end, the verdict |
|
|
895
|
+
| `engine/questions.rb` | `Engine::QuestionsPart`: ask-user-question (QuestionDesk, forwarded) |
|
|
896
|
+
| `engine/toolkit.rb` | `Engine::ToolkitPart`: the toolkit, `ctx.load_tools`, System 1's toolkit suggestion |
|
|
897
|
+
| `engine/context_judge.rb` | `Engine::ContextJudgePart`: the attached-context candidate judge and its cards |
|
|
898
|
+
| `engine/turn_helpers.rb` | `Engine::TurnHelpersPart`: a turn's settings, images, model ref, used memories, the turn-end warm-up |
|
|
899
|
+
| `engine/turn_end.rb` | `Engine::TurnEndPart`: the normal ending and after-turn hooks, release, what an ending keeps, last_turn, the answer's display |
|
|
874
900
|
| `event_fanout.rb` | `EventFanout`: how an event reaches the turn's sink and the observers (`emit_event`), the loops' stream handler, the working set's emit and dedupe; the Engine forwards |
|
|
875
901
|
| `engine_snapshots.rb` | `EngineSnapshots`: the UIs' read model, `session_state_snapshot` (cheap, never raises) and `stats_snapshot` (may probe `/props`); the Engine forwards |
|
|
876
902
|
| `model_binding.rb` | `ModelBinding`: the model choice and the prompt profile resolved for it, under one mutex, and the overlay keys; the Engine forwards |
|
|
@@ -71,8 +71,8 @@ the payload, so an edit and a continue carry none: a new link starts with no las
|
|
|
71
71
|
counts. A leading `[done]`, `[dropped]` or `[active]` is stripped, so a text can't pass for a status. Any whitespace
|
|
72
72
|
collapses, Unicode line separators too, so each item stays one line of the frame.
|
|
73
73
|
- It never writes session.json and runs on any thread: it only adds a file to `notes/`.
|
|
74
|
-
- It is refused
|
|
75
|
-
(`NoteDelivery::
|
|
74
|
+
- It is refused while a `--non-interactive` one-shot has the session, with the reason `chi note` gives
|
|
75
|
+
(`NoteDelivery::ONE_SHOT_REFUSAL`, in `OwnerLock::TUI_BUSY`'s words).
|
|
76
76
|
- A second edit stacks on an unabsorbed first, since the current list reads the pending file.
|
|
77
77
|
- `WorkerInbound#absorb_notes` adds only the newest list in a batch and deletes the others, so edits made during a
|
|
78
78
|
turn cost one entry. It runs between turns: an idle edit lands at the worker's next pass (the fallback tick, or just
|
|
@@ -115,7 +115,8 @@ Bridge's `TurnAccumulator#current_messages`) leaves tool calls out. A move shows
|
|
|
115
115
|
## Proposals
|
|
116
116
|
|
|
117
117
|
`action: propose` (`change` add, reword, done or drop; `id`; `text`; `note`, why) checks the change against the list
|
|
118
|
-
(`Objectives::Proposal#refusal
|
|
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`, which keeps it until the turn is over,
|
|
119
120
|
as `#context_choice`'s cards are kept. `run_turn`'s ensure shows them only for a turn that completed: a canceled or
|
|
120
121
|
failed one drops them (`Engine#discard_objective_proposals`), so its Apply card never outlives a turn whose call a
|
|
121
122
|
rollback has taken out of the history, and nothing carries over to the next turn. A card is between turns (no
|
|
@@ -226,7 +227,7 @@ notes said, what tools returned, and the agent's visible answers labelled `[agen
|
|
|
226
227
|
newest 24,000 kept). Without those answers a claim about what the agent said or asked could only come back `unclear`. So the request says they settle
|
|
227
228
|
what was said or asked and are never proof of work, which needs a tool's output or a person's word. That rule is words to a model, not a mechanism: a
|
|
228
229
|
probe on Qwen3.8 Flash Next held it 3 of 3 (only the agent's "all pass": `unclear`), and a verdict is information only either way.
|
|
229
|
-
The record never holds the agent's thinking (`
|
|
230
|
+
The record never holds the agent's thinking (`AnswerText.visible`, the rule the continue note's last answer and decision-card's `/short` share: a chat entry's `thinking` and `anthropic_content` aren't read; a native
|
|
230
231
|
entry's content is the raw response, so its inline thoughts, a thought cut off unclosed, and its call markup come out), the system prompt, the list's
|
|
231
232
|
notes, a continue's carry note, an earlier session's answer that would read as outside news
|
|
232
233
|
(`Objectives.outside_note?`, the rule the audit shares), or the objectives tool's calls, since a move isn't evidence for itself.
|
|
@@ -237,7 +238,7 @@ stays inside; the request says that what's inside is data and its instructions a
|
|
|
237
238
|
the frame to hold; a model may still be swayed by text inside it, which is why a verdict is information only.
|
|
238
239
|
|
|
239
240
|
**Its host.** The model is the session's, or `objectives.check_model`, whose host may be another one. The whole user text goes through
|
|
240
|
-
`
|
|
241
|
+
`Samagotchi::Redact.text`, chi's participant redaction: known key shapes and `token=`-style values become `[redacted]`, and
|
|
241
242
|
the home folder `~`. That is by pattern, so a secret of another shape still goes.
|
|
242
243
|
|
|
243
244
|
**State.** `objective_checks.jsonl` beside the session: a `running` line, then one with `verdict` (confirmed, refuted, unclear,
|
|
@@ -5,8 +5,8 @@ chi web with a link of their own. This page is the identity layer: grants, the g
|
|
|
5
5
|
reaches, and what is written where; and their **turns**: review turns queued, read-only, limited and capped, pair
|
|
6
6
|
turns that act under the owner's guardrails; and what they **see**: their page and its reads, all through one
|
|
7
7
|
filter. Participants are experimental. An agent participant waits on its own turns' replies through the API. The
|
|
8
|
-
code and docs say "participants", "review" and "pair", never "shared":
|
|
9
|
-
|
|
8
|
+
code and docs say "participants", "review" and "pair", never "shared": that word means several UIs
|
|
9
|
+
on one worker.
|
|
10
10
|
|
|
11
11
|
Participants reach chi web on the **LAN** (`web.host: lan`), or **behind an auth proxy** with `web.public_url`
|
|
12
12
|
(experimental; docs/configuration.md, "Behind an auth proxy"): there every request needs a token, this machine's too,
|
|
@@ -18,7 +18,7 @@ works on this machine only) is the participant, never the owner.
|
|
|
18
18
|
|
|
19
19
|
## Grants
|
|
20
20
|
|
|
21
|
-
A grant (`
|
|
21
|
+
A grant (`Grants`, `lib/samagotchi/grants.rb`) binds **one random token** to **one session**, **one
|
|
22
22
|
participant** and **one role**, until it expires or is revoked.
|
|
23
23
|
|
|
24
24
|
- **Token.** `chp_` + 43 URL-safe characters (32 random bytes, as the owner's `Web::Token`). The prefix lets the gate
|
|
@@ -57,7 +57,7 @@ participant** and **one role**, until it expires or is revoked.
|
|
|
57
57
|
an invite can't keep only its own grant and lose every other digest. The owner's invite, cap, revoke and list are
|
|
58
58
|
refused with one message naming the file (`Grants::Corrupt`: `chi share` exits 1, chi web answers 500 with it);
|
|
59
59
|
an archive or delete still goes ahead. Fix the file, or move it aside to end every link.
|
|
60
|
-
- **Exposure** (`exposure`, `
|
|
60
|
+
- **Exposure** (`exposure`, `ParticipantExposure`; the review-composer plan's D3): what the participant learns
|
|
61
61
|
of the review's usage, their review data's `viewer.usage` ([below](#what-a-participant-sees)). One level today,
|
|
62
62
|
`own`, every grant's: their own spend and cap, their own queued turns and queue limit, their link's expiry, and the
|
|
63
63
|
session's model; never another participant's or the owner's usage, nor the session's. `ParticipantExposure::LEVELS`
|
|
@@ -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 (`Engine#verify_quote_in?`, `
|
|
472
|
+
also be in the answer as the pages show it (`Engine#verify_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
|
|
@@ -555,7 +555,7 @@ fields: a field added to an event later reaches nobody until it is named there.
|
|
|
555
555
|
paste, drop or pick with one line (the image button is hidden) until participant images land. The grip's floor and
|
|
556
556
|
the fold are the owner page's keys (`chi_composer_floor`, `chi_composer_collapsed`), per browser. The owner's
|
|
557
557
|
participant view mounts none: its dock stays hidden.
|
|
558
|
-
- **Redaction** (`
|
|
558
|
+
- **Redaction** (`Samagotchi::Redact`, `lib/samagotchi/redact.rb`, on every string that goes out): secret shapes (`sk-…`, `ghp_…`,
|
|
559
559
|
`github_pat_…`, `AKIA…`, `xox…-`, `chp_…`, `AIza…`, `rk_live_…`, JWTs, Slack webhooks, private key blocks, an
|
|
560
560
|
`Authorization:` header's value, any `…token` / `…api_key` / `…secret` / `…password` value, `=` or `:`, quoted or
|
|
561
561
|
not, and a URL's password), the owner's web token, the session's folder (shown as relative paths) and the home
|
|
@@ -749,7 +749,7 @@ participant TUI in v1.
|
|
|
749
749
|
participant; the status row counts who is in.
|
|
750
750
|
- **Commands.** `/share`, `/participants` (`TerminalUI::ParticipantCommands`) run `ShareCommand` for the attached
|
|
751
751
|
session through the link's `SessionAccess` (`AttachLink#session_access`: `Local`, or the remote's), so their words
|
|
752
|
-
and refusals are `chi share`'s, led by the command as typed.
|
|
752
|
+
and refusals are `chi share`'s, led by the command as typed.
|
|
753
753
|
|
|
754
754
|
## Invariants
|
|
755
755
|
|
|
@@ -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!`,
|
|
@@ -120,7 +122,7 @@ right-hand column happens.
|
|
|
120
122
|
groups changes the prompt key and asks `/toolkit` to confirm the cache cost once the session has history.
|
|
121
123
|
- A worker's `execute` and `task_create` descriptions carry the finish wake's sentence ("if you end your turn before
|
|
122
124
|
it finishes, chi tells you when it does"; `Engine.new(task_finish_notes:)`, set once as the worker builds its
|
|
123
|
-
Engine);
|
|
125
|
+
Engine); a `--non-interactive` one-shot's, and a worker's under `session.task_wakes: off`, don't. A session that moves
|
|
124
126
|
between the two, or whose worker restarts after `task_wakes` changed, misses the cache once.
|
|
125
127
|
- On remote Claude, `LLM::PromptCache.mark` sends the system message as two text parts. The stable part carries
|
|
126
128
|
breakpoint #1, so a **new session** reads it from the cache. The last non-system message carries breakpoint
|
|
@@ -69,9 +69,9 @@ command, or a flag of a supported command is missing from both lists, or when th
|
|
|
69
69
|
exist. **A new session command or flag fails that spec until you choose one.**
|
|
70
70
|
|
|
71
71
|
`Contract::RUN` does the same for bin/chi's run flags (`RunOptions.switches`): `SUPPORTED` (`--attach`, `--resume`,
|
|
72
|
-
`-p`, `--
|
|
72
|
+
`-p`, `--model`, `--thinking`, `--llm-context*`, `--toolkit`, `--tool`, `--no-tool`, `--set`,
|
|
73
73
|
`--parent-answers`, `--no-interrupt`, `--no-default-input`, `--dir`, `--help`, `--version`) or `LOCAL_ONLY`
|
|
74
|
-
(`--
|
|
74
|
+
(`--non-interactive`: one turn in this process, P4-D4; `--verbose`: this machine's debug log; `--memory`, `--mute`,
|
|
75
75
|
`--append-system-prompt`: this machine's memories and files, P4-D2/D3; `chi web`'s flags; `--profile`). A config
|
|
76
76
|
registry flag (`--recap-base-url` …) is this machine's config: `LOCAL_ONLY` without being listed
|
|
77
77
|
(`Contract.run_status`). The contract spec fails on a run flag in neither.
|
|
@@ -152,7 +152,7 @@ the session commands*. Keep that table in step with `SessionAccessRoutes`.
|
|
|
152
152
|
- **A remote attach.** Before the loop runs (resolve, create, wake, the feature check) every failure is
|
|
153
153
|
`AttachLauncher::Error` or the command's own line, exit 1 (`BadRequest`: exit 2): the wake's 409 `owned_by_tui`
|
|
154
154
|
is `OwnedByTUI` (its message is the one `chi --attach` prints here too: `AttachLauncher.connect` now uses it), its
|
|
155
|
-
409 `scratch` and 504 `worker_timeout` are `Refused` with `
|
|
155
|
+
409 `scratch` and 504 `worker_timeout` are `Refused` with `Session::SCRATCH_REFUSED` / `BRIDGE_LATE`'s words
|
|
156
156
|
(`Remote#attach_wake`). Once the loop runs, `WebBridgeClient` turns every `RemoteFailed` into `Unreachable`
|
|
157
157
|
(an `IOError`), so the loop's rescues catch it and `AttachedLoop#worker_down` prints its message as is ("could not
|
|
158
158
|
send the prompt (remote studio (http://…): the token was refused …)"); the stream's end is worded by its reason
|
|
@@ -190,7 +190,7 @@ with a "Machine-local" comment; the rest branch on `@access.remote?` (`SendComma
|
|
|
190
190
|
| `context`'s default session (`SAMAGOTCHI_PARENT_SESSION`, `ContextCommand#inside_session`) | client | no default: ids are required (`added_by` still reads it) |
|
|
191
191
|
| A context URL's provider (`ContextProviders.resolve`) | client's bundles | server's bundles (`Remote#context_add_url`) |
|
|
192
192
|
| UI-only slash commands (`SessionCommands.ui_only_entry`), `--image` ingest, note text (`SessionInbox.checked_text`), push text (`ContextSources.parse_output`) | client | client too (the server checks note and push text again) |
|
|
193
|
-
| A remote attach's `--attach`/`--resume` id (`Session.resolve_id`), the leftover scratch check, the
|
|
193
|
+
| A remote attach's `--attach`/`--resume` id (`Session.resolve_id`), the leftover scratch check, the one-turn run's refusal (`OwnerLock::TUI_BUSY`) | client (bin/chi, `AttachLauncher.connect`) | server: `Remote#resolve`, then `POST …/wake`'s 409s |
|
|
194
194
|
| A new attached session's `--toolkit`, `--set`, `--thinking`, `--llm-context*`, `--parent-answers`, `--model` | client (bin/chi) | server: `POST /api/sessions` with `idle: true` and `SetupWords`; its warnings printed as `send --new` prints them |
|
|
195
195
|
| The status row's default model, the newest chi for the version line, what a parent may pick (exit 3) | client | server: the wake's `default_model`, `installed`, `parent_strategy` (the last read again from `GET …/worker` at report time) |
|
|
196
196
|
| `@path` images (ingest, convert, downscale) | client, into the session's `images/` | client, into a scratch dir, then uploaded (`POST …/images`) |
|
|
@@ -213,7 +213,7 @@ with a "Machine-local" comment; the rest branch on `@access.remote?` (`SendComma
|
|
|
213
213
|
(`App#principal(req)`), so a handler reads the principal and never trusts the body for it. Each `ROUTES` row names
|
|
214
214
|
who may use it (its permission column, `:owner` or `:participant`; `Principal#permits?`; any other answers 403
|
|
215
215
|
`not_permitted`). The owner passes every route (`local_viewer?` only hides `editor_url`). A participant, a
|
|
216
|
-
`
|
|
216
|
+
`Grants` token for one session, reaches only the `:participant` rows, and another session's id is a 404
|
|
217
217
|
([participants.md](participants.md)). A turn's author comes from the principal
|
|
218
218
|
(`Principal#author`, nil for the owner) and rides with the turn (`SessionManager.deliver_turn(author:)` →
|
|
219
219
|
`BridgeClient#post_turn` → `Bridge#handle_post_turn` → the input file, the fallback's too → the turn's `origin`);
|
|
@@ -297,7 +297,7 @@ bin/chi ── RunOptions.parse ── a remote, run flags ──> RemoteAttachC
|
|
|
297
297
|
**chi web's routes** (tabled in [architecture.md](../architecture.md)): `POST …/wake` (`Local#wake`: the live worker
|
|
298
298
|
as is, else `SessionManager.resume_session` and `WorkerLink.await`, at most `WAKE_MAX_WAIT` 10 s, the attach's
|
|
299
299
|
`BRIDGE_WAIT` here, after waiting for a worker still leaving to let go of its lock, `OwnerLock.await_release`: its
|
|
300
|
-
recap on leave's timeout plus 5 s; a leftover scratch session is refused in `
|
|
300
|
+
recap on leave's timeout plus 5 s; a leftover scratch session is refused in `Session::SCRATCH_REFUSED`'s words, a late Bridge
|
|
301
301
|
in `LaunchMode::BRIDGE_LATE`'s; a worker on a newer sidecar protocol is refused, 409 `worker_newer`, never resumed over) and `GET …/worker` (`Local#worker`: live, the stop marker, the owner, `leaving` and `leave_seconds` when its worker
|
|
302
302
|
is leaving, `WorkerState#leaving?`, `stuck` for a worker that never published its Bridge, `skew` for a newer protocol; it never starts one; it also names that machine's `default_model`), both answering the worker as `{epoch, started_at, version,
|
|
303
303
|
features}`; `POST …/exit` (the worker's exit, `client_id` passed on only when it is a `tui:` one), `POST …/recap`,
|
|
@@ -436,7 +436,7 @@ the closing line reads `stopped` as `unreachable`, the words it always had.
|
|
|
436
436
|
writes the stream's GET (host, port, path, extra headers) and reads its headers, for a stream through chi web too.
|
|
437
437
|
- `RunOptions.parse(argv)` is bin/chi's one run-flag parser (`RunOptions.switches` lists its flags), so a remote
|
|
438
438
|
branch can take the parsed options before bin/chi's local checks; `LaunchMode.attached_conflict` and
|
|
439
|
-
`
|
|
439
|
+
`Session::SCRATCH_REFUSED` / `BRIDGE_LATE` are the words both would print.
|
|
440
440
|
|
|
441
441
|
An https chi web (a pod behind a proxy) needs TLS and chunked decoding in `StreamRequest`.
|
|
442
442
|
- **Shared sessions (guests, roles).** The only access point is `App#token_gate`: one token, the owner's. Roles would
|
|
@@ -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/internals/web-push.md
CHANGED
|
@@ -75,7 +75,7 @@ dropped that way, and an unknown id is ignored.
|
|
|
75
75
|
That is how a subscription iOS revoked silently (Apple keeps answering 201, so `:gone` never comes) leaves the store:
|
|
76
76
|
the user turns the bell off and on, the page has no live subscription to delete, and the new one replaces it. Every write is a read-change-write under
|
|
77
77
|
`store.lock` (chi web's request threads and the sender thread both write). A `subscriptions.json` that doesn't read
|
|
78
|
-
lists no device and is never written over. `Store#check_permissions!` uses `
|
|
78
|
+
lists no device and is never written over. `Store#check_permissions!` uses `PrivateFiles.check!`, the check the
|
|
79
79
|
grants use, worded with what the folder holds; `Server.launch` runs it (`Push.preflight!`, with the contact and the
|
|
80
80
|
key) before any tunnel goes up, and a refusal is one `Error:` line, exit 1.
|
|
81
81
|
|