samagotchi 0.152.0 → 0.154.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 +45 -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/bundle-authoring.md +1 -1
- data/docs/bundles.md +93 -12
- data/docs/cli.md +83 -99
- data/docs/configuration.md +46 -40
- data/docs/context.md +3 -3
- data/docs/desktop.md +1 -1
- data/docs/guardrails.md +15 -15
- data/docs/hooks.md +13 -12
- data/docs/internals/anthropic.md +3 -3
- data/docs/internals/attached-context.md +5 -2
- data/docs/internals/background-tasks.md +3 -4
- data/docs/internals/block-renderers.md +4 -0
- data/docs/internals/bridge.md +3 -3
- data/docs/internals/context-telemetry.md +1 -1
- data/docs/internals/engine.md +42 -12
- data/docs/internals/objectives.md +6 -5
- data/docs/internals/participants.md +8 -8
- data/docs/internals/prompt-caching.md +8 -4
- data/docs/internals/remote-sessions.md +7 -7
- data/docs/internals/web-frontend.md +20 -17
- data/docs/internals/web-push.md +1 -1
- data/docs/memory.md +2 -2
- data/docs/plugins.md +52 -43
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +25 -25
- data/docs/sub-agent.md +2 -2
- data/docs/system-one.md +1 -1
- data/docs/testing.md +18 -8
- data/lib/samagotchi/answer_text.rb +34 -0
- data/lib/samagotchi/bridge.rb +43 -32
- data/lib/samagotchi/bridge_client.rb +10 -9
- 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.rb +2 -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/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/bundles/web-search/manifest.yml +13 -0
- data/lib/samagotchi/bundles/web-search/plugin.rb +174 -0
- 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 +2 -2
- 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 +63 -2642
- 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/guardrails/review_boundary.rb +4 -3
- data/lib/samagotchi/hook_routes.rb +3 -3
- data/lib/samagotchi/idle_recap.rb +4 -5
- data/lib/samagotchi/idle_reminders.rb +3 -4
- data/lib/samagotchi/iteration_limit.rb +2 -2
- data/lib/samagotchi/kernel_loop.rb +10 -11
- data/lib/samagotchi/launch_mode.rb +13 -32
- data/lib/samagotchi/level_setting_command.rb +3 -2
- data/lib/samagotchi/llm_context_command.rb +3 -2
- data/lib/samagotchi/llm_context_stale.rb +2 -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/message_parts.rb +3 -2
- 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/pending_input_queue.rb +2 -2
- 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 +59 -36
- 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 +3 -3
- data/lib/samagotchi/sessions_command.rb +4 -4
- data/lib/samagotchi/share_command.rb +10 -10
- data/lib/samagotchi/steer.rb +1 -1
- data/lib/samagotchi/system_prompt.rb +102 -109
- 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 +2 -3
- 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 +1 -7
- 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 +3 -3
- data/lib/samagotchi/terminal_ui/status_row.rb +7 -8
- data/lib/samagotchi/terminal_ui.rb +142 -1387
- data/lib/samagotchi/title_command.rb +2 -2
- data/lib/samagotchi/tool_runner.rb +14 -16
- data/lib/samagotchi/toolkit.rb +8 -5
- data/lib/samagotchi/toolkit_binding.rb +30 -5
- data/lib/samagotchi/toolkit_command.rb +2 -1
- data/lib/samagotchi/toolkit_report_command.rb +9 -5
- data/lib/samagotchi/toolkit_suggest.rb +12 -6
- data/lib/samagotchi/tools/builtin_calls.rb +14 -10
- data/lib/samagotchi/tools/deferred_tools.rb +2 -2
- 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/registry.rb +44 -20
- data/lib/samagotchi/tools/send_note.rb +2 -1
- data/lib/samagotchi/tools/tool_index.rb +27 -21
- data/lib/samagotchi/turn_endings.rb +2 -2
- data/lib/samagotchi/turn_flow.rb +4 -5
- data/lib/samagotchi/update_hint.rb +2 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +39 -50
- 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/app.js +61 -357
- 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/chains.js +2 -2
- data/lib/samagotchi/web/public/claim_link.js +4 -4
- data/lib/samagotchi/web/public/composer_mode.js +110 -0
- data/lib/samagotchi/web/public/format.js +3 -3
- data/lib/samagotchi/web/public/owner_claim.js +81 -0
- data/lib/samagotchi/web/public/owner_verify.js +112 -0
- data/lib/samagotchi/web/public/session_fields.js +3 -3
- data/lib/samagotchi/web/public/strip_grip.js +3 -3
- data/lib/samagotchi/web/public/strip_hidden.js +65 -0
- data/lib/samagotchi/web/public/version_notices.js +132 -0
- data/lib/samagotchi/web/push/store.rb +1 -1
- data/lib/samagotchi/web/session_access_routes.rb +5 -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 +2 -2
- data/lib/samagotchi/web/session_summary.rb +1 -1
- data/lib/samagotchi/worker.rb +106 -9
- data/lib/samagotchi/worker_link.rb +3 -3
- data/lib/samagotchi/working_set_command.rb +3 -2
- metadata +36 -6
- 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/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
|
```
|
|
@@ -57,7 +57,8 @@ Engine#run_turn(session, prompt, on_event:, max_iterations:, cancel_controller:,
|
|
|
57
57
|
│ # local System 1 host they wait for the turn's end: started before warm_up_next_turn, whose warm-up
|
|
58
58
|
│ # waits for them, or from run_turn's ensure; docs/context.md, The judge);
|
|
59
59
|
│ # then the session's toolkit (apply_toolkit!: Registry#toolkit, ToolIndex#refresh! for find_tools'
|
|
60
|
-
│ # chi line, a prompt rebuild when it changed
|
|
60
|
+
│ # chi line, a prompt rebuild when it changed; a warn notice per plugin tool that arrived mid-session
|
|
61
|
+
│ # with nothing to reach it, Registry#stranded_names). A Stop during the probes or the waits returns false
|
|
61
62
|
│ # (the ask's line says canceled; an ensure marks one the turn never settled failed).
|
|
62
63
|
│ # fires the :session_start hook on the Engine's first turn, then the :before_turn hook
|
|
63
64
|
│ # (a frozen copy of the history, the prompt, and resumed)
|
|
@@ -119,7 +120,7 @@ StandardError in run_turn:
|
|
|
119
120
|
# error_kind, retryable, host and summary.
|
|
120
121
|
→ re-raised
|
|
121
122
|
|
|
122
|
-
Interrupt (Ctrl-C;
|
|
123
|
+
Interrupt (Ctrl-C; a one-shot only: a worker traps INT, see the signal row below):
|
|
123
124
|
→ if turn.ended: re-raised only
|
|
124
125
|
→ turn.controller.cancel!(:ctrl_c)
|
|
125
126
|
→ end_turn(turn, "canceled"): ctrl_c_messages keeps the loop's conversation, else turn.messages
|
|
@@ -172,6 +173,12 @@ Stopped before the model was asked (prepare_turn returned false):
|
|
|
172
173
|
→ LLM::ModelResult(text: "", canceled: true, cancellation_reason: reason)
|
|
173
174
|
```
|
|
174
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`) 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
|
+
|
|
175
182
|
## The model↔tool loop
|
|
176
183
|
|
|
177
184
|
The Engine does not talk to models directly. `Engine#backend` picks the loop the effective model's host speaks:
|
|
@@ -298,12 +305,13 @@ One iteration with tool calls:
|
|
|
298
305
|
on_stream_event:, max_tool_output_chars:)
|
|
299
306
|
→ under TurnSettings#loads_tools, a direct call of a source tool's acts_as (mcp_fs_read) is wrapped
|
|
300
307
|
into the call_tool call it stands for (Registry::Index#wrap, ToolIndex#wrap)
|
|
301
|
-
→ first, the call's entry unwraps it (Registry::Entry#unwrap: a call_tool of chi/<built-in
|
|
302
|
-
that
|
|
308
|
+
→ first, the call's entry unwraps it (Registry::Entry#unwrap: a call_tool of chi/<name>, a built-in or
|
|
309
|
+
a plugin tool, becomes that tool's call with via: "call_tool", ToolIndex#unwrap); the rest sees the
|
|
310
|
+
real call
|
|
303
311
|
→ a tool the session's toolkit leaves out (Registry#left_out; with via:, one Registry#indexed doesn't
|
|
304
312
|
reach) skips the gate: no hooks, no card; KernelLoop#dispatch answers it (the call_tool route for a
|
|
305
313
|
reachable tool, the not-loaded error for a removed one). Under TurnSettings#loads_tools a reachable
|
|
306
|
-
built-in called by its own name is no such call: it runs through the gate, and KernelLoop#dispatch
|
|
314
|
+
built-in or plugin tool called by its own name is no such call: it runs through the gate, and KernelLoop#dispatch
|
|
307
315
|
takes it (#loaded_entry)
|
|
308
316
|
→ Guardrails::Gate#evaluate (the before_tool_call hooks, then the core checks)
|
|
309
317
|
→ :tool_call_started
|
|
@@ -424,7 +432,7 @@ what a resolve gives, all without a `/model`. Frozen, so any thread may hold one
|
|
|
424
432
|
- Never resolved (they hold on a broken resolve): `effective_model_ref`, `bare_model_name`,
|
|
425
433
|
`guardrail_model_name`, `sync_model_key!`, `served_model(probe: false)` (`HostRegistry#model_ref`/`#bare_name`).
|
|
426
434
|
- Readers on other threads: the Bridge (`/state`, `/stats`, the SSE reset, a steer's cut wait), the worker's
|
|
427
|
-
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`).
|
|
428
436
|
- `ModelChoice` (`ModelBinding#choice`) holds the name and the typed name as one value, so a reader on another
|
|
429
437
|
thread never pairs one model's name with another's typed name.
|
|
430
438
|
- `EffectiveModel.of_session` builds a saved session's (no worker): `Web::App`, `LLMContextStrategy.for_session`.
|
|
@@ -515,7 +523,7 @@ drain hands the message to the model at a boundary, or until the turn ends (`Ste
|
|
|
515
523
|
### The turn's sink (`on_event`) and the observers
|
|
516
524
|
|
|
517
525
|
`Engine#emit_event` (`EventFanout#emit_event`, `event_fanout.rb`, which also builds the loops' stream handler and
|
|
518
|
-
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
|
|
519
527
|
persistent observers (`Engine#subscribe`), which get a copy with a monotonic `event_seq`. The observers are the
|
|
520
528
|
metrics, the debug log, the thinking tails, and in a worker the Bridge, which streams to the web and the attached
|
|
521
529
|
TUI. Errors in the sink or an observer are caught; they never break the turn.
|
|
@@ -644,7 +652,9 @@ Engine initialization (in this order, among other things):
|
|
|
644
652
|
the bundles' plugins load (load_plugins → ExtensionLoad#load_plugins; each plugin's ctx is a
|
|
645
653
|
Plugin::Context over plugin_host, whose ctx.sessions it builds itself: the test kit passes its own, sessions:),
|
|
646
654
|
then (plugins or not) @tool_index.declare(@tools): find_tools + call_tool (source "index", not core),
|
|
647
|
-
declared per toolkit by Registry#declared? (a source registered at load, or a
|
|
655
|
+
declared per toolkit by Registry#declared? (a source registered at load, or a tool registered then that
|
|
656
|
+
the toolkit leaves reachable: Registry::Index#loaded; Toolkit::Resolved#declares? treats built-ins and
|
|
657
|
+
plugin tools alike: full declares both, another kit its list and adds) and
|
|
648
658
|
sets call_tool's unwrap (Tools::ToolIndex: the chi group from Registry#reachable_names, the sources'
|
|
649
659
|
groups, search, call checks, the unwrap; a source runs the call with its own plugin Context)
|
|
650
660
|
@reminder_store = ReminderStore.new (the kernel gets it), @reminder_queue, @reminders
|
|
@@ -773,7 +783,7 @@ The verdict is one of:
|
|
|
773
783
|
- **allow**: the call runs.
|
|
774
784
|
- **deny**: that call's output is replaced with denial text; the rest of the batch still runs.
|
|
775
785
|
- **ask**: `Gate#settle_ask` first looks for a stored approval that covers the call. Otherwise
|
|
776
|
-
`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
|
|
777
787
|
`--non-interactive` run has no one to ask, so the call is denied. An approval wider than this once is stored.
|
|
778
788
|
|
|
779
789
|
docs/guardrails.md covers the rules and the approval scopes.
|
|
@@ -866,7 +876,27 @@ Paths are under `lib/samagotchi/`.
|
|
|
866
876
|
|
|
867
877
|
| File | Role |
|
|
868
878
|
|---|---|
|
|
869
|
-
| `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 |
|
|
870
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 |
|
|
871
901
|
| `engine_snapshots.rb` | `EngineSnapshots`: the UIs' read model, `session_state_snapshot` (cheap, never raises) and `stats_snapshot` (may probe `/props`); the Engine forwards |
|
|
872
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,9 +469,9 @@ 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
|
-
page quotes it the same way (`
|
|
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
|
|
476
476
|
checks the same anchor against its parent's history. A completed Verify reply whose final non-empty line has
|
|
477
477
|
a valid `Verdict: confirmed|refuted|unclear — <reason>` appends a verdict to the session. Both pages place its card
|
|
@@ -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
|
|
|
@@ -108,15 +108,19 @@ right-hand column happens.
|
|
|
108
108
|
Claude 5.x cost the preserved thinking each time).
|
|
109
109
|
- find_tools' description is a function of the toolkit and the source groups **at load**: `ToolIndex#declare`
|
|
110
110
|
snapshots source group names and description lines (name, summary, callable count). `ToolIndex#refresh!` (from
|
|
111
|
-
`ToolkitBinding#apply!`
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
`ToolkitBinding#apply!` at a toolkit change, and from `Engine#apply_staged_tools!` when a plugin's tool set
|
|
112
|
+
changed) filters that snapshot by the toolkit and rebuilds the `chi` line from the built-ins and plugin tools it
|
|
113
|
+
reaches; it never consults live group names. The same group filter drives searches and deferred MCP tools.
|
|
114
|
+
Whether `find_tools`/`call_tool` are declared (`Registry#index_needed?`) reads the groups and the entries
|
|
115
|
+
registered at load (`Registry::Index#loaded`), never later ones: a plugin tool `chi.replace_tools` adds doesn't
|
|
116
|
+
bring them in, and a loaded plugin tool it later removes still counts, so `find_tools` stays declared, perhaps
|
|
117
|
+
with an empty `chi` group. That is deliberate: the declared tools stay the same for the worker's life. An MCP server that finishes booting
|
|
114
118
|
between load and the first turn changes search answers only, so two sessions on the same toolkit with the same
|
|
115
119
|
MCP cache send the same bytes. Kits that omit `groups:` keep their previous request bytes; switching only
|
|
116
120
|
groups changes the prompt key and asks `/toolkit` to confirm the cache cost once the session has history.
|
|
117
121
|
- A worker's `execute` and `task_create` descriptions carry the finish wake's sentence ("if you end your turn before
|
|
118
122
|
it finishes, chi tells you when it does"; `Engine.new(task_finish_notes:)`, set once as the worker builds its
|
|
119
|
-
Engine);
|
|
123
|
+
Engine); a `--non-interactive` one-shot's, and a worker's under `session.task_wakes: off`, don't. A session that moves
|
|
120
124
|
between the two, or whose worker restarts after `task_wakes` changed, misses the cache once.
|
|
121
125
|
- On remote Claude, `LLM::PromptCache.mark` sends the system message as two text parts. The stable part carries
|
|
122
126
|
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
|
|
@@ -10,8 +10,9 @@ only library. Pure modules (`*_model.js`, `session_state.js`, `sessions_list.js`
|
|
|
10
10
|
and have `node --test` specs under `spec/web/public/` (happy-dom for the DOM ones); the pages as a whole are covered
|
|
11
11
|
by `npm run e2e` (`spec/e2e/`). `app.js` has no unit spec.
|
|
12
12
|
|
|
13
|
-
**`app.js` has a size cap**: `npm run lint` fails past `max-lines` in `eslint.config.js` (counted as `wc -l` counts
|
|
14
|
-
|
|
13
|
+
**`app.js` has a size cap**: `npm run lint` fails past `max-lines` in `eslint.config.js` (counted as `wc -l` counts:
|
|
14
|
+
2000, ratcheted down from 2250 when the versions, the owner's claim links and Verify, the composer's mode and the
|
|
15
|
+
strip's hide went into modules of their own, which left room for a few features' wiring). A new feature goes into a module of its own, wired from `app.js` in a few lines; a
|
|
15
16
|
refactor that shrinks `app.js` ratchets the cap down with it. Never raise it: extract instead.
|
|
16
17
|
|
|
17
18
|
**New exports go in new modules.** The static JS is served at unversioned URLs. Today every load revalidates
|
|
@@ -23,7 +24,7 @@ page. A factory dep a new version stops passing is the same hazard the other way
|
|
|
23
24
|
out of `send_turn.js`, `session_actions.js` and `lists_view.js`); a page loaded before keeps all its old modules
|
|
24
25
|
until a reload, so the risk is a mixed cache, not an open tab. `answer_fold.js`, `details_motion.js`,
|
|
25
26
|
`turn_range.js`, `history_reveal.js`, `latest.js`, `session_payload.js`, `method_api.js`, `session_store.js`,
|
|
26
|
-
`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`, `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` 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
28
|
|
|
28
29
|
## Two pages, two models
|
|
29
30
|
|
|
@@ -37,7 +38,7 @@ claim links, `composer_mount.js`, the verdict card and Verify fresh, `verify_tar
|
|
|
37
38
|
`auth_lost.js`, `token_fragment.js`, `format.js`) but none of the data layer, the stream handling or the message
|
|
38
39
|
rendering. It has its own `api` (`{status, ok, body}`, never throws, its own ended / auth-lost handling) and its own
|
|
39
40
|
`EventSource`. Its parallel copies of the owner's Verify and claim helpers (`verifyFreshState`, `verifyAnchorFor`,
|
|
40
|
-
`showPendingClaim`, `CLAIM_ROOTS`) differ on purpose in places (the source kind `reply` vs `answer`, the server's
|
|
41
|
+
`showPendingClaim`, `CLAIM_ROOTS`; the owner's are `owner_verify.js` and `owner_claim.js`) differ on purpose in places (the source kind `reply` vs `answer`, the server's
|
|
41
42
|
`fresh_running` vs `verify_fresh`, no home/cwd in its claim options). Don't converge them as a side effect of another
|
|
42
43
|
change.
|
|
43
44
|
|
|
@@ -46,8 +47,8 @@ change.
|
|
|
46
47
|
| Holder | What | Who writes | How a change reaches the screen |
|
|
47
48
|
|---|---|---|---|
|
|
48
49
|
| **The session store** (`session_store.js`; app.js `store`) | `store.selected` (the open session's id), `store.current` and `store.sessions` (below) | through the store only (see The session store); other modules get getters (`selected: () => store.selected`, `state: () => store.current`) or the store itself (`stream_handlers.js`, `send_turn.js`, `session_actions.js`) | the store runs the redraws that read what changed (below); nobody calls them by hand |
|
|
49
|
-
| **app.js module scope**: about 20 top-level `let`s (`streamControl`, `eventsControl`, `pageCapabilities`/`pageEditorUrl`, `
|
|
50
|
-
| **`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`); `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`) |
|
|
50
|
+
| **app.js module scope**: about 20 top-level `let`s (`streamControl`, `eventsControl`, `pageCapabilities`/`pageEditorUrl`, `resyncsInFlight`/`resyncAgain`, `resyncAttempts`, `scopeError`…) | the page's mode and its streams | app.js only; other modules get getters and hooks | the writer touches the redraw that reads it (`store.touch("infoBar")`: the stream; `version_notices.js` does it for the versions and its `restarting` set), or calls a redraw only it feeds |
|
|
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`) |
|
|
51
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` |
|
|
52
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 |
|
|
53
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) |
|
|
@@ -73,8 +74,9 @@ through the store**, never `current.X = …`:
|
|
|
73
74
|
old one writes there alone.
|
|
74
75
|
- `store.batch(fn)`, `store.touch(name)`, `store.on(name, fn)`: the redraws (below).
|
|
75
76
|
|
|
76
|
-
The ESLint block `samagotchi/session-writes` (eslint.config.js) enforces it in the
|
|
77
|
-
(app.js, `stream_handlers.js`, `send_turn.js`, `session_actions.js`, `start_page.js`, `working_set_view.js
|
|
77
|
+
The ESLint block `samagotchi/session-writes` (eslint.config.js) enforces it in the seven modules that hold the state
|
|
78
|
+
(app.js, `stream_handlers.js`, `send_turn.js`, `session_actions.js`, `start_page.js`, `working_set_view.js`,
|
|
79
|
+
`owner_verify.js`): an
|
|
78
80
|
assignment, `++`, `delete` or `Object.assign` into `current`, `store.current` or `state()` is an error
|
|
79
81
|
(`spec/web/public/lint_session_writes.test.js` runs it on a sample). An alias (`const st = state(); st.x = …`) isn't
|
|
80
82
|
caught: don't write through one.
|
|
@@ -105,16 +107,16 @@ stale).
|
|
|
105
107
|
**The redraws are registered, not called.** app.js registers each name once (`store.on`, at the bottom, once every
|
|
106
108
|
piece a redraw calls exists): `lists` → `lists.render()`, `paintActive` → `lists.paintActive()`, `settingsPanel` →
|
|
107
109
|
`settingsControl.refresh()`, `workingSetStrategy` → `workingSet.strategyChanged()`, `featureGates` (below), `composer` →
|
|
108
|
-
`
|
|
110
|
+
`composerMode.update()` (`composer_mode.js`), `infoBar` → `updateInfoBar()`. A writer writes and stops there: no `updateInfoBar()` after
|
|
109
111
|
a store write. A value written unchanged redraws nothing (a chunk with the same ctx %).
|
|
110
112
|
|
|
111
113
|
An input the store doesn't hold calls `store.touch(name)` where it changes: `infoBar` for the clock (liveTurn's
|
|
112
|
-
`onTick`), the versions (`
|
|
114
|
+
`onTick`), the versions and the sessions restarting (`version_notices.js` `notice`, `restart`), the stream (`startStream`, `workerGone`), the working
|
|
113
115
|
set's and the participants' `onChange`, the continue and review in-flight sets
|
|
114
116
|
(`session_actions.js`); `composer` and `infoBar` for `liveTurn.running` (`setTurnRunning`, with `settingsPanel`:
|
|
115
117
|
the open ⚙ panel's running-turn note) and at the end of `renderSessionView` (a whole view drawn). Left as they are,
|
|
116
|
-
on purpose:
|
|
117
|
-
`
|
|
118
|
+
on purpose: `composer_mode.js`'s phone-width listener calls its `update()` (the bar doesn't read it),
|
|
119
|
+
`strip_hidden.js`'s apply and `applyView` call `updateSessionTitle()`, and `closeStream`/`stream.restart` set
|
|
118
120
|
`streamControl = null` without a redraw (a resync or a workerGone follows).
|
|
119
121
|
|
|
120
122
|
A function that writes more than once batches its synchronous part, so each redraw runs once: `select` (before its
|
|
@@ -178,7 +180,7 @@ The redraws, by their store names:
|
|
|
178
180
|
- `infoBar` (`updateInfoBar()`): the session bar's buttons and info line (`info_bar_model.js sessionBar` →
|
|
179
181
|
`info_bar.js`), the annotate bar, the fold controls, `updateSessionTitle()` and the fresh link's picker. Cheap: it
|
|
180
182
|
runs on most writes and every clock tick.
|
|
181
|
-
- `composer` (`
|
|
183
|
+
- `composer` (`composer_mode.js` `update()`): the composer's placeholder, buttons and delivery line (selected, running,
|
|
182
184
|
owner).
|
|
183
185
|
- `settingsPanel` (`settingsControl.refresh()`): the ⚙ chip's **open panel** (`settings_chip.js`; the chip itself is
|
|
184
186
|
drawn by the bar), on `thinking`, `reasoningHistory` and `llmContext`; `workingSetStrategy`: the working set's open
|
|
@@ -190,7 +192,8 @@ The redraws, by their store names:
|
|
|
190
192
|
`forgetSession()` is the one reset: `select` and `clearSelection` both call it, so anything per-session that isn't
|
|
191
193
|
in `store.current` (capabilities, the markdown warning and tip, the init row, the Verify claim, the armed Revise,
|
|
192
194
|
the running turn's DOM) is cleared there, and the feature gates are primed for the empty session. A new per-session holder
|
|
193
|
-
(an armed composer anchor like the Verify claim or the Revise) goes into it. The armed
|
|
195
|
+
(an armed composer anchor like the Verify claim or the Revise) goes into it. The armed Verify claim is
|
|
196
|
+
`owner_verify.js`'s holder (`anchor()`, `set(a)`, `clear(a?)`: `clear(a)` lets go only of `a`). The armed Revise
|
|
194
197
|
(`pendingReviseAnchor`) is set only through `setReviseAnchor`, which redraws its chip and the Revise buttons (a
|
|
195
198
|
redraw only it feeds, not a store field).
|
|
196
199
|
|
|
@@ -222,7 +225,7 @@ each keeps one set of handlers, confirmations and in-flight rules. A chip the sh
|
|
|
222
225
|
matters at exactly 600 px, where the CSS is a phone's but the shell (`SHEET_MAX`, `<`) still draws a popover, not a sheet.
|
|
223
226
|
A new chip on the bar needs its place in that CSS's hidden list and a row in `moreRows`, and its attention state in the row, or
|
|
224
227
|
it is lost on a phone. `app.js`'s `updateInfoBar` feeds the sheet (`update`) on every redraw; its rows and actions draw into
|
|
225
|
-
fixed slots, so a row that joins keeps the focus where it was.
|
|
228
|
+
fixed slots, so a row that joins keeps the focus where it was. `composer_mode.js`'s phone-width listener closes the sheet when the window grows.
|
|
226
229
|
|
|
227
230
|
|
|
228
231
|
## The stage's height
|
|
@@ -321,11 +324,11 @@ reach another device or the server.
|
|
|
321
324
|
| `chi_model`, `chi_model_recent` | `model_pick.js`, `model_picker.js`, `start_page.js` | the start page's model and its recent list |
|
|
322
325
|
| `chi_composer_collapsed` | `composer_fold.js` | the composer folded (a card's text field folds it for the time it has the focus on a narrow or touch screen, `composer_grip.js watchCardField`, on top of this and of the visit's state: neither is written) |
|
|
323
326
|
| `chi_composer_floor` | `composer_grip.js`, `composer_mount.js` | the composer's dragged height |
|
|
324
|
-
| `chi_strip_hidden` (`chi_zen`: the old name, read once) |
|
|
327
|
+
| `chi_strip_hidden` (`chi_zen`: the old name, read once) | `strip_hidden.js` | the session strip hidden |
|
|
325
328
|
| `chi_stage_collapsed` | `stage_view.js` | the stage collapsed |
|
|
326
329
|
| `chi_working_set_collapsed` | `working_set_rail.js` | the working set rail collapsed |
|
|
327
330
|
| `chi_markdown_tip_dismissed` | `markdown_tip.js` | the markdown tip dismissed |
|
|
328
|
-
| `chi_update_available_shown` |
|
|
331
|
+
| `chi_update_available_shown` | `version_notices.js` | update notices already shown |
|
|
329
332
|
| `chi_notify`, `chi_push_device` | `notifications.js` (an injected storage) | the bell's state, this device's push id |
|
|
330
333
|
| `chi_answer_fold:<session>` | `answer_fold.js`, `revise_stack.js` | the answers and long prompts (`<turn id>:prompt`) folded by hand, and the Revise stubs unfolded by hand (`stub:<turn id>`) (swept with the session list) |
|
|
331
334
|
| `chi_review_history:<grant>`, `chi_review_history_session:<session>` | `review_history.js` (participant page) | a participant's composer history |
|
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
|
|
data/docs/memory.md
CHANGED
|
@@ -33,8 +33,8 @@ dir has a folder of the same basename with another hash, chi says so, naming bot
|
|
|
33
33
|
`mv` that fixes it (a name match can't prove it's the same project, so nothing is renamed for you):
|
|
34
34
|
|
|
35
35
|
- at session start, a `memory>` load warning (a `:guardrail_warning` with label `memory`; the attached
|
|
36
|
-
TUI,
|
|
37
|
-
worker's cards), once per worker or
|
|
36
|
+
TUI, `--non-interactive` and the web show it; a UI that attaches later gets it from the
|
|
37
|
+
worker's cards), once per worker or `--non-interactive` process, so a respawn or resume says it again;
|
|
38
38
|
- under a `memory_read` miss that looked in project scope (`scope: "project"` or none), and under the
|
|
39
39
|
project index read, on a `Hint:` line after `Error: memory not found: …`. A system-scope read never
|
|
40
40
|
gets it.
|