samagotchi 0.155.0 → 0.157.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 +57 -1
- data/bin/chi +2 -1
- data/docs/architecture.md +22 -13
- data/docs/bundles.md +3 -1
- data/docs/cli.md +9 -8
- data/docs/configuration.md +36 -4
- data/docs/guardrails.md +9 -5
- data/docs/hooks.md +11 -5
- data/docs/internals/anthropic.md +1 -1
- data/docs/internals/attached-context.md +3 -2
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bridge.md +83 -6
- data/docs/internals/bundles.md +28 -18
- data/docs/internals/context-telemetry.md +4 -3
- data/docs/internals/engine.md +641 -937
- data/docs/internals/llm-context-forget.md +8 -3
- data/docs/internals/objectives.md +2 -2
- data/docs/internals/participants.md +3 -2
- data/docs/internals/prompt-caching.md +4 -1
- data/docs/internals/remote-sessions.md +8 -3
- data/docs/internals/web-frontend.md +3 -2
- data/docs/internals/working-set.md +2 -2
- data/docs/memory.md +13 -2
- data/docs/plugins.md +35 -2
- data/docs/sessions.md +11 -11
- data/docs/sub-agent.md +17 -2
- data/docs/system-one.md +3 -2
- data/docs/testing.md +53 -2
- data/lib/samagotchi/answer_command.rb +2 -1
- data/lib/samagotchi/archive_store.rb +4 -3
- data/lib/samagotchi/bootstrap/config_writer.rb +1 -1
- data/lib/samagotchi/bootstrap_command.rb +3 -2
- data/lib/samagotchi/bridge/card_places.rb +10 -4
- data/lib/samagotchi/bridge/card_store.rb +3 -0
- data/lib/samagotchi/bridge/turn_accumulator.rb +109 -9
- data/lib/samagotchi/bridge/turn_drawings.rb +265 -0
- data/lib/samagotchi/bridge.rb +31 -6
- data/lib/samagotchi/broadcast/recipients.rb +8 -8
- data/lib/samagotchi/broadcast/triage_model.rb +1 -1
- data/lib/samagotchi/broadcast_command.rb +3 -3
- data/lib/samagotchi/bundle_command/check.rb +18 -0
- data/lib/samagotchi/bundle_command/try/sandbox.rb +2 -2
- data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +5 -2
- data/lib/samagotchi/bundles/known-names/manifest.yml +4 -7
- data/lib/samagotchi/bundles/known-names/{hooks/known_names.rb → plugin.rb} +17 -0
- data/lib/samagotchi/bundles/source-links/manifest.yml +4 -7
- data/lib/samagotchi/bundles/source-links/{hooks/source_links.rb → plugin.rb} +23 -4
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/child_move.rb +13 -57
- data/lib/samagotchi/child_reports.rb +2 -2
- data/lib/samagotchi/children_status.rb +15 -21
- data/lib/samagotchi/cli/parent_wait.rb +9 -23
- data/lib/samagotchi/config.rb +20 -112
- data/lib/samagotchi/context_command.rb +1 -2
- data/lib/samagotchi/context_sources.rb +5 -4
- data/lib/samagotchi/context_window.rb +9 -12
- data/lib/samagotchi/desktop/macos.rb +1 -1
- data/lib/samagotchi/effective_model.rb +57 -22
- data/lib/samagotchi/engine/context_judge.rb +19 -11
- data/lib/samagotchi/engine/messages.rb +1 -1
- data/lib/samagotchi/engine/models.rb +5 -2
- data/lib/samagotchi/engine/objectives.rb +29 -11
- data/lib/samagotchi/engine/plugins.rb +1 -1
- data/lib/samagotchi/engine/recap.rb +8 -4
- data/lib/samagotchi/engine/side_requests.rb +21 -11
- data/lib/samagotchi/engine/toolkit.rb +10 -9
- data/lib/samagotchi/engine/turn_end.rb +1 -1
- data/lib/samagotchi/engine/turn_helpers.rb +7 -7
- data/lib/samagotchi/engine/verify.rb +1 -1
- data/lib/samagotchi/engine.rb +43 -12
- data/lib/samagotchi/events.rb +3 -2
- data/lib/samagotchi/extension_load.rb +6 -3
- data/lib/samagotchi/guardrails/targets.rb +24 -9
- data/lib/samagotchi/guardrails_command.rb +1 -1
- data/lib/samagotchi/host_address.rb +52 -0
- data/lib/samagotchi/host_config.rb +72 -1
- data/lib/samagotchi/host_model.rb +2 -6
- data/lib/samagotchi/host_registry.rb +58 -46
- data/lib/samagotchi/kernel_loop.rb +53 -124
- data/lib/samagotchi/llm/anthropic_wire.rb +2 -2
- data/lib/samagotchi/llm/chat_loop.rb +40 -21
- data/lib/samagotchi/llm/native_backend.rb +1 -1
- data/lib/samagotchi/llm_context_apply.rb +54 -1
- data/lib/samagotchi/llm_context_notice.rb +1 -1
- data/lib/samagotchi/llm_context_strategy.rb +29 -67
- data/lib/samagotchi/llm_context_view.rb +9 -0
- data/lib/samagotchi/memory_bundle/asset_installer.rb +71 -76
- data/lib/samagotchi/memory_bundle/builder.rb +2 -1
- data/lib/samagotchi/memory_bundle/file_actions.rb +193 -0
- data/lib/samagotchi/memory_bundle/file_digest.rb +13 -0
- data/lib/samagotchi/memory_bundle/file_state.rb +152 -0
- data/lib/samagotchi/memory_bundle/installed_copy.rb +2 -1
- data/lib/samagotchi/memory_bundle/installed_manifest.rb +8 -1
- data/lib/samagotchi/memory_bundle/installer.rb +185 -169
- data/lib/samagotchi/memory_bundle/manifest.rb +14 -4
- data/lib/samagotchi/memory_bundle/merger.rb +69 -65
- data/lib/samagotchi/memory_bundle/plugin_ref.rb +6 -5
- data/lib/samagotchi/memory_bundle/profile.rb +5 -5
- data/lib/samagotchi/memory_bundle/provenance.rb +15 -1
- data/lib/samagotchi/memory_bundle/remerge.rb +25 -12
- data/lib/samagotchi/memory_bundle/shipped_update.rb +51 -6
- data/lib/samagotchi/memory_bundle/status.rb +14 -20
- data/lib/samagotchi/memory_bundle/system_bundle.rb +24 -6
- data/lib/samagotchi/memory_bundle/trash.rb +23 -8
- data/lib/samagotchi/memory_bundle/uninstaller.rb +16 -11
- data/lib/samagotchi/memory_usage.rb +1 -1
- data/lib/samagotchi/model_lookup.rb +75 -0
- data/lib/samagotchi/model_price.rb +4 -10
- data/lib/samagotchi/model_profile.rb +7 -6
- data/lib/samagotchi/model_settings.rb +37 -2
- data/lib/samagotchi/models_command.rb +1 -1
- data/lib/samagotchi/note_command.rb +2 -3
- data/lib/samagotchi/objectives_audit.rb +4 -3
- data/lib/samagotchi/participant_mode.rb +2 -1
- data/lib/samagotchi/participants.rb +2 -2
- data/lib/samagotchi/plugin/api.rb +41 -6
- data/lib/samagotchi/plugin/context.rb +1 -1
- data/lib/samagotchi/plugin/loader.rb +33 -10
- data/lib/samagotchi/plugin/sessions.rb +3 -6
- data/lib/samagotchi/plugin/tool_spec.rb +5 -3
- data/lib/samagotchi/project_env.rb +1 -1
- data/lib/samagotchi/reasoning_history.rb +14 -20
- data/lib/samagotchi/recap_place.rb +27 -0
- data/lib/samagotchi/reminder_carry.rb +1 -1
- data/lib/samagotchi/reply_wait/liveness.rb +38 -0
- data/lib/samagotchi/reply_wait/poll.rb +44 -15
- data/lib/samagotchi/reply_wait/target.rb +113 -0
- data/lib/samagotchi/reply_wait.rb +22 -2
- data/lib/samagotchi/reprefill_meter.rb +45 -0
- data/lib/samagotchi/review_command.rb +1 -1
- data/lib/samagotchi/sampling_settings.rb +9 -16
- data/lib/samagotchi/self_report.rb +36 -37
- data/lib/samagotchi/send_command.rb +17 -23
- data/lib/samagotchi/session.rb +370 -445
- data/lib/samagotchi/session_access/local.rb +12 -11
- data/lib/samagotchi/session_access/remote.rb +22 -6
- data/lib/samagotchi/session_archive_command.rb +2 -2
- data/lib/samagotchi/session_chain.rb +7 -6
- data/lib/samagotchi/session_continue.rb +6 -5
- data/lib/samagotchi/session_delete_command.rb +1 -1
- data/lib/samagotchi/session_field.rb +133 -0
- data/lib/samagotchi/session_inbox.rb +49 -1
- data/lib/samagotchi/session_manager/delivery.rb +254 -0
- data/lib/samagotchi/session_manager/emptiness.rb +139 -0
- data/lib/samagotchi/session_manager/lifecycle.rb +274 -0
- data/lib/samagotchi/session_manager/listing.rb +118 -0
- data/lib/samagotchi/session_manager/removal.rb +190 -0
- data/lib/samagotchi/session_manager/spawn.rb +242 -0
- data/lib/samagotchi/session_manager/worker_command.rb +157 -0
- data/lib/samagotchi/session_manager.rb +28 -1541
- data/lib/samagotchi/session_metrics.rb +8 -5
- data/lib/samagotchi/session_paths.rb +57 -0
- data/lib/samagotchi/session_retention.rb +2 -2
- data/lib/samagotchi/session_row.rb +200 -0
- data/lib/samagotchi/session_setup.rb +15 -12
- data/lib/samagotchi/sessions_command.rb +11 -129
- data/lib/samagotchi/share_command.rb +1 -1
- data/lib/samagotchi/starting_marker.rb +65 -0
- data/lib/samagotchi/steer_cut_after.rb +7 -14
- data/lib/samagotchi/system_one/recorder.rb +49 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +6 -26
- data/lib/samagotchi/terminal_ui/exit_lines.rb +1 -1
- data/lib/samagotchi/terminal_ui/join_replay.rb +86 -8
- data/lib/samagotchi/terminal_ui/local_link.rb +2 -2
- data/lib/samagotchi/terminal_ui.rb +5 -7
- data/lib/samagotchi/testing/install.rb +2 -1
- data/lib/samagotchi/testing/sessions.rb +1 -1
- data/lib/samagotchi/thinking.rb +16 -21
- data/lib/samagotchi/token_usage.rb +1 -1
- data/lib/samagotchi/tool_activity.rb +10 -4
- data/lib/samagotchi/tool_declarations.rb +2 -2
- data/lib/samagotchi/tool_output_cut.rb +17 -0
- data/lib/samagotchi/tool_response.rb +5 -3
- data/lib/samagotchi/tool_runner.rb +25 -5
- data/lib/samagotchi/toolkit_command.rb +1 -1
- data/lib/samagotchi/tools/delegate.rb +55 -45
- data/lib/samagotchi/tools/delegate_relay.rb +5 -5
- data/lib/samagotchi/tools/delegate_result.rb +5 -5
- data/lib/samagotchi/tools/delegate_wait.rb +41 -17
- data/lib/samagotchi/tools/index_hint.rb +14 -0
- data/lib/samagotchi/tools/list_sessions.rb +7 -7
- data/lib/samagotchi/tools/registry.rb +28 -11
- data/lib/samagotchi/tools/send_note.rb +4 -6
- data/lib/samagotchi/tools/tool_index.rb +52 -51
- data/lib/samagotchi/tools/tool_surface.rb +22 -8
- data/lib/samagotchi/turn_context.rb +2 -1
- data/lib/samagotchi/turn_stages/context_judge.rb +38 -36
- data/lib/samagotchi/turn_stages/objectives.rb +56 -52
- data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +13 -17
- data/lib/samagotchi/turn_stages/verify_anchors.rb +9 -10
- data/lib/samagotchi/turn_stages/warmup.rb +14 -15
- data/lib/samagotchi/update_command.rb +11 -7
- data/lib/samagotchi/verify_fresh.rb +7 -11
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +11 -14
- data/lib/samagotchi/web/app.rb +45 -28
- data/lib/samagotchi/web/message_display.rb +57 -7
- data/lib/samagotchi/web/participant_access.rb +3 -3
- data/lib/samagotchi/web/public/activity.js +16 -7
- data/lib/samagotchi/web/public/app.js +29 -8
- data/lib/samagotchi/web/public/bubbles.js +8 -3
- data/lib/samagotchi/web/public/cards_place.js +15 -0
- data/lib/samagotchi/web/public/commands.js +5 -1
- data/lib/samagotchi/web/public/data.js +4 -2
- data/lib/samagotchi/web/public/history_shape.js +220 -0
- data/lib/samagotchi/web/public/recap_stamp.js +17 -0
- data/lib/samagotchi/web/public/reload_check.js +71 -0
- data/lib/samagotchi/web/public/stage_model.js +7 -5
- data/lib/samagotchi/web/public/stage_view.js +7 -3
- data/lib/samagotchi/web/public/stream_handlers.js +8 -2
- data/lib/samagotchi/web/public/timing.js +43 -2
- data/lib/samagotchi/web/public/turn_block.js +215 -30
- data/lib/samagotchi/web/public/turn_drawing.js +43 -0
- data/lib/samagotchi/web/public/turn_events.js +10 -1
- data/lib/samagotchi/web/public/turn_pieces.js +29 -0
- data/lib/samagotchi/web/reload_check_route.rb +95 -0
- data/lib/samagotchi/web/server.rb +4 -2
- data/lib/samagotchi/web/session_access_routes.rb +6 -6
- data/lib/samagotchi/web/session_control.rb +4 -4
- data/lib/samagotchi/web/session_create.rb +3 -3
- data/lib/samagotchi/web/session_hub.rb +9 -17
- data/lib/samagotchi/web/session_show.rb +67 -5
- data/lib/samagotchi/web/session_summary.rb +39 -28
- data/lib/samagotchi/worker.rb +83 -32
- data/lib/samagotchi/worker_boot.rb +184 -0
- data/lib/samagotchi/worker_link.rb +3 -0
- data/lib/samagotchi/worker_signals.rb +1 -1
- data/lib/samagotchi/worker_task_wakes.rb +2 -2
- metadata +32 -4
- data/lib/samagotchi/send_command/wait_target.rb +0 -85
data/docs/internals/bridge.md
CHANGED
|
@@ -16,6 +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
|
+
| `TurnDrawings` | `turn_drawings.rb` | Each finished turn as live drew it (`turn_drawings.jsonl`): written by the accumulator's `on_turn_end` hook, read by chi web's show. |
|
|
19
20
|
| `CardPlaces` | `card_places.rb` | Pure: where `CardStore`'s entries sit in the messages the page draws (`turns_since`, and the `opening_id` stamp). |
|
|
20
21
|
| `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
22
|
|
|
@@ -27,12 +28,19 @@ whose seq a cursor is) and its **secret**; both are published in `bridge.json` (
|
|
|
27
28
|
|
|
28
29
|
A joining UI must draw a turn already half done, and the ring may have dropped its early events. The accumulator
|
|
29
30
|
folds the event log into one hash, `current_turn`: the prompt, `turn_id`, origin, `at`, and ordered `parts`
|
|
30
|
-
(generation, thinking, text, tool, input, steer, reminder, notice), plus `pending_question`. A tool part keeps its
|
|
31
|
-
row fields (`ToolRowFields`), its output cut head-and-tail at `ToolOutputCut::WEB_PREVIEW_CHARS` so a
|
|
32
|
-
row a reload draws,
|
|
31
|
+
(generation, thinking, text, tool, input, steer, reminder, notice, card), plus `pending_question`. A tool part keeps its
|
|
32
|
+
row fields (`ToolRowFields`), its `tool_id`, its output cut head-and-tail at `ToolOutputCut::WEB_PREVIEW_CHARS` so a
|
|
33
|
+
joined row is the row a reload draws, `elapsed_ms` while it runs (the accumulator times running calls itself, `clock:`)
|
|
34
|
+
and, once finished, the event's `duration_ms` (its own clock only for an older worker's event that has none). A
|
|
33
35
|
`notice` part holds the event of a row that is neither text nor a tool call (`TurnNotice::FIELDS`: a hook's notice,
|
|
34
36
|
the "asking again" row, a steer's cut, a ✂ row, a question and its answer), so a join replays it through the handler
|
|
35
|
-
that drew it live.
|
|
37
|
+
that drew it live. A `card` part is a card the turn showed in its step (`in_turn`), its latest version in its first
|
|
38
|
+
one's place; `replay_events` and `join_events` skip it (a running turn's join takes its cards from CardStore), and
|
|
39
|
+
only `replay_events(turn, cards: true)` (a finished turn's drawing) replays it as its `card` event.
|
|
40
|
+
|
|
41
|
+
At the terminal event, before clearing, the accumulator hands a deep copy of the turn and the end event to its
|
|
42
|
+
`on_turn_end:` hook (the Bridge's `TurnDrawings#append`). The hook runs under the accumulator's mutex and the
|
|
43
|
+
observer lock, so it never calls back in; an error is logged (`turn_end_hook_failed`) and the fold goes on.
|
|
36
44
|
|
|
37
45
|
The turn is cleared on its terminal event (`Events::TURN_END`), the same event that brings its messages into the
|
|
38
46
|
session, so a snapshot shows a turn in exactly one of the two places. Beside the turn it keeps what a snapshot also
|
|
@@ -40,10 +48,58 @@ needs and the messages lag: `queued` turns (`:turn_enqueued` until started, merg
|
|
|
40
48
|
the idle `recap` (until a turn starts), a pending `continue_offer`, and the running turn's `working_set`.
|
|
41
49
|
|
|
42
50
|
Three other users: `replay_events(turn)` turns a `current_turn` back into the live events that would have drawn it
|
|
43
|
-
(the
|
|
51
|
+
(the attached TUI's join); `join_events` is the page's unfold of the same turn plus the queue behind it (its
|
|
52
|
+
`turn_started` also carries `at`, `cut_after` and every author key, then a `turn_enqueued` per queued prompt and a
|
|
53
|
+
`command_queued` per waiting command), which chi web's `GET /api/sessions/:id` sends as `replay` from the worker's
|
|
54
|
+
snapshot (`from_wire` symbolizes it), so any worker's snapshot gives the page the same events
|
|
55
|
+
(`spec/shared/turn_snapshot.json` pins it against turn_events.js `snapshotEvents`, the page's own fallback for one
|
|
56
|
+
release); `messages_of` gives a
|
|
44
57
|
plugin's `ctx.messages` the turn so far (`Engine#running_turn_messages`); `participant_turn?` tells `POST /turn` that
|
|
45
58
|
a participant's review turn runs, so the owner's input queues behind it.
|
|
46
59
|
|
|
60
|
+
## TurnDrawings
|
|
61
|
+
|
|
62
|
+
A reload or a reconnect draws a finished turn from what live drew, not rebuilt from its saved messages: the
|
|
63
|
+
accumulator's final fold of the turn (its parts in order, in-turn cards included) and the end event's display keys
|
|
64
|
+
(`TurnDrawings::END_KEYS`), one JSON line per turn in `<session dir>/turn_drawings.jsonl`
|
|
65
|
+
(`{v: 1, turn_id, kind, from, to, from_digest, to_digest, drawing, end}`, `TurnDrawings::Line`).
|
|
66
|
+
|
|
67
|
+
- **The range comes from the Engine.** The end event's `kept_from`/`kept_to` (`Engine#end_turn`) name the turn's own
|
|
68
|
+
messages, `kept[from...to]`. No range (an older worker, a turn that kept nothing) or an empty one writes no line,
|
|
69
|
+
and the turn draws as before.
|
|
70
|
+
- **Validity.** A line holds while the messages reach `to` and `messages[from]` and `messages[to - 1]` digest as the
|
|
71
|
+
line says (`TurnDrawings.digest`: role, content and time, one canonical form for the live symbol-keyed messages
|
|
72
|
+
and the saved, scrubbed ones). A rollback, a rolled-back failure or a prompt handed back drops it. Of overlapping
|
|
73
|
+
valid lines (a rollback, then a re-run that said the same) and of one turn id's lines, the newest wins
|
|
74
|
+
(`TurnDrawings.valid`).
|
|
75
|
+
- **Writing.** The accumulator calls `TurnDrawings#append` at the end event, inside the observer delivery, so the
|
|
76
|
+
line is on disk before a stream writer (subscribed later) gets the end frame. That departs from SessionObserver's
|
|
77
|
+
"enqueue-only" note, as CardStore's save does: one append per turn end. It writes `"\n"` first when the file's last
|
|
78
|
+
byte isn't one (a torn line from a crash), then the line in one write; a failure is logged (`turn_drawing_failed`).
|
|
79
|
+
A part's output is the accumulator's cut (`WEB_PREVIEW_CHARS`); a diff is the one the saved `tool_diffs` have
|
|
80
|
+
(`TextDiff`'s cut); images are refs.
|
|
81
|
+
- **Reading.** `TurnDrawings.read(dir)` skips an unknown `v`, a torn or broken line and one missing a field, and is
|
|
82
|
+
cached by the file's path, size and mtime. `#last` is the newest line this worker wrote or started from.
|
|
83
|
+
- **The snapshot.** `Bridge#snapshot` (the snapshot and reset frames, `GET /snapshot`) adds `last_turn_drawing:
|
|
84
|
+
{drawing, end}`, `#last` as the file has it, not unfolded, when it holds against the snapshot's messages and its `to`
|
|
85
|
+
is their size (the last exchange); it is absent otherwise (no turn yet, a rollback, a Continue's note after it, a line
|
|
86
|
+
of another version, one over the 1 MB a restart reads back: `TurnDrawings.sendable?`). An attaching TUI unfolds it (`TerminalUI::JoinReplay`, `TurnAccumulator.replay_events`) and draws
|
|
87
|
+
that turn's rows through its `EventRenderer`, as a running turn's join does, then the end event's cancel or failure
|
|
88
|
+
line, else the saved answer; without it (or for a drawing that isn't the last prompt's turn: a reminder's, a
|
|
89
|
+
Continue's) the rows are rebuilt from the messages as before. `chi --remote` attaches through chi web's stream proxy,
|
|
90
|
+
which relays an owner's frames byte for byte, the field included. A participant never gets it: its stream's snapshot
|
|
91
|
+
is bare (`ParticipantView`). An older TUI ignores the field; an older worker sends none.
|
|
92
|
+
- **The show.** chi web's full read with `?parts=1&drawings=1` (the page's) stamps each display item of a kind a
|
|
93
|
+
drawing draws (the opening, steps, steers, merged lines, the empty answer) in a valid range with `drawn: <turn_id>`
|
|
94
|
+
and drops its `parts` (its ✂ marks stay as `marks: {tool_id => edit}`), and sends `turn_drawings: {turn_id =>
|
|
95
|
+
{events, end}}`: `TurnAccumulator.replay_events(drawing, cards: true)`, the one unfold, in Ruby. A context note in
|
|
96
|
+
a range and a `!cmd`'s record are not stamped. Without `drawings=1` (an older page) the read is what it was.
|
|
97
|
+
CardStore lists an in-turn entry's turn as `of_turn`, so the page leaves a drawn turn's own rows and cards to its
|
|
98
|
+
drawing (`cards_place.js`). A live worker whose sidecar features lack `turn_drawings` (`TurnDrawings::FEATURE`:
|
|
99
|
+
an older one lists no `of_turn`) gets no drawings in the show: its turns all draw as before.
|
|
100
|
+
- **Compat.** An older worker writes nothing (its turns fall back); an older chi web never opens the file. A fork
|
|
101
|
+
or a continue copies no drawings: its turns fall back. A one-shot run (no Bridge) writes none.
|
|
102
|
+
|
|
47
103
|
## CardStore
|
|
48
104
|
|
|
49
105
|
The cards a plugin or the Engine shows (`Engine#show_card`), hook notices, load warnings (`:guardrail_warning`),
|
|
@@ -77,6 +133,9 @@ the "asking again" and steer-cut rows, ✂ rows and questions (with how they wer
|
|
|
77
133
|
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
134
|
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
135
|
keeps today's rule: a rolled-back turn's cards name the opening before it.
|
|
136
|
+
The saved recap carries the same stamp: `saved_recap`'s `opening_id` (`RecapPlace.opening_id`, the id of the last
|
|
137
|
+
opening its `covered` messages hold, read at read time, so `recap.json` is unchanged), which the page places it after
|
|
138
|
+
(`recap_stamp.js`); an older worker's `saved_recap` has none, and the page counts `turns_since`.
|
|
80
139
|
- **A turn that leaves nothing** is read, not told: an in-turn entry not `current` whose turn has no opening in the messages
|
|
81
140
|
(a `!rollback`, a failed turn with nothing kept, a prompt stopped before the model) lists as a between-turns entry at its
|
|
82
141
|
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
|
|
@@ -195,6 +254,22 @@ The Bridge exposes:
|
|
|
195
254
|
"params":"path=notes.txt","event_seq":812}
|
|
196
255
|
```
|
|
197
256
|
|
|
257
|
+
Both tool events carry the run's `tool_id` (`ToolIds`, the id its output is saved under in the session's
|
|
258
|
+
`tool_ids`, handed out before the batch runs), and `tool_call_completed` carries `duration_ms`: the call's time from
|
|
259
|
+
the `tool_call_started` emit to its own, less an approval's `waited_ms`, rounded first (SessionMetrics' tool record
|
|
260
|
+
definition). It is the one duration every UI shows: the web's live row, the tool record a reload draws, the attached
|
|
261
|
+
terminal's line; each keeps its own clock only for an older worker's event without it. A long output's
|
|
262
|
+
`tool_call_completed` also carries `preview`: `ToolOutputCut.preview` of the capped output at `WEB_PREVIEW_CHARS`
|
|
263
|
+
(a `read`'s head, any other tool's head and tail), the text a reload's row has from the saved output
|
|
264
|
+
(`MessageParts#tool_part`; spec/tool_preview_parity_spec.rb pins the two). The web row and the accumulator's part
|
|
265
|
+
take it; the TUI keeps `output`.
|
|
266
|
+
|
|
267
|
+
An end event (`turn_completed`, `turn_canceled`, `turn_failed`) of a turn that kept messages carries `kept_from` and
|
|
268
|
+
`kept_to`: the turn's own messages are `messages[kept_from...kept_to]` of the session it leaves (`Engine#end_turn`;
|
|
269
|
+
`kept_from` is the conversation's size under the system head the turn may have added, before its reminders and
|
|
270
|
+
prompt). Small integers on the event only, never on a message; no client draws them (`Bridge::TurnDrawings` reads
|
|
271
|
+
them). An older worker's end event has neither.
|
|
272
|
+
|
|
198
273
|
A `turn_completed` frame omits `result`; `turn_summary.output` carries the answer:
|
|
199
274
|
|
|
200
275
|
```
|
|
@@ -256,7 +331,9 @@ The Bridge exposes:
|
|
|
256
331
|
deletes it, and the Bridge announces `:turn_dequeued`. `200 {status: "removed", enqueued_id, prompt}`, `409 already_started`
|
|
257
332
|
(a turn took it), `409 not_queued` (a next-step message: a drain may merge it any moment), `404
|
|
258
333
|
unknown_enqueued_id`. It never wakes the worker. The worker names it in its sidecar features (`unqueue`).
|
|
259
|
-
A worker's start prunes the `input/removed/` and `input/settled/` tombstones
|
|
334
|
+
A worker's start prunes the `input/removed/` and `input/settled/` tombstones and the drain's merge records
|
|
335
|
+
(`input/merged/<enqueued_id>` = `{into, at}`, written by `Worker#pending_input_drain` before it deletes a file it
|
|
336
|
+
merges; their own directory, which a 0.156 waiter never reads) older than a week
|
|
260
337
|
(`SessionInbox::TOMBSTONE_MAX_AGE`, log `tombstones_pruned`); otherwise they go with the session dir.
|
|
261
338
|
- `POST /session/:id/relay` — the approval relay, on a delegated child's worker
|
|
262
339
|
(`{action, relay_id, question_id}`): `opened` / `closed` (with a `reason`) set or clear the
|
data/docs/internals/bundles.md
CHANGED
|
@@ -44,12 +44,14 @@ user-facing side (what a bundle owns, profiles, needs) is in [memory.md](../memo
|
|
|
44
44
|
| `MemoryFiles` | which top-level `*.md` files of a bundle are its memories (`.select`: the `files:` list, else all but README/LICENSE and the like), `.unshipped` (listed, not shipped) and `.symlinks` (skipped: a symlink is never a memory) |
|
|
45
45
|
| `Manifest` | parses/validates `manifest.yml` (`name:` is the bundle's name and a directory under `.bundles/`, so a path such as `../x`, `a/b`, `.` or `..` is refused: `name: … is not a bundle name`; YAML that doesn't parse or isn't a mapping is a `ValidationError` too; files, hooks as `BundleHook`, plugin, scripts, needs, requires_chi, includes, context_providers, session_settings: the keys a session's `chi --set` may give, a list or a mapping of them to types (`SESSION_SETTING_TYPES`), recorded at install in `manifest.json` (`session_settings`, `session_setting_types`) for `SessionConfig` to check; settings: the keys config.yml's `bundles: <name>:` may have, the same shape, recorded as `settings` (nil when the manifest has none) and `setting_types` for `BundleSettingsCheck`, whose warnings the Engine holds for the first turn and `SessionConfig`'s refusals share the words of) and writes it (`Manifest.write`, for Builder). It ignores `requires_chi_next`, a shipped bundle's release-time marker (`script/release/release_tools.rb`, docs/releasing.md). `KEYS` is every top-level key it reads, so a key outside it is one this chi ignores: `#unknown_keys`, in the order the manifest lists them, said in `Manifest.unknown_key_line`'s words by the Installer and `chi bundle check`. `SINCE` is the first release that read each key or shape (taken from the tags), and `#floor_keys` names the ones this manifest uses — including the shapes only a newer chi reads (a `files:` mapping, a hook's `events:`, a typed `session_settings:`/`settings:`) — for check's `requires-chi-floor` |
|
|
46
46
|
| `Installer` | one install/upgrade/dry run of one bundle (below) |
|
|
47
|
-
| `AssetInstaller` | the parts that live in `.bundles/<name>/`: hooks, guardrail rules, plugin, scripts; returns `AssetInstaller::Installed` (the paths the record keeps). A file it replaces or removes whose bytes are neither the recorded sha nor the incoming file's is copied to the Installer's `Trash` first (`Trash#copy_edit`, `#copy_edits` for a folder at any depth, as `hooks/x`, `guardrails/x`, `plugin/x`, `scripts/sub/x`; no sha recorded counts as edited; a dev link never is) |
|
|
48
|
-
| `
|
|
47
|
+
| `AssetInstaller` | the parts that live in `.bundles/<name>/`: hooks, guardrail rules, plugin, scripts; returns `AssetInstaller::Installed` (the paths the record keeps). A file it replaces or removes whose bytes are neither the recorded sha nor the incoming file's is copied to the Installer's `Trash` first (`Trash#copy_edit`, `#copy_edits` for a folder at any depth, as `hooks/x`, `guardrails/x`, `plugin/x`, `scripts/sub/x`; no sha recorded counts as edited; a dev link never is). The rule alone, read-only, is `Trash.edit?` / `Trash.edits`. A dry run reports each asset as the real run would (`would_install`, `would_update`, up to date): one result, `#result_for`, recorded as is or through `#planned` |
|
|
48
|
+
| `FileState` (file_state.rb) | one bundle memory as a command meets it, derived, never stored: `FileState.of(key, record:, provenance:, target_dir:, source_dir:, claimants:, shipped:)` reads the file (D), its base (B), the bundle's text (I) and the record's entry (R) once into `Digests` (`disk`, `base`, `incoming`, `recorded`, `marked_incoming`; `:unreadable` for a file it can't read) and names its `kind` and `mark` (`:none`, `:current`, `:legacy` = `InstalledBundle.legacy_mark?`). With a source (install, upgrade, remerge, restore, a plan): `:new`, `:user_same`, `:user_file` (not the bundle's), `:missing`, `:level` (D == I), `:pristine`, `:edited`, `:diverged`, `:pristine_no_base`, `:edited_no_base`, `:dropped_pristine`, `:dropped_edited`, `:dropped_missing` (`shipped: false`: the bundle no longer ships the key), `:unreadable`; with `source_dir: nil` (status, uninstall, diff) only `:pristine`, `:edited`, `:pristine_no_base`, `:edited_no_base`, `:missing`, `:unreadable`. Edited or not is always `#edited?` = `!Provenance.unchanged?` (the one edited rule: the checksum first, then the base); the base then says what changed, and a base the checksum contradicts (equal to an edited file) is no base to merge from (`:edited_no_base`). `#source_moved?` (a mark that speaks of other text than the bundle ships: Remerge's first question), `#shared_with` (the other bundles listing it, `Provenance.claimants` asked lazily, at most once: only a dropped file and an uninstall ask) |
|
|
49
|
+
| `FileActions` (file_actions.rb) | what each command that acts does in each state: one table per command (`INSTALL`, `FORCE` = install and upgrade `--force`, `UPGRADE`, `REMERGE`, `RESTORE`, `UNINSTALL`), a cell keyed `[kind, mark]`, `[kind, :any]` or `[kind, :shared]`, its value an `Action` (`do`: `:write_incoming`, `:merge`, `:decide`, `:keep`, `:skip`, `:move_to_trash`, `:refuse`, `:restore`; `save_first`; `next_base`: `:on_disk`, `:incoming`, `:recorded`, `:none`; `word`, the report bucket; `record`: `:own`, `:keep_entry`, `:mark`, `:hold_mark`, `:clear_mark`, `:drop`, `:not_owned`). `.for(command, state)`. A `:decide` cell (`[:diverged, :any]`, the legacy guard's `[:pristine, :legacy]`) isn't a function of kind and mark: `Merger::Upgrade#decision` answers it with an `Outcome`, which `.decided(outcome, command)` turns into the action. A dry run reads its command's row and doesn't do it (`Installer::DRY_WORDS` gives its word). A new kind or a new acting command adds a row in each table it reaches; `file_actions_spec` checks every cell against the bundle-state corpus |
|
|
50
|
+
| `Merger` | the upgrade's merge. `Merger::Upgrade` (provenance, target dir, source dir) is the one way to ask what an upgrade does to a file, nothing written: `#decision(key[, state])` → a `Decision` (the `FileState`, the UPGRADE action with a `:decide` cell answered, and the `Outcome`), `#outcome` its `Outcome` (`status`: `install`, `fast_forward`, `keep`, `noop`, `conflict`, `merged`; `base`: the next base, `Action#next_base`'s words), `#state`, `#forecast` (a `Forecast` of `merged` and `behind` in one pass), `#behind` (a `conflict` outcome as a `FileStanding::Behind`: why, and from `TextMerge.changed_sections` what it missed, nothing for an older mark, whose base may be the user's text), `#paths`, used by the Installer's merge and dry run, `Remerge`, `ShippedUpdate.plan` and `SystemBundle.sync(dry_run:)`. A `:decide` cell runs `.merge_outcome` (`TextMerge` where both sides changed, git once per call: `merged`, with its `TextMerge::Result` and the digest it read as the Outcome's `merge` and `current_sha`; `ALREADY_MERGED`, a `keep` with base `:incoming`, when the merge adds nothing; or `conflict` with that `merge`, whose `why` says why none was possible; no base: `TextMerge::NO_BASE`, a base the checksum contradicts: `FileStanding::LEGACY_BASE`). `.write_merged` puts a merge in place: the digest checked just before the rename (`AtomicFile.write`'s block), through a symlink, the mode kept, the old file in the `Trash`; false when a live `memory_write` got in. A mark from an older chi (`InstalledBundle.legacy_mark?`: no `incoming_sha`, or `legacy_base`, which keeps that known once the digest is recorded, until the mark goes) may have the user's own text as its base, 0.149's doing, and a merge against it would undo that text with nothing to show, or a fast-forward replace all of it: the upgrade's `[:pristine, :legacy]` cell decides: `#decision` makes a fast-forward that takes a line out of the file, and `.merge_outcome` a merge that does, a `conflict` (`FileStanding::LEGACY_BASE`), on the Installer's and `Remerge`'s path alike, unless `ShippedHistory` proves the base is a version chi shipped. The price, with no such gem left or for a bundle chi doesn't ship: a real base looks the same, so such a file whose bundle rewords or removes a line it still holds stays behind until it is resolved by hand, with `--agent` or with `--force`; no retry fixes it. A fast-forward that goes ahead over a mark is a `save_first` cell (`Installer#save_first`): a file put back to its base under this chi's mark, or an older mark with a proved base or an update that only adds. `.markers?` (a file holding a merge's `<<<<<<<`/`>>>>>>>` lines; `=======` alone is a Markdown underline) |
|
|
49
51
|
| `ShippedHistory` | `.shipped?(bundle, file, path)`: whether those bytes are that file of that bundle (by manifest name, since a shipped dir can be renamed) in an installed `samagotchi-<version>` gem. Reads `<gem dir>/gems/` by hand, for `Gem.path` (set by a chi home's boot string) and the home's `gem_dirs` from home.json (a checkout or another Ruby runs on other gems), never `Gem::Specification`. Size, then bytes, then the manifest. Anything it can't read is no match, which keeps the guard. Asked only when `Merger`'s guard would otherwise fire: with 54 gems on the Studio a miss costs about 4 ms, a hit less |
|
|
50
52
|
| `TextMerge` | the text merge: `.merge(base:, ours:, theirs:)` → a `Result` (`status` `:clean` with `text`, `:conflict` with `hunks`, `:failed` with `why`). `git merge-file -p --diff3 --marker-size=20` on temp copies in a temp cwd, with every `GIT_*` that points at a repository or a config unset or at `/dev/null` (it can run inside another tool's hook). git isn't a runtime dependency: none on PATH, or macOS's `/usr/bin/git` with no developer tools behind it (`.apple_stub?`, by `xcode-select -p`; running it pops a dialog), is `:failed`. So is a git past `TIMEOUT` (10 s; its process group is killed: this runs under `SystemBundle`'s lock), and any doubt about the answer: `min(hunks, 127)` against the exit status, a status over 127, a NUL in a side, marker lines that don't pair up (a user's line equal to one), a line no input holds, or an empty answer to non-empty inputs. `Scan` reads the output: a hunk unions (ours, then the entries of theirs that ours lacks) only when the last heading before it, at level 1 or 2 and outside code fences, HTML comments and any hunk, is titled exactly `Changelog` or `Change log`, with nothing but list entries (an item and its indented lines) since that heading and no fence or comment open, and base, ours and theirs each hold only list entries, with every base entry still on both sides: both only added. Any other hunk is a real conflict, and markers never leave the module. An entry is an item and the lines indented deeper under it, nested items too, and the union compares whole entries. `.changed_sections(base, theirs, ours)`: the headings of the sections the bundle changed and the file hasn't got, and whether text under no heading did, which is what names a miss: a conflict's hunks can't, since the whole file stays and the one hunk may be a Changelog line beside a whole new section. `Hunk#heading` counts the places under no heading |
|
|
51
53
|
| `FileStanding` | the one owner of what chi says of an edited bundle file, used by every surface: `Behind` (`#state`, `#line`: `x.md stays behind 0.8.0: no merge possible (why); missed ## A, ## B, ## C and 2 more`, `would stay` in a dry run), `MergeNote`, `.merged_line`, `.no_merge` (for `LEGACY_BASE` it says chi can't tell whether the file holds edits), `.clauses` (a report's clauses for one bundle), `.behind_count`, `.stay_behind`, `.counts_sentence` (the push), `.still_behind`, `.text` (UTF-8, whatever a name or heading was tagged), `FORCE_TAKES`, `FORCE_MOVES`, `IN_THE_TRASH`. Behind means marked and bytes other than the mark's `incoming_sha` (`.behind?`, read by `InstalledBundle#behind_files` and `Remerge` alike): a file brought level by hand isn't, a missing one is a missing file, and one that can't be read is. An older mark no retry has looked at reads `may be behind` (`Behind#unsure`, and the counts say so), and isn't behind at all when level with its recorded source on disk (`InstalledBundle#behind_file?`). A new surface reads these and adds no wording |
|
|
52
|
-
| `Remerge` | the retry at the same version, memory files only, for a file marked `conflict`: `#run(dry_run:)` → a `Result` (`merged`, `settled`, `stamped`, `behind`, `warnings`, `trash_copy`). It merges what merges and settles those entries (`Provenance#resolve_conflicts`, the index line refreshed), leaving `version`, `installed_at` and the assets alone: the Installer would `rm_rf` guardrails, plugin and scripts under live sessions. Every file it leaves is in `behind`, with why: one not shipped now (`MemoryFiles.select`) keeps its mark, base and text; one whose `incoming_sha` isn't the shipped text (a checkout at one VERSION) is left alone, no git runs, and it reads as its mark says; a mark with no digest (older chi) is tried as well, and a real conflict then records it (`Provenance#refresh_marks`, which writes only when the mark would read differently, so a conflict at each start writes nothing). A mark whose digest matches is tried on every run, since the file or git may have changed. `noop` and `keep` settle quietly; `fast_forward` waits for the next upgrade, reading as behind until then; `install` is a missing file, which isn't behind, and a warning says so. A file it can't read or replace stays as it is, saying why. Called from `ShippedUpdate.plan_row`/`apply` (`chi update`) and `SystemBundle.sync`'s equal-version branch, under its lock (each chi start and `chi update`); a bundle chi doesn't ship retries through `chi bundle upgrade`. No marks: nothing is read beyond the record. One marked file costs one git run per Engine start, about 12 ms on the Studio (0.1 ms with none) |
|
|
54
|
+
| `Remerge` | the retry at the same version, memory files only, for a file marked `conflict` (its `FileState` and `REMERGE` row; a `:decide` cell through `Merger::Upgrade#decision`): `#run(dry_run:)` → a `Result` (`merged`, `settled`, `stamped`, `behind`, `warnings`, `trash_copy`). It merges what merges and settles those entries (`Provenance#resolve_conflicts`, the index line refreshed), leaving `version`, `installed_at` and the assets alone: the Installer would `rm_rf` guardrails, plugin and scripts under live sessions. Every file it leaves is in `behind`, with why: one not shipped now (`MemoryFiles.select`) keeps its mark, base and text; one whose `incoming_sha` isn't the shipped text (a checkout at one VERSION) is left alone, no git runs, and it reads as its mark says; a mark with no digest (older chi) is tried as well, and a real conflict then records it (`Provenance#refresh_marks`, which writes only when the mark would read differently, so a conflict at each start writes nothing). A mark whose digest matches is tried on every run, since the file or git may have changed. `noop` and `keep` settle quietly; `fast_forward` waits for the next upgrade, reading as behind until then; `install` is a missing file, which isn't behind, and a warning says so. A file it can't read or replace stays as it is, saying why. Called from `ShippedUpdate.plan_row`/`apply` (`chi update`) and `SystemBundle.sync`'s equal-version branch, under its lock (each chi start and `chi update`); a bundle chi doesn't ship retries through `chi bundle upgrade`. No marks: nothing is read beyond the record. One marked file costs one git run per Engine start, about 12 ms on the Studio (0.1 ms with none) |
|
|
53
55
|
| `Provenance` | the `.bundles/<name>/` dir: `#write` the record + bases (its `files:` is the full list: a key left out is dropped, a nil value keeps the entry with no base; `marks:` are the files left behind, each a `FileStanding::Behind`, and `incoming_dir:` the bundle text each speaks of, recorded as `incoming_sha` and saved at `#incoming_path`; an older mark that stays keeps `legacy_base`), `#resolve_conflicts` (base to the incoming file, mark and its fields (`MARK_FIELDS`) and saved text gone; a file whose incoming copy is gone keeps both), `#refresh_marks` (what a retry found: why, missed, the digest and `legacy_base`, the saved text), `#note_merged` and `#clear_merge_notes(notes)` (`MERGE_NOTE_FIELDS`, kept through `#write`; the saved text is best effort, after the manifest, so it can't leave a plugin under the old record's sha; any other failure writing the record still can, until a retry), `#record` → `InstalledBundle`, `.each_installed`, `.claimants` (which bundles own a file). `#write(manifest_from: DIR)` keeps `DIR/manifest.yml` at `#kept_manifest_path` (written aside and renamed; for `dev:` a link to its realpath) and records `manifest_sha` (`FileDigest.sha256`, `DEV_SHA` for dev) and `installed_by` (this chi's VERSION); the Installer and `Profile.install` pass their source dir, and a write without it carries the existing record's two fields, the kept file left alone. The in-place edits (`#resolve_conflicts`, `#record_restored`, `#refresh_marks`, `#note_merged`, `#clear_merge_notes`) keep them. The record's mirror of the manifest keys (`requires_chi` … `setting_types`) is still written from the kwargs, exactly as before: an older chi sharing the memories dir (an old worker, chi web, gem or worktree) reads only the record, and without the mirror's `requires_chi` it would load a too-new bundle's plugin and hooks. A key a later manifest gains is never mirrored. Any chi without the kept manifest that rewrites a record drops `manifest_sha` and `installed_by` (it rebuilds the record from its own kwargs). `#adopt_manifest(dir, dry_run:)` heals such a record (`InstalledBundle#manifest_unkept?`: no `manifest_sha`, or its file gone): when `dir` is the source of what's installed (its manifest names the bundle at the recorded version; each mirrored key, normalised as `.mirror_fields` records it (the one shape `#write` uses too), equals the record's, a key the record lacks being fine only when `Manifest::SINCE` has it, since an older chi couldn't have recorded it; each recorded hook's events, on_error and priority are the manifest's; and every recorded plugin, hook, script, rule and unmarked memory sha is that of `dir`'s file; else the why names the key or file) it keeps `dir/manifest.yml` and records `manifest_sha`, nothing else (not `installed_at`, the mirror or `installed_by`); otherwise it returns why and writes nothing. Never for a dev record (its next `install --dev` links the file) |
|
|
54
56
|
| `InstalledManifest` (installed_manifest.rb) | the one answer to "this installed bundle's manifest" (`InstalledBundle#manifest`): requires_chi, trust_level, hook metadata, needs, the scripts' names, context providers, block renderers, session settings and settings with their types, file descriptions, a profile's includes. Read from the kept `manifest.yml` (`origin: :kept`) when the record's `manifest_sha` is set and the file still has it (`Provenance.sha_matches?`, so a dev link is never checked), else from the record's `mirror` (`origin: :record`), in these cases: no kept file and no `manifest_sha` (a record from before chi kept it; no problem); no kept file, or something other than a file in its place, while `manifest_sha` is set (`:missing`); a kept file and no `manifest_sha` (a chi without the kept manifest rewrote the record: `problems["manifest.yml"] = :stale`); a kept file with another sha, or one that isn't a YAML mapping (`:edited`); the sha matches (`:kept`). `#standing` is that symbol, `#key_problems` the rest. The kept file is read by `Manifest.tolerant` key by key in a fixed order (its comment): a key that doesn't parse under this chi takes the mirror's value if that parses, else empty, with its message in `problems` (a reader that logged it logs that); `block_renderers` is checked against the record's scripts when the kept `scripts:` fails, and one bad hook entry keeps the record's hooks whole. Hooks: each *recorded* hook with the kept manifest's events, on_error and priority and the record's sha (a found `hooks/*.rb` with no entry as recorded; a manifest hook the record lacks isn't installed, so isn't listed). `description` and `unknown_keys` come only from a kept file; a profile's `includes` is always the record's (its members, identity). The value is deep-frozen, since callers share it. One process-wide cache (a Hash under a Mutex, cleared at `CACHE_LIMIT`) keyed by the record value and the kept file's stat (mtime, size, inode, device; a dev link's target): chi web asks per request, a worker at start and per `ContextProviders.resolve`. `#requires_chi_failure(version = VERSION)` is the one requires_chi gate; `#experimental?`, `#needs?`, `#context_providers?`, `#block_renderers?` |
|
|
55
57
|
| `ManifestStanding` (manifest_standing.rb) | the one owner of the words for an installed bundle's `manifest.yml` as `InstalledManifest` read it: `.state` (`:kept`, `:legacy`, `:stale`, `:missing`, `:edited`), `.label` (`[kept]` / `[from the install record]`), `.line(bundle, refused:)` (the case and the command: `chi bundle upgrade NAME` (reads it again from a git URL, or a source that didn't match, with why), `chi update keeps it` while untried, the source is gone (`Installer.source_gone_line`), `chi bundle install --dev SOURCE` for a dev record; for a kept one, only the keys whose `Manifest::SINCE` floor is above the record's `installed_by`: newer than its installer), `.keep(bundle, source_dir:, dry_run:)` → `Outcome` (the record after, the line, `kept`): `Provenance#adopt_manifest` from `source_dir`, else from the recorded source (a dir; a zip/tar through `PreparedSource`, cleaned up; never a git clone). Read by `ShippedUpdate` (`Row#manifest`, the plan's dry `keep` and apply's), `UpdateCommand#external_rows` (a `kept its manifest` row, else a warn row, before the bundle's re-check, which then sees the kept file) and `chi bundle status NAME` (`BundleCommand#manifest_line`: `Manifest:`, the label, `.status_line`: for a record with a folder source (a bundle chi ships: this chi's shipped copy at the recorded version, which `chi update` reads), a dry `adopt_manifest` first, so the line says whether `chi update` keeps it or why not; an archive isn't extracted there, its line says `chi update tries to keep it`; a profile chi doesn't ship is never kept by `chi update`, so its line names `chi bundle upgrade NAME`) |
|
|
@@ -57,9 +59,9 @@ user-facing side (what a bundle owns, profiles, needs) is in [memory.md](../memo
|
|
|
57
59
|
| `BlockRenderers` (`lib/samagotchi/block_renderers.rb`) | a manifest's `block_renderers:`: `.parse_list`, and what chi web asks without loading plugins: `.for_installed` (first bundle by name wins a language; a record with an error, an unmet `requires_chi` or a list that doesn't parse is skipped) and `.script_path` (`load` membership first, then `Provenance.sha_matches?`). `Manifest` checks each `load` is a `scripts:` key; the files are ordinary scripts everywhere else ([block-renderers.md](block-renderers.md)) |
|
|
58
60
|
| `Builder` | `chi bundle build`: memories (+ the installed bundle's hooks, rules, plugin, scripts) → zip/tar/dir |
|
|
59
61
|
| `Profile` | meta bundles (`includes:`): install the members, record which; `InstallResult#kept_own` names a new member's memory files the user already had, with other bytes (`kept_own_line`, printed by `chi bundle`, `chi update` and `chi bootstrap`); a dry run runs each member's Installer as a dry run too, so it names them as well |
|
|
60
|
-
| `SystemBundle`, `ShippedUpdate` | the built-in system bundle (`ensure!` at Engine start, `sync` in `chi update`) and `chi update`'s upgrade of shipped bundles; both drive the Installer with `upgrade: true` and `Remerge` at an equal version (`ShippedUpdate::Row`'s `:would_merge`, `:merged`; `SystemBundle::Result`'s `:merged`). `SystemBundle` also puts back a memory missing at an equal version, that file alone, with no Installer (`verify_files_present`, `Provenance#record_restored`). `ensure!` echoes a merge in one line, in the row's words. That reaches a terminal launch; a worker has only its log (`Log.configure(stderr: false)`), so every merge leaves a note on the file's record (`Installer#note_merges`, a failed run's too, and `Remerge#note_merges`; `Provenance#note_merged`: version, time, trash dir). `chi bundle status` shows it, and `chi update` reads the notes before the run (`SystemBundle.merge_notes`, `ShippedUpdate::Row#earlier`), names them in the row and clears after it those exact ones and the notes of the files that run merged (a worker's meanwhile, on another file, stays), unless it was a dry run, the row failed or has no place for them (a skipped system row), or the run is unattended (`ChildEnv::UNATTENDED_UPDATE`, which `Web::UpdateOps` sets for `update.auto`, an env marker so a hand-over's exec keeps it and an older chi ignores it; `--json` alone says nothing, as a person or script may use it). `chi bundle upgrade` clears the notes of what it printed, and `--force` those of the files it replaced. `--json`'s `merged` is this run's, `earlier` the notes'. `ensure!` says what an upgrade left behind and nothing of a file already behind, which `Result#behind` still has for the row. A plan or sync that can't read a file is that bundle's failed row, in `Trash.failure_message`'s words (`ShippedUpdate.plan_row`, `SystemBundle.sync`). At an equal version both keep the shipped `manifest.yml` for a record an older chi wrote (`Provenance#adopt_manifest` from the shipped dir): `SystemBundle.sync` under its lock, not in a dry run (so at the first Engine start or `chi update` after the upgrade, once), and `ShippedUpdate.apply` for a row that ends `:up_to_date` or `:merged` (`#keep_manifest`, through `ManifestStanding.keep`: `Row#manifest` is its line, `would keep its manifest` from the plan's dry run, appended to the row's status); a bumped bundle gets it from its Installer. `ShippedUpdate.external` is the rest: what `chi update` only checks |
|
|
62
|
+
| `SystemBundle`, `ShippedUpdate` | the built-in system bundle (`ensure!` at Engine start, `sync` in `chi update`) and `chi update`'s upgrade of shipped bundles; both drive the Installer with `upgrade: true` and `Remerge` at an equal version (`ShippedUpdate::Row`'s `:would_merge`, `:merged`; `SystemBundle::Result`'s `:merged`). `SystemBundle` also puts back a memory missing at an equal version, that file alone, with no Installer (`verify_files_present`, `Provenance#record_restored`), checked against the manifest's `files:` sha as an installed one is (`FileDigest.mismatch`). A sync's `Result#warnings` reach `chi update`'s row (`UpdateCommand#system_bundle_row`) and `chi bootstrap`'s line (`BootstrapCommand#system_bundle_line`); `ShippedUpdate.apply` puts an update's `Checksum mismatch` warnings in `Row#note`. `ensure!` echoes a merge in one line, in the row's words. That reaches a terminal launch; a worker has only its log (`Log.configure(stderr: false)`), so every merge leaves a note on the file's record (`Installer#note_merges`, a failed run's too, and `Remerge#note_merges`; `Provenance#note_merged`: version, time, trash dir). `chi bundle status` shows it, and `chi update` reads the notes before the run (`SystemBundle.merge_notes`, `ShippedUpdate::Row#earlier`), names them in the row and clears after it those exact ones and the notes of the files that run merged (a worker's meanwhile, on another file, stays), unless it was a dry run, the row failed or has no place for them (a skipped system row), or the run is unattended (`ChildEnv::UNATTENDED_UPDATE`, which `Web::UpdateOps` sets for `update.auto`, an env marker so a hand-over's exec keeps it and an older chi ignores it; `--json` alone says nothing, as a person or script may use it). `chi bundle upgrade` clears the notes of what it printed, and `--force` those of the files it replaced. `--json`'s `merged` is this run's, `earlier` the notes'. `ensure!` says what an upgrade left behind and nothing of a file already behind, which `Result#behind` still has for the row. A plan or sync that can't read a file is that bundle's failed row, in `Trash.failure_message`'s words (`ShippedUpdate.plan_row`, `SystemBundle.sync`). At an equal version both keep the shipped `manifest.yml` for a record an older chi wrote (`Provenance#adopt_manifest` from the shipped dir): `SystemBundle.sync` under its lock, not in a dry run (so at the first Engine start or `chi update` after the upgrade, once), and `ShippedUpdate.apply` for a row that ends `:up_to_date` or `:merged` (`#keep_manifest`, through `ManifestStanding.keep`: `Row#manifest` is its line, `would keep its manifest` from the plan's dry run, appended to the row's status); a bumped bundle gets it from its Installer. `ShippedUpdate::Row#replaced` is what the upgrade copies to the trash (`ShippedUpdate.trash_bound`: `Trash.edits` / `Trash.edit?` against the shipped source over the hooks `AssetInstaller` installs or prunes and the rules, plugin and scripts folders, no recorded sha counting as edited), the same rule and files as `AssetInstaller`'s copies. `ShippedUpdate.external` is the rest: what `chi update` only checks |
|
|
61
63
|
| `InstalledCopy`, `BundleCommand::InstalledCheck` | `chi update`'s re-check of the bundles chi didn't ship ([docs/cli.md](../cli.md#updating)). `InstalledCopy` lays a record and its installed files out as a source folder (manifest.yml from `Manifest.write`, the files' own shas, memories from `bases/`; its `files:`, `hooks:`, `plugin:` and `scripts:` are always what the record and the copy hold, so a memory the install left out, one with no base or a manifest hook the record lacks gives no `missing-file`, and every other key is the kept `manifest.yml`'s when `InstalledManifest` read it, so the check sees its unknown keys, else the mirror's; a kept key that didn't parse keeps the value `InstalledManifest` fell back to, since `InstalledCheck` already names its problem); `InstalledCheck` forks per bundle (a plugin's `exit` or hang is that bundle's row; stdio to /dev/null, so `--json` stays parseable; `TIMEOUT`, then KILL of its group) and runs `Check.new(dir, installed: true)` there, which leaves out what only suits a source tree. Its `boundary` finding is narrower there than in a source tree: `Testing::Boundary.gone?` calls a reference gone only when its constant doesn't resolve or the object has no method the name names, so a bundle reaching `Samagotchi::Log` gets no row. lib loads lazily, so when a name doesn't resolve `gone?` requires every file of this chi's library in that same child and asks again (about 0.2 s, only for a bundle that reaches into chi at all); anything it can't read — a `const_get`, a require of a file that is there — is taken as still there. The fork is the point: `Sandbox` swaps ENV and the plugin's code is arbitrary. `UpdateCommand#external_rows` makes the rows (`level`, never `failed`) and holds the check back while a newer gem is yet to take over (`@not_checked`), since a fork is this chi's code |
|
|
62
|
-
| `Uninstaller`, `Trash`, `Status`, `Listing` | remove (the bundle's own folder too: an edited file there is copied to the trash first, `Uninstaller#trash_copy_line`); keep removed files (`Trash#move`) and a copy of overwritten ones (`Trash#copy`, its `as:` keeping a subfolder); `chi bundle status` (per file, `behind`, `merge_note`, `unreadable`; `issues=` counts behind, missing and unreadable, not edited); `chi bundle list` (`Installed#behind`) |
|
|
64
|
+
| `Uninstaller`, `Trash`, `Status`, `Listing` | remove (the bundle's own folder too: an edited file there is copied to the trash first, `Uninstaller#trash_copy_line`); keep removed files (`Trash#move`) and a copy of overwritten ones (`Trash#copy`, its `as:` keeping a subfolder); `Provenance.unchanged?` (the record's checksum, else the base; neither counts as edited) is the one edited rule, read by `prune_dropped_files`, the uninstall's refusal and `chi bundle status`'s `modified`; `chi bundle status` (per file, `behind`, `merge_note`, `unreadable`; `issues=` counts behind, missing and unreadable, not edited); `chi bundle list` (`Installed#behind`) |
|
|
63
65
|
| `IndexUpdater`, `IndexSync` | the managed `index.md` lines (Installer; and the write/edit tools for memories) |
|
|
64
66
|
| `EntrySize` | one memory's size in tokens: `memory.entry_warn_tokens`' crossing note after a memory write, `chi self`'s `big memories` row |
|
|
65
67
|
| `IndexSize` | one scope's memory index as the prompt carries it (tokens, lines, bytes): `memory.index_warn_tokens`' crossing note after a memory write, `chi self`'s summary, the prompt's figures (`SystemPrompt`) |
|
|
@@ -86,6 +88,12 @@ status and diff, `Builder` and `InstalledCopy`. Their requires_chi checks are `I
|
|
|
86
88
|
`Hooks::BundleLoader.load` is given the installed bundle's manifest and uses the same gate. `GuardrailWiring#rules_stamp` stats each bundle's
|
|
87
89
|
`manifest.yml` with its `manifest.json` and `guardrails/*`, so a changed requires_chi re-reads the rules. No reader
|
|
88
90
|
calls `Manifest.new` on a kept file: `Manifest.tolerant` through `InstalledManifest` is the one read.
|
|
91
|
+
A guardrail plugin (`plugin:` `required: true`) is known from `InstalledManifest#plugin_required` before
|
|
92
|
+
`Plugin::Loader.load_bundle` builds it: the install records the flag in the record's `plugin:` (`PluginRef#required`,
|
|
93
|
+
`Provenance#write`'s `plugin_required:`), and the kept manifest's flag is ORed with it, so a missing, edited or
|
|
94
|
+
unparsable kept manifest.yml never drops it (an older chi's `PluginRef` ignores the key):
|
|
95
|
+
its failure goes to the hook failures as required (`ExtensionLoad#load_plugins` passes them as `guardrail_failures:`),
|
|
96
|
+
where the gate denies every tool call; a `chi.on(on_error: :fail_closed)` hook is wrapped `Hooks.wrap(policy: :deny)`.
|
|
89
97
|
The checks live in `Hooks::BundleLoader.load` and `Plugin::Loader.load_bundle`; the loading itself is
|
|
90
98
|
`BundleLoader.load_hook_file` (one hook file, into a given `namespace:`) and `Plugin::Loader.register_file` (one
|
|
91
99
|
plugin file: build, `register`, `commit!`), which raise instead of logging: the bundle test kit
|
|
@@ -122,8 +130,8 @@ The order matters: warnings print in it, and a plugin/script the manifest names
|
|
|
122
130
|
`#source_change` (the summary's first line, `Source changes: old → new`). A source `SourceIdentity` can't place
|
|
123
131
|
(an older record's relative path) never refuses.
|
|
124
132
|
3. `install_memories`, per memory (`MemoryFiles.select`: a non-empty `files:` list, else every top-level `*.md` but
|
|
125
|
-
README, CHANGELOG, LICENSE and the like; a listed name not shipped warns; a symlink is skipped with a warning): an upgrade 3-way merges the files the bundle owns (`
|
|
126
|
-
|
|
133
|
+
README, CHANGELOG, LICENSE and the like; a listed name not shipped warns; a symlink is skipped with a warning): each file's `FileState` and its row in `FileActions`' `FORCE`, `UPGRADE` or `INSTALL` table (`#memory_command`) say what happens, done by `#write_memory`: an upgrade 3-way merges the files the bundle owns (a `:decide` cell through `Merger::Upgrade#decision`: a clean text merge replaces the file through `Merger.write_merged`, and a real conflict, no git or no base leaves it untouched, `note_conflict`, in `#behind`; a fast-forward over an entry this chi marked `conflict`, a file put back by hand, is a `save_first` cell: a copy in the trash first, `#save_first`), a dry run only
|
|
134
|
+
reads the row (`preview_memory`, `would_merge` for a merge, git running on temp copies), a plain install writes new files, takes the new text of an owned file never edited (`#edited?` false, unmarked: D4, an `Updated` as an upgrade's fast-forward) and skips the others, and `--force` overwrites, a file that
|
|
127
135
|
differs copied to the trash first (`Trash#copy`, said by `#trash_copy_line`; a profile's in `InstallResult#trash_copies`). A
|
|
128
136
|
same-name file the bundle didn't write is the user's: skipped and never recorded (`@owned`). Model overlays
|
|
129
137
|
(`<name>.<key>.md`) get no index line (`note_overlay`).
|
|
@@ -131,16 +139,16 @@ The order matters: warnings print in it, and a plugin/script the manifest names
|
|
|
131
139
|
guardrails, plugin, scripts; each set replaces the previous version's, an edited file copied to the trash first. requires_chi warnings for hooks/rules. A
|
|
132
140
|
file that is a symlink or resolves outside the bundle dir (`AssetInstaller#linked?`, a linked `hooks/` too) is
|
|
133
141
|
skipped with a warning (a hook or rule) or raises `InstallError` (the plugin, a script).
|
|
134
|
-
5. An upgrade's `prune_dropped_files`: unchanged → trash; edited → kept (`kept_pruned`); owned by another bundle → kept.
|
|
135
|
-
6. `verify_memory_checksums` (strict), `warn_missing_needs`.
|
|
136
|
-
7. `write_provenance` (manifest, not a dry run):
|
|
142
|
+
5. An upgrade's `prune_dropped_files`: each dropped file's `FileState` (`shipped: false`) and its `UPGRADE` / `FORCE` row: unchanged (`#edited?` false) → trash; edited → kept (`kept_pruned`); owned by another bundle → kept.
|
|
143
|
+
6. `verify_memory_checksums` (strict: a memory whose action takes the bundle's text, `Action#takes_source?`: installed, forced, an upgrade's or a re-install's fast-forward, or merged into a local edit; it checks the source file the write or merge takes its bytes from, so a dry run checks what it would bring in; `Remerge` doesn't check), `warn_missing_needs`.
|
|
144
|
+
7. `write_provenance` (manifest, not a dry run): each owned file's base comes from its action's `next_base`
|
|
137
145
|
(`Installer#next_base`): the file on disk, which has the bundle's bytes; the incoming file for a merge, since the merged
|
|
138
146
|
file holds the user's edit and taking it would make current == base, so the next upgrade would fast-forward over the
|
|
139
|
-
edit (the edited signal stays
|
|
147
|
+
edit (the edited signal stays `Provenance.unchanged?` false, the checksum else the base, which `chi bundle status` and uninstall's refusal read); and where the user's edit was
|
|
140
148
|
kept (keep, conflict, whose `FileStanding::Behind` goes in `marks:`; a file the bundle dropped, kept_pruned, and a re-install's skipped edit take the same path by their
|
|
141
|
-
|
|
149
|
+
action's `:recorded`): those keep the old base, so the next upgrade still sees the edit. One with no base stays without one (a nil in `Provenance#write`'s `files:`, which keeps the
|
|
142
150
|
entry and its recorded checksum): taking the user's file would let the next upgrade fast-forward over it. A plain
|
|
143
|
-
install carries the `conflict` mark of a file it skipped (`held_conflict_marks`), a newer source moving `version`
|
|
151
|
+
install carries the `conflict` mark of a file it skipped (`held_conflict_marks`: the `:hold_mark` cells), a newer source moving `version`
|
|
144
152
|
included; it goes once the file has the incoming bytes. A plain install also keeps the record (the old base) of a file an earlier install
|
|
145
153
|
owned and this version doesn't ship, while it is on disk (`dropped_owned_files`), so the next upgrade prunes it.
|
|
146
154
|
8. `detect_placeholders`.
|
|
@@ -155,13 +163,15 @@ doesn't own: the Installer's `MemoryFiles.select`; `ShippedUpdate.forecast`' eve
|
|
|
155
163
|
can name a file dropped from `files:` or now a symlink, which the upgrade doesn't merge: edited, it stays (`kept_pruned`, or `kept_shared` when another bundle has it too); `SystemBundle`'s dry
|
|
156
164
|
run the manifest's `files:`, so a manifest with none plans nothing (the shipped one lists its files).
|
|
157
165
|
|
|
158
|
-
Results are `@results` (file → `
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
166
|
+
Results are `@results` (file → `Installer::FileResult`: its `word`, a memory's `FileState` and action, a `note`),
|
|
167
|
+
summarised by `#summary` by word: a file that already holds the bundle's bytes (`:up_to_date`, a memory or a
|
|
168
|
+
hook/rule/plugin/script, in a dry run as in the real run) is listed as `Up to date:`, and only the files really left
|
|
169
|
+
out (`:kept_own`, a memory's dry-run `:would_skip`) as `Skipped:`. A memory's dry-run word is its action's through
|
|
170
|
+
`Installer::DRY_WORDS`. A dry run's `would_update` (a hook, plugin or script an upgrade would change) is `Would update:`; an asset
|
|
171
|
+
row is never `would_skip`. A plain install over a bundle already installed from the same source ends with `NAME was already
|
|
162
172
|
installed from SOURCE: its hooks, rules, plugin and scripts now match it and its record was rewritten`, plus
|
|
163
173
|
`; your own copies of its memories were kept (chi bundle upgrade NAME merges the bundle's changes into them)` when
|
|
164
|
-
a memory was left out as
|
|
174
|
+
a memory was left out as the user's (`:kept_own`: an edited one, or one the bundle doesn't own; a never-edited one takes the new text and isn't named there). An upgrade with conflicts keeps an
|
|
165
175
|
owned source dir alive (`#source_dir`) until `#cleanup_source!`, because the conflict agent reads the incoming
|
|
166
176
|
files; `BundleCommand#settle_conflicts` then runs `Provenance#resolve_conflicts`, which moves a base forward and keeps
|
|
167
177
|
the entry's description, only for a file the agent step changed that holds no markers (`Merger.markers?`), and whose incoming
|
|
@@ -114,9 +114,10 @@ Context window lookup (`ContextWindow.resolve`, the first that answers wins; the
|
|
|
114
114
|
chat host has no `/props`, so the chat loop and the engine skip this step for it.
|
|
115
115
|
2. The host's model list (the chat adapter's `#context_window`, e.g. a provider's `context_length`; `:model_list`).
|
|
116
116
|
3. The config file's `models.<key>.window_tokens` (by the model's lookup names, as sampling and vision look a model
|
|
117
|
-
up; `:model_setting`), else `hosts.<name>.window_tokens` (`:host_setting`). `ContextWindow.setting`
|
|
118
|
-
turn for the effective model, after the hooks that may switch it.
|
|
119
|
-
4. `context.window_tokens`:
|
|
117
|
+
up; `:model_setting`), else `hosts.<name>.window_tokens` (`:host_setting`). `ContextWindow.setting(lookup)` (the
|
|
118
|
+
effective model's `ModelLookup`) reads it per turn for the effective model, after the hooks that may switch it.
|
|
119
|
+
4. `context.window_tokens`: `--context-window-tokens`, `SAMAGOTCHI_CONTEXT_WINDOW_TOKENS` or the config file (`:cli`,
|
|
120
|
+
`:env` or `:config`).
|
|
120
121
|
5. `ContextWindow::DEFAULT_TOKENS`, 256000 (`:default`).
|
|
121
122
|
|
|
122
123
|
Settings (3 and 4) only fill in when the server and its list report nothing (a server that is down, a chat host whose list has no window). A
|