samagotchi 0.155.0 → 0.156.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -1
- data/bin/chi +1 -1
- data/docs/architecture.md +10 -6
- 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/engine.md +625 -935
- data/docs/internals/llm-context-forget.md +8 -3
- data/docs/internals/objectives.md +2 -2
- data/docs/internals/prompt-caching.md +4 -1
- data/docs/internals/remote-sessions.md +3 -1
- data/docs/internals/working-set.md +1 -1
- data/docs/sessions.md +2 -2
- data/docs/system-one.md +3 -2
- data/lib/samagotchi/bootstrap/config_writer.rb +1 -1
- data/lib/samagotchi/broadcast_command.rb +3 -3
- data/lib/samagotchi/bundle_command/try/sandbox.rb +1 -1
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/child_move.rb +7 -51
- data/lib/samagotchi/children_status.rb +1 -1
- data/lib/samagotchi/cli/parent_wait.rb +1 -1
- data/lib/samagotchi/config.rb +17 -112
- data/lib/samagotchi/context_command.rb +1 -2
- data/lib/samagotchi/context_window.rb +3 -3
- data/lib/samagotchi/desktop/macos.rb +1 -1
- data/lib/samagotchi/engine/context_judge.rb +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/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 +14 -9
- data/lib/samagotchi/guardrails/targets.rb +24 -9
- data/lib/samagotchi/host_config.rb +72 -1
- data/lib/samagotchi/host_model.rb +2 -6
- data/lib/samagotchi/host_registry.rb +1 -1
- data/lib/samagotchi/kernel_loop.rb +46 -121
- data/lib/samagotchi/llm/anthropic_wire.rb +2 -2
- data/lib/samagotchi/llm/chat_loop.rb +34 -18
- 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 +7 -9
- data/lib/samagotchi/llm_context_view.rb +9 -0
- data/lib/samagotchi/model_price.rb +4 -10
- data/lib/samagotchi/model_profile.rb +6 -6
- data/lib/samagotchi/model_settings.rb +37 -2
- data/lib/samagotchi/note_command.rb +1 -2
- data/lib/samagotchi/objectives_audit.rb +4 -3
- data/lib/samagotchi/participants.rb +2 -2
- data/lib/samagotchi/plugin/context.rb +1 -1
- data/lib/samagotchi/plugin/sessions.rb +2 -5
- data/lib/samagotchi/plugin/tool_spec.rb +5 -3
- data/lib/samagotchi/project_env.rb +1 -1
- data/lib/samagotchi/reasoning_history.rb +3 -3
- data/lib/samagotchi/reminder_carry.rb +1 -1
- data/lib/samagotchi/reprefill_meter.rb +45 -0
- data/lib/samagotchi/review_command.rb +1 -1
- data/lib/samagotchi/sampling_settings.rb +3 -3
- data/lib/samagotchi/self_report.rb +7 -7
- data/lib/samagotchi/session.rb +50 -7
- data/lib/samagotchi/session_access/local.rb +7 -5
- data/lib/samagotchi/session_access/remote.rb +17 -3
- data/lib/samagotchi/session_archive_command.rb +2 -2
- data/lib/samagotchi/session_chain.rb +7 -6
- data/lib/samagotchi/session_continue.rb +6 -5
- data/lib/samagotchi/session_delete_command.rb +1 -1
- data/lib/samagotchi/session_manager/delivery.rb +254 -0
- data/lib/samagotchi/session_manager/emptiness.rb +138 -0
- data/lib/samagotchi/session_manager/lifecycle.rb +274 -0
- data/lib/samagotchi/session_manager/listing.rb +189 -0
- data/lib/samagotchi/session_manager/removal.rb +190 -0
- data/lib/samagotchi/session_manager/spawn.rb +242 -0
- data/lib/samagotchi/session_manager/worker_command.rb +157 -0
- data/lib/samagotchi/session_manager.rb +26 -1540
- data/lib/samagotchi/session_metrics.rb +1 -1
- data/lib/samagotchi/sessions_command.rb +1 -1
- data/lib/samagotchi/share_command.rb +1 -1
- data/lib/samagotchi/starting_marker.rb +65 -0
- data/lib/samagotchi/steer_cut_after.rb +3 -3
- data/lib/samagotchi/system_one/recorder.rb +49 -0
- data/lib/samagotchi/terminal_ui/exit_lines.rb +1 -1
- data/lib/samagotchi/terminal_ui.rb +2 -6
- data/lib/samagotchi/testing/sessions.rb +1 -1
- data/lib/samagotchi/thinking.rb +3 -3
- data/lib/samagotchi/token_usage.rb +1 -1
- data/lib/samagotchi/tool_activity.rb +10 -4
- data/lib/samagotchi/tool_declarations.rb +2 -2
- data/lib/samagotchi/tool_output_cut.rb +17 -0
- data/lib/samagotchi/tool_runner.rb +7 -4
- data/lib/samagotchi/toolkit_command.rb +1 -1
- data/lib/samagotchi/tools/delegate.rb +10 -5
- data/lib/samagotchi/tools/delegate_result.rb +4 -4
- data/lib/samagotchi/tools/index_hint.rb +14 -0
- data/lib/samagotchi/tools/registry.rb +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/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +3 -3
- data/lib/samagotchi/web/app.rb +15 -8
- data/lib/samagotchi/web/participant_access.rb +2 -2
- data/lib/samagotchi/web/session_access_routes.rb +3 -3
- data/lib/samagotchi/web/session_control.rb +3 -3
- data/lib/samagotchi/web/session_create.rb +3 -3
- data/lib/samagotchi/web/session_hub.rb +1 -1
- data/lib/samagotchi/web/session_show.rb +3 -3
- data/lib/samagotchi/web/session_summary.rb +3 -4
- data/lib/samagotchi/worker.rb +55 -27
- data/lib/samagotchi/worker_boot.rb +184 -0
- data/lib/samagotchi/worker_signals.rb +1 -1
- data/lib/samagotchi/worker_task_wakes.rb +2 -2
- metadata +13 -1
|
@@ -29,7 +29,7 @@ lets the model forget its own tool outputs. The pieces:
|
|
|
29
29
|
those steps, counted with the forget's own: `protect_steps + 1`) unless the forget keeps lines of it; a keep
|
|
30
30
|
outside the output's lines, of a preview, or holding all of it; an output of `SMALL` (80) chars or less; an output
|
|
31
31
|
with an edit already; an unknown id. What passes is saved as a staged `LLMContextEdit` of kind `:forget` (`by:
|
|
32
|
-
"model"`, the note, `keep`), and the apply rule runs at once as at a request (`KernelLoop#apply_llm_context!`), so
|
|
32
|
+
"model"`, the note, `keep`), and the apply rule runs at once as at a request (`LLMContextApply.apply!`, through `KernelLoop#apply_llm_context!`), so
|
|
33
33
|
the result can say whether the batch goes with the next request or waits for the turn's end. `LLMContextApply`'s
|
|
34
34
|
protect_steps hold covers stale's stubs only: a forget was checked when it was made.
|
|
35
35
|
- **Restore** takes the forget off the entry (`LLMContextEdit.remove`, a new edits Hash); the next request sends the
|
|
@@ -38,8 +38,13 @@ lets the model forget its own tool outputs. The pieces:
|
|
|
38
38
|
- **Which edits are sent** is one predicate, `LLMContextView#sends?(edit)`: applied, and its kind one of the view's
|
|
39
39
|
layers or `:drop`. The view stubs by it and `WorkingSet` decides "still sent" by it. The view's layers are
|
|
40
40
|
`LLMContextStrategy::LAYERS` (`stale`, `forget`), the strategy's words, not `LLMContextEdit::KINDS`: whatever a
|
|
41
|
-
caller passes as the strategy, an edit kind no strategy names never becomes a layer that `
|
|
42
|
-
hand to `LLMContextApply.run!`.
|
|
41
|
+
caller passes as the strategy, an edit kind no strategy names never becomes a layer that `LLMContextApply.apply!` would
|
|
42
|
+
hand to `LLMContextApply.run!`. Both loops build their view with `LLMContextView.for(llm_context)` (the turn's
|
|
43
|
+
strategy, none without one) and apply with `LLMContextApply.apply!(conversation, llm_context, moment:, context:,
|
|
44
|
+
warmup:, emit:, model:)`, module functions that hold no loop state: the run, its `llm_context_apply` log line (with
|
|
45
|
+
the run's model) and its ✂ row (`LLMContextNotice`; none for the warm-up's copy). `KernelLoop#llm_context_view` and
|
|
46
|
+
`#apply_llm_context!` are one-line defaults over them (the native run's settings), `ChatLoop`'s the same over its
|
|
47
|
+
turn's. Under none the view has no layers and, unless an entry holds a drop
|
|
43
48
|
(`LLMContextEdit.drop_on?`, one scan), `#messages` returns the Array it was given.
|
|
44
49
|
- **The user's layer, always on**: an edit of kind `:drop` (`by: "user"`, the working set's drop:
|
|
45
50
|
[working-set.md](working-set.md#steering-drop-and-undrop)) is sent under every strategy, none included, as
|
|
@@ -117,7 +117,7 @@ Bridge's `TurnAccumulator#current_messages`) leaves tool calls out. A move shows
|
|
|
117
117
|
`action: propose` (`change` add, reword, done or drop; `id`; `text`; `note`, why) checks the change against the list
|
|
118
118
|
(`Objectives::Proposal#refusal`: its line through `Objectives::Edit`, the edit the card's Apply runs, so the tool refuses
|
|
119
119
|
what Apply would) and hands it to `Engine#propose_objective` (`TurnStages::Objectives#propose`), which keeps it until the turn is over,
|
|
120
|
-
as `TurnStages::ContextJudge`'s cards are kept. `run_turn`'s ensure (`TurnStages::Objectives#finally`) shows them only for a turn that completed: a canceled or
|
|
120
|
+
as `TurnStages::ContextJudge`'s cards are kept. `run_turn`'s ensure (`Engine#show_objective_proposals`, the cards `TurnStages::Objectives#finally` gives) shows them only for a turn that completed: a canceled or
|
|
121
121
|
failed one drops them, so its Apply card never outlives a turn whose call a
|
|
122
122
|
rollback has taken out of the history, and nothing carries over to the next turn. A card is between turns (no
|
|
123
123
|
`in_turn`), so it writes no `pending_card.json`, the stage doesn't say it waits, and it keeps its buttons over a
|
|
@@ -139,7 +139,7 @@ any call. An add's text can't split on ` | `. The body names the line, for a ter
|
|
|
139
139
|
|
|
140
140
|
`Objectives.view` (`List#to_view`) is what the page draws: the items, each with its last move, and `pending`, whether
|
|
141
141
|
the newest list is a file still waiting. The session read (`objectives`, no key with no list) and the live
|
|
142
|
-
`:objectives_updated` carry it. `Engine#announce_objectives` (`TurnStages::Objectives#announce`) emits when the view differs from the last: after an edit,
|
|
142
|
+
`:objectives_updated` carry it. `Engine#announce_objectives` (under the event lock, `TurnStages::Objectives#announce` giving the event) emits when the view differs from the last: after an edit,
|
|
143
143
|
once a list note is in (`add_context_note`), at a turn's end (its moves) and after a rollback between turns. A session
|
|
144
144
|
with no list emits nothing. Between two tabs that is all it takes. A participant gets neither: the event is in
|
|
145
145
|
`ParticipantView::DROPPED`.
|
|
@@ -222,7 +222,10 @@ is prefilled once. The native and the chat loop use the same plan.
|
|
|
222
222
|
- The debug log's `generation_completed` line: `prompt=` (the request's prompt tokens), `cached=` (read from the
|
|
223
223
|
server's cache), `cache_write=` (written to it, where the provider reports writes) and `reprefill=` (of the last
|
|
224
224
|
request's prompt on the same model through the same host, what was prefilled again; a `/model` to the same bare
|
|
225
|
-
model on another host is the switch's prefill, not a re-prefill). The
|
|
225
|
+
model on another host is the switch's prefill, not a re-prefill). The baseline is the Engine's `ReprefillMeter`
|
|
226
|
+
(`reprefill_meter.rb`), one for both loops: the last request whichever loop sent it, keyed by the model and the
|
|
227
|
+
host's client (`KernelLoop#client`, `ChatLoop#client`), so a switch to another host and back starts over on the
|
|
228
|
+
way back; the turn-end warm-up resets it (`KernelLoop#warmed_up!`). The `http` line of a
|
|
226
229
|
remote Claude or Messages request shows `cache=on` (or `cache=1h`) when it carried breakpoints.
|
|
227
230
|
- The warm-up logs `model warmup` with `cached=`, `prefilled=` and `prefill_ms=`.
|
|
228
231
|
- `/stats`: "cached N (P%)", "cache writes" and "re-prefilled" for the session.
|
|
@@ -121,7 +121,9 @@ the session commands*. Keep that table in step with `SessionAccessRoutes`.
|
|
|
121
121
|
- **Refusals.** `SessionAccessRoutes#refusal_response` (and the page routes that raise) send
|
|
122
122
|
`{error, detail, message, …fields}`: `detail` is the page's wording, `message` is the exception's own text, and the
|
|
123
123
|
fields are those the class is built from (`session_id`, `reason`, `ids`, `busy_id`, `version`).
|
|
124
|
-
`Remote#raise_refusal` rebuilds the class that Local's callee raises: `
|
|
124
|
+
`Remote#raise_refusal` rebuilds the class that Local's callee raises: `Session::NotFound` for a `404 not_found`
|
|
125
|
+
whose detail is `no session X` (or, from chi web 0.155.0 and older, `Session not found: X`), a plain
|
|
126
|
+
`ArgumentError` for another `not_found`, `Session::AmbiguousId`,
|
|
125
127
|
`SessionManager::OwnedByTUI` / `DeleteRefused` / `ArchiveRefused` / `RestartRefused`, `SessionContinue::Refused`,
|
|
126
128
|
and `ModelProfile::MissingModel` for `invalid_model` (no model there, or `--model`'s host unknown there: "could not
|
|
127
129
|
start a session: …", exit 1, as here).
|
|
@@ -103,7 +103,7 @@ pending: [Steering: add and unadd](#steering-add-and-unadd)) (`Engine#emit_worki
|
|
|
103
103
|
- after each `:generation_started`, in `build_stream_event_handler` (`EventFanout`), from the running turn's
|
|
104
104
|
conversation (`Engine#live_conversation`, read on the turn's thread) under the turn's strategy
|
|
105
105
|
(`TurnContext#settings`' `llm_context`; `EventFanout#working_set_llm_context`: the running turn's, the last turn's
|
|
106
|
-
until it has its own). Both loops start a generation after `
|
|
106
|
+
until it has its own). Both loops start a generation after `LLMContextApply.apply!`, so the set is
|
|
107
107
|
what that request sends;
|
|
108
108
|
- once in `Engine#end_turn`, after `replace_session_messages` and before the end event, from the kept messages (the
|
|
109
109
|
turn-end apply has run by then);
|
data/docs/sessions.md
CHANGED
|
@@ -23,7 +23,7 @@ Sessions are plain files — no DB. Each session is `~/.local/state/samagotchi/s
|
|
|
23
23
|
|
|
24
24
|
A session is deleted if **expired by age OR overflow by count** (unless `keep_status` or the live-owner guard: a session a worker or `chi` still has open is never pruned). A session's `status` is its turn state (`idle`/`running`), not whether a worker is alive. Orphan dirs without a `*.json` are never deleted, except skeleton-only ones (see *Empty sessions*). Deletion removes both `*.json` and sidecar dir atomically. Set both `DAYS=0` and `MAX=0` to retain forever.
|
|
25
25
|
|
|
26
|
-
**Worker idle exit:** a background worker (plain `chi`, Web UI sessions) exits after `SAMAGOTCHI_SESSION_IDLE_EXIT_MINUTES` (`session.idle_exit_minutes`, default `30`, `0`=never) with no turn running or queued, no client on its stream (an open web tab or attached terminal keeps it), no reminder registered, no background task it watches for a [finish wake](internals/background-tasks.md#finish-wake) still running or waiting for its wake turn (with `session.task_wakes: wake`; not one whose turn you stopped) and no continue offer (the step-limit question) waiting for an answer. It removes `bridge.json` and frees `owner.lock`; the next prompt or `--attach` wakes a new worker (`WorkerIdleExit`, `
|
|
26
|
+
**Worker idle exit:** a background worker (plain `chi`, Web UI sessions) exits after `SAMAGOTCHI_SESSION_IDLE_EXIT_MINUTES` (`session.idle_exit_minutes`, default `30`, `0`=never) with no turn running or queued, no client on its stream (an open web tab or attached terminal keeps it), no reminder registered, no background task it watches for a [finish wake](internals/background-tasks.md#finish-wake) still running or waiting for its wake turn (with `session.task_wakes: wake`; not one whose turn you stopped) and no continue offer (the step-limit question) waiting for an answer. It removes `bridge.json` and frees `owner.lock`; the next prompt or `--attach` wakes a new worker (`WorkerIdleExit`, `WorkerBoot.run_session_loop`). As its Bridge stops, the worker marks the lock leaving (`OwnerLock#mark_leaving`: `leaving_at`, and `leave_wait`, its `recap.timeout`), since it keeps the lock while it writes the recap on leave: an `--attach` or `--resume` meanwhile (here, or a remote one through chi web's wake) prints `(waiting for the session to finish its recap…)` (`LaunchMode::RECAP_WAIT`) and waits for the release, up to that budget plus `OwnerLock::LEAVE_MARGIN` (5 s), before starting the new worker (`OwnerLock.await_release`). A lock held by a worker that isn't leaving (a live one, a stuck one) is left as before. A client can also ask the worker to exit right away (Bridge `POST /session/:id/exit`; `/exit` in an attached terminal does, `/detach` doesn't): the same rules apply without the timeout (even with `0`), and the asker's own stream doesn't count. The reply names what keeps it up (for background tasks, which ones and how to stop them). When every hold is one the user can stop — reminders, a watched task still running, a delegate with a live worker — the 409 also carries `holds: [{kind:, items: [{id:, label:, detail:}]}]` (a reminder's detail is its schedule, `every 20m`/`every 1h`, `, due now` when it is already due), and an attached terminal asks what to do with them; its "stop them and exit" posts `{"stop": "all"}`, which clears the reminders, stops those tasks (`TaskRuntime.stop_task`, as `task_stop`) and stops those children (`SessionManager.stop_session`) before leaving, re-checking first that nothing the user can't stop appeared meanwhile (`HoldStop`). A delegate counts only for that ask: it never holds the idle exit, and an exit that deletes or archives the session does not ask about it. The session's status stays as it was, not `stopped`. With `"restart": true` (a worker whose sidecar lists the `restart` feature) the worker hands the session to a new worker on the newest chi installed: it leaves, then starts its successor once its lock is free, keeping even an empty session and writing no recap. Clients' streams don't hold a restart (a web tab and an attached terminal reconnect to the new worker); what would be lost does: a turn running or queued, a continue offer, a question or approval waiting, a `/btw` (or another anytime command) still running, a delegate's approval relay open or settled in the last 10 minutes, and background tasks this conversation started that still run (`WorkerIdleExit#hold_for_restart`; 409 `held` with `reason`). Reminders go along: as it leaves, the worker writes them to `<id>/reminders.json` (0600: `session_id`, `written_at`, and each reminder's id, name, description, interval and next fire as wall-clock time; `ReminderCarry`), and its successor reads and deletes the file as it starts, before its idle scheduler runs. A restored reminder keeps its id and schedule; one that came due meanwhile fires once at once, and one further ahead than its interval (a clock that jumped) is due one interval from now. The file counts only for the same session within 2 minutes of `written_at`, and not once the session is stopped: otherwise it is deleted unread (log `reminders_carry_dropped`, at warn unless the session was stopped). A stop that lands while the worker leaves for a restart wins: the worker writes no file, and no successor starts (`WorkerBoot.run_session_loop`, log `restart_dropped`); an archive deletes the file of each session it archives. Once an exit or restart is taken (claimed before a stop-all stops anything), the first one decides how the worker leaves: a later request of the same kind is answered as that exit, one of another kind (a stop-all from another terminal after a restart, chi web's "Restart workers" after a `/exit`) gets 409 `held` with `reason` `leaving_exit`, `leaving_delete` or `leaving_restart`, and a restart says it was refused rather than "restarted". A write that fails keeps the worker up with its reminders (log `reminders_carry_failed`), and the caller reports no new worker. A reminder already due when the restart is asked runs its turn first, so the restart's wait (10 s) may report no new worker yet though the hand-off follows. A worker from before this still refuses a restart for its reminders (409 `reminders`). Only an asked restart carries them: the fallback's TERM (a worker that can't be asked, below), a stop, an idle exit (which reminders hold anyway) and a crash drop them as before. What lives only in the worker's memory goes: the event history a late client replays, the kept `/btw` answers, a plugin's in-memory state.
|
|
27
27
|
|
|
28
28
|
**Empty sessions:** a session nothing happened in is deleted when it is left, so `chi` → `/exit` leaves no trace. Empty means: no messages, no turn tried (a failed turn leaves a `last_prompt` and keeps it), no memory used, no pending question, nothing in `input/`, `notes/` or `images/`, nothing in its dir beyond the worker's skeleton, mode `assist`, and the default model. A session on another model (`/model`, `--model`) counts as prepared and stays (`SessionManager.discardable?`). The worker checks as it idle-exits or leaves on a client's request and deletes it once its lock is free, checking again first. The attached `/exit` then says `Detached; the session is empty: it will be discarded if nothing arrives before its worker stops.` (a note or prompt landing first keeps it). The retention sweep catches those killed first: empty sessions over an hour old with no live owner, whatever `DAYS`/`MAX`, and skeleton-only dirs with no `*.json`. `session.keep_empty: true` (env `SAMAGOTCHI_SESSION_KEEP_EMPTY`) turns all of it off.
|
|
29
29
|
|
|
@@ -345,7 +345,7 @@ The title names the session wherever its first message did: `chi sessions list`
|
|
|
345
345
|
|
|
346
346
|
A session can **continue** an earlier one: "the next run of the same routine" (a daily coordinator, say, continued once a day or several times). The new session's `continues` field names the previous link; the chain is derived from it, never stored: follow `continues` back for the earlier links, forward for the later ones. It is not a parent: archiving one link leaves the next alone, and lists fold delegates, not links. A link is continued at most once, so a chain has no forks and its latest link is the one nobody continues.
|
|
347
347
|
|
|
348
|
-
`
|
|
348
|
+
`SessionContinue.run` makes the next link:
|
|
349
349
|
|
|
350
350
|
- **What it copies:** the previous link's folder, its model (the name it was typed as, so an alias holds) unless the continue names one (`--model`, the web's `model`), and its own LLM context values. A model given is checked like a new session's (a host config.yml doesn't have refuses the continue, nothing archived). An inherited model whose host can't run it any more (gone from config.yml, `enabled: false`, an alias naming another host) **falls back to the default model** with a warning, `gone:org/model can't run (unknown host 'gone' in model …); started on <default>`, decided before the archive; with no default model configured the continue is refused. The carry note doesn't mention the model. The new link carries no transcript beyond the note's last answer, so a different model, or another provider, starts clean there. Its preview is the previous link's (until its first message is saved), unless it starts with a message. Its title (`/title`) is the previous link's, with or without a first message.
|
|
351
351
|
- **The carry note:** the new link starts with a context note from `session chain` (shown in its transcript as `note from session chain`), queued before its worker spawns, so its first turn sees it: `This session continues 3fa2c1d0 (2026-10-08 09:12), the previous link of its chain. Its recap, as of 2026-10-08 18:02:` and the previous link's recap (`recap.json`; cut at 8000 characters). No recap (fewer user turns than a recap needs, recaps off, none written in time): `… It left no recap.` Then `Its last answer:` and the previous link's last answer, read after the recap wait, so its exact shas, paths and next steps survive: a longer one keeps its last 4000 characters, after a `…` (a wrap-up closes on what comes next). There is none when the newest bubble was a step that asked for tools, when a later turn failed or was cut before it answered, and on a review link (a continue doesn't copy the grants, so the next link's participants could differ); `session.carry_last_answer: false` leaves it out of every note. No other transcript. The note never makes a continue fail: it stays within the 16 KiB a note may hold, the answer taking what the head and recap leave, and a session file that can't be read gives no answer. An idle new link holding only the note is kept, not discarded as empty.
|
data/docs/system-one.md
CHANGED
|
@@ -10,8 +10,9 @@ was sent and what came of it. The runs don't leave the machine; a hosted System
|
|
|
10
10
|
## The common layer
|
|
11
11
|
|
|
12
12
|
`SystemOne::Client` asks. `SystemOne::Recording` wraps it where a use builds its
|
|
13
|
-
client (`
|
|
14
|
-
|
|
13
|
+
client (`SystemOne::Recorder#wrap`, over the recorder `Engine#system_one_recorder` builds
|
|
14
|
+
for each call: the Engine's runs and its model then; `Engine#system_one_recording` for the
|
|
15
|
+
guardrails): the same duck type, the same answers and the same raises. A cancelled ask saves its run, then raises; a run that can't be saved is
|
|
15
16
|
one warn and never changes the answer. A use that needs the run's id at once passes it
|
|
16
17
|
(`client.ask(..., run_id: id)`); one client serves several threads.
|
|
17
18
|
|
|
@@ -304,7 +304,7 @@ module Samagotchi
|
|
|
304
304
|
|
|
305
305
|
def written_ok?(real, expected, name)
|
|
306
306
|
parse(File.read(real)) == expected &&
|
|
307
|
-
|
|
307
|
+
HostConfig.all(env: @env, path: real).key?(name.downcase)
|
|
308
308
|
rescue StandardError
|
|
309
309
|
false
|
|
310
310
|
end
|
|
@@ -348,11 +348,11 @@ module Samagotchi
|
|
|
348
348
|
# The full id +given+ names (an id or the start of one), or nil after
|
|
349
349
|
# saying why there is none.
|
|
350
350
|
def resolve_session(given)
|
|
351
|
-
id = Session.
|
|
352
|
-
Session.load(id, state_dir: @state_dir) #
|
|
351
|
+
id = Session.resolve!(given, state_dir: @state_dir)
|
|
352
|
+
Session.load(id, state_dir: @state_dir) # a file that won't load is refused here too
|
|
353
353
|
id
|
|
354
354
|
rescue ArgumentError => e
|
|
355
|
-
error_line("chi broadcast: #{e.
|
|
355
|
+
error_line("chi broadcast: #{e.message}")
|
|
356
356
|
nil
|
|
357
357
|
rescue SystemCallError => e
|
|
358
358
|
error_line("chi broadcast: session #{given} can't be read: #{e.message}")
|
|
@@ -130,7 +130,7 @@ module Samagotchi
|
|
|
130
130
|
|
|
131
131
|
# Stops what the run started that outlives its child chi: the
|
|
132
132
|
# workers of its delegates (they leave the child's process tree,
|
|
133
|
-
#
|
|
133
|
+
# WorkerBoot.detach_from_spawner) and its tasks (a group of
|
|
134
134
|
# their own each, reparented once the child exits). Found by their
|
|
135
135
|
# records in the sandbox, so nothing outside it: a session's lock
|
|
136
136
|
# held by a group leader, a running task's record whose pid still
|
|
@@ -55,8 +55,8 @@ Only the nested form is read. An env name used as a top-level key (`SAMAGOTCHI_D
|
|
|
55
55
|
**Excluded maps** (YAML-only, not part of the flat registry; skipped by scalar loader):
|
|
56
56
|
|
|
57
57
|
- `model_aliases:` map of alias → model id (`ModelRef.parse`, through `HostRegistry#resolve`). Keys lowercased on write (`ConfigFile.write_model_alias!`). Values may be bare `model` or qualified `host:model` (hybrid; only `:` names a host, `org/model` is an id). Aliases apply once: never point an alias at another alias.
|
|
58
|
-
- `hosts:` map of `name → {host, port | url, api, api_key_env, auth, dialect, profile, first_token_timeout, vision, sampling, thinking, enabled, models}` (`
|
|
59
|
-
- `models:` map of model id or alias → `{profile, vision, sampling, thinking, reasoning_history}` (`
|
|
58
|
+
- `hosts:` map of `name → {host, port | url, api, api_key_env, auth, dialect, profile, first_token_timeout, vision, sampling, thinking, enabled, models}` (`HostConfig.all`, `host_registry.rb` `HostEntry`). Names lowercased; `url:` (http/https, optional path) replaces host/port, never both; `api_key_env:` names the env var holding the API key (never write a key into config.yml); `auth:` (`bearer|x-api-key`) is the header the key goes in (default `x-api-key` on `api: anthropic`, Bearer elsewhere); `dialect:` (`fireworks`, or `standard` for the default; `api: anthropic` only) adjusts the Messages wire for that provider (Bearer by default, no thinking `display`, no `cache_control`, `x-session-affinity`; `LLM::Dialect`, docs/configuration.md "Anthropic Messages hosts"); `first_token_timeout` (seconds, `0` = off; a negative or non-number warns and is ignored) overrides `server.first_token_timeout` for that host; `models:` (a map of model ids, or a plain list) declares ids the host serves whatever its `/v1/models` lists: no "doesn't list" warning, and a bare id routes there as if listed (`HostModel`); an entry may hold `price: {input, output, cache_read?, cache_write?}` in USD per 1M tokens (`ModelPrice`; an unset cache rate is the input rate), which estimates a cost the provider doesn't report or reports as 0 (`cost_estimate`, shown as `~$`), and `served: [ids] | any`, the models a gateway may answer that id with (no served-model warning for them); model settings stay in the root `models:`; a worker reads `hosts:` from the file as it starts (no environment variable sets hosts).
|
|
59
|
+
- `models:` map of model id or alias → `{profile, vision, sampling, thinking, reasoning_history}` (`ModelSettings.all`). `thinking:` (here, on a host, or `thinking.level` for every model) is `off|low|medium|high|default` (`Thinking`; unquoted `off` works, `on` is not a level; `default` = send nothing). Precedence: the session's own level (`/thinking LEVEL`, saved in the session; `chi --thinking` on a session run, `chi send --new --thinking`) > `chi web --thinking`/`SAMAGOTCHI_THINKING_LEVEL` > `models:` > `hosts.<name>.thinking` > `thinking.level` in config.yml > `default`. A `sampling:` key wins over the fields a level sends (`null` drops one); `docs/configuration.md` "Thinking". `reasoning_history:` (here or on a host) is `drop|turn|all`, which of its reasoning a chat host's model gets back. Precedence: the session's own level (`/reasoning-history LEVEL`, saved in the session) > `SAMAGOTCHI_REASONING_HISTORY` > `models:` > `hosts.<name>.reasoning_history` > `turn`; `docs/configuration.md` "Reasoning history". `/thinking` and `/reasoning-history` ask before a change that restarts the prompt cache; `--yes` applies it (`chi send` commands apply directly). `sampling:` (here or on a host) is a map of request fields passed to the provider as written (`temperature`, `top_p`, `presence_penalty`, `repeat_penalty`, …; a model's fields win over its host's per field; `null` = don't send; chi's own fields like `max_tokens`/`stream` are refused with a warning; `docs/configuration.md` "Sampling"). Keys match case-insensitively. `profile` (here or on a host) is `qwen36|gemma4`: the raw prompt format for native hosts. Precedence: `--profile`/`SAMAGOTCHI_MODEL_PROFILE` > `models:` > `hosts.<name>.profile` > the llama.cpp server's chat template > the name (`qwen`/`gemma`) > `qwen36` (`ModelProfile.resolve`). Set one when a model's name hides its family (e.g. a Qwen fine-tune under another name whose server publishes no template).
|
|
60
60
|
- `hooks:` map of `hooks_dir` + per-event lists `{path, on_error}` (`Hooks::Loader.load`). `hooks_dir` may start with `~`.
|
|
61
61
|
- `guardrails:` tool-call rules (`docs/guardrails.md`; read in `Engine#guardrail_rules`, parsed by `Guardrails::Rules.parse`): `enabled` (bool, default true; `false` drops rules and hooks' asks, a deny still applies; config.yml only, like every `guardrails` setting: no env variable), `parent_answers` (`soft`, the default: a parent agent's `chi answer` may deny, answer, Continue and give "Allow once"; `standard`: no allow; `hard`: deny, Stop and plain answers only; never a wider scope; replaces `parent_approvals` and `turn.parent_continue`; a convention for an honest parent, not a security boundary, and the user's choice: set it only when the user asks), `rules:` (list), `disable:` (a rule id or a list of them, `id` or `bundle:id`, switching off a bundle's or config rule without editing it), `small_models` (`auto`, the default: small = 32B or less read from the name; a glob or list of globs; `[]` for none). A rule takes only `id`, `tool`, `command`, `path`, `git`, `models`, `verdict`, `reason`, `scopes`: `id` required; at least one of `tool` (a name, `shell` = execute + task_create, a `File.fnmatch` glob like `"mcp_*"`, or a list), `command` (a Ruby regex on the shell command), `path` (a glob, or `outside_repo`: outside the session's repo, tmp folders and memory files excepted), `git` (only `outside_repo`: a shell call runs commit/add/reset/… in another checkout); `models` (optional: `small`, a model-name glob, or a list; without it the rule is for every model); `verdict` `ask|deny`; `scopes` (for `ask`) a subset of `once, session, repo, rule`. **Any parse error (an unknown key, a bad regex, no verdict) makes chi deny every tool call** until fixed, so validate with `YAML.safe_load` and keep the list shape. Rules are read again on the next tool call after config.yml or a bundle's rules change: no restart needed. Installed bundles' rule files (`chi bundle install guardrails`) add to them; `/guardrails` lists what loaded.
|
|
62
62
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: samagotchi-system
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.156.0
|
|
4
4
|
scope: system
|
|
5
5
|
description: Default system memories — identity, self map, config modification protocol, memory guide and the delegated-session rules
|
|
6
6
|
files:
|
|
7
7
|
identity.md: sha256:29c2f7df9f6c5acbc5ec17000a4490741e55e426af15ca0d81c8706edd9bd027
|
|
8
8
|
self_map.md: sha256:709bd52710bf6c50334a89a10742ea1554c5985cc2d7c4e7571e526cc4479526
|
|
9
|
-
config_modification_protocol.md: sha256:
|
|
9
|
+
config_modification_protocol.md: sha256:25ed2d4901f00eeea226db5eb2b75a05d6cdee4015ac8daa4bed624f745c3922
|
|
10
10
|
memory_guide.md: sha256:8b7727b939b9c04486ca59859561023dcc319a73e0f5b47b3fcf5741edaa69e2
|
|
11
11
|
delegated.md: sha256:a0d6ddcd4a8be06526d964898611775fd251cfd9d145a9d16afaccaa503597bd
|
|
@@ -18,27 +18,22 @@ module Samagotchi
|
|
|
18
18
|
autoload :SessionContinue, File.expand_path("session_continue", __dir__)
|
|
19
19
|
|
|
20
20
|
# A continue moving the previous link's open delegates to the chain's new
|
|
21
|
-
# link (
|
|
21
|
+
# link (SessionContinue.run), instead of archiving them with
|
|
22
22
|
# it. A moved child gets a parent override (Session::PARENT_FILE), never a
|
|
23
23
|
# rewritten session file, so its live worker saving a stale copy changes
|
|
24
24
|
# nothing; its worker reads the override for its rings and at each turn's
|
|
25
25
|
# start (Bridge::FEATURES "reparent").
|
|
26
26
|
#
|
|
27
27
|
# #apply runs between the new link's save and its worker's spawn, while
|
|
28
|
-
# the link is
|
|
29
|
-
# second worker from the child's process until the link's
|
|
30
|
-
# Each step is idempotent,
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
28
|
+
# the link is marked starting (StartingMarker): a ring from a moved child
|
|
29
|
+
# must not spawn a second worker from the child's process until the link's
|
|
30
|
+
# own holds it. Each step is idempotent, and an intent file (INTENT_FILE)
|
|
31
|
+
# in the new link's folder names the move until it is done: a worker that
|
|
32
|
+
# starts and finds one finishes the move (#adopt), and a failed continue
|
|
33
|
+
# moves the same children back.
|
|
34
34
|
module ChildMove
|
|
35
35
|
# <new link dir>/move.json: {from, ids, noted}, while a move is under way.
|
|
36
36
|
INTENT_FILE = "move.json"
|
|
37
|
-
# <new link dir>/starting: {pid, at}, from the move to the worker's spawn.
|
|
38
|
-
STARTING_FILE = "starting"
|
|
39
|
-
# A starting marker older than this, or whose process is gone, is left
|
|
40
|
-
# over from a continue that died: it holds nothing back.
|
|
41
|
-
STARTING_STALE = 60.0
|
|
42
37
|
# Who the note to a moved child is from (ContextNote's label).
|
|
43
38
|
NOTE_SOURCE = "session chain"
|
|
44
39
|
# What a live child's worker must name in its sidecar to be moved.
|
|
@@ -206,44 +201,5 @@ module Samagotchi
|
|
|
206
201
|
end
|
|
207
202
|
SessionInbox.write_note(child_id, text: text, source: NOTE_SOURCE, state_dir: state_dir)
|
|
208
203
|
end
|
|
209
|
-
|
|
210
|
-
# Hold off a worker for the continue's new link (#starting?) until its
|
|
211
|
-
# own worker holds it: written before the move, removed by that worker
|
|
212
|
-
# once it has the owner lock (Worker#start).
|
|
213
|
-
def mark_starting(session_id, state_dir:)
|
|
214
|
-
dir = Session.session_dir(session_id, state_dir: state_dir)
|
|
215
|
-
FileUtils.mkdir_p(dir)
|
|
216
|
-
AtomicFile.write(File.join(dir, STARTING_FILE), JSON.generate({ "pid" => Process.pid, "at" => Time.now.iso8601(3) }))
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
def clear_starting(session_id, state_dir:)
|
|
220
|
-
FileUtils.rm_f(File.join(Session.session_dir(session_id, state_dir: state_dir), STARTING_FILE))
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
# Whether a continue is moving delegates to +session_id+ right now: its
|
|
224
|
-
# marker is there, fresh, and the process that wrote it runs.
|
|
225
|
-
def starting?(session_id, state_dir:)
|
|
226
|
-
file = File.join(Session.session_dir(session_id, state_dir: state_dir), STARTING_FILE)
|
|
227
|
-
return false unless File.exist?(file)
|
|
228
|
-
|
|
229
|
-
data = JSON.parse(File.read(file))
|
|
230
|
-
return false if Time.now - Time.iso8601(data["at"].to_s) > STARTING_STALE
|
|
231
|
-
|
|
232
|
-
process_alive?(data["pid"].to_i)
|
|
233
|
-
rescue JSON::ParserError, ArgumentError, SystemCallError
|
|
234
|
-
false
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
# Signal 0 only asks whether the process exists.
|
|
238
|
-
def process_alive?(pid)
|
|
239
|
-
return false unless pid.positive?
|
|
240
|
-
|
|
241
|
-
Process.kill(0, pid)
|
|
242
|
-
true
|
|
243
|
-
rescue Errno::ESRCH
|
|
244
|
-
false
|
|
245
|
-
rescue Errno::EPERM
|
|
246
|
-
true
|
|
247
|
-
end
|
|
248
204
|
end
|
|
249
205
|
end
|
|
@@ -113,7 +113,7 @@ module Samagotchi
|
|
|
113
113
|
# session counts loaded itself.
|
|
114
114
|
def summary_of(session, state_dir:)
|
|
115
115
|
live = WorkerLink.resolve(Session.session_dir(session.id, state_dir: state_dir), unlink: false).worker_owned?
|
|
116
|
-
{ live: live, busy: live && session.status == Session::STATUS_RUNNING,
|
|
116
|
+
{ live: live, busy: live && session.status == Session::STATUS_RUNNING, saved_status: session.status,
|
|
117
117
|
updated_at: session.updated_at, waiting: session.waiting_question(live: live)&.dig(:kind),
|
|
118
118
|
preview: session.last_prompt.to_s }
|
|
119
119
|
end
|
|
@@ -55,7 +55,7 @@ module Samagotchi
|
|
|
55
55
|
def resolve(given)
|
|
56
56
|
@session_id = @access.resolve(given)
|
|
57
57
|
rescue ArgumentError => e
|
|
58
|
-
error_line("#{command_name}: #{e.
|
|
58
|
+
error_line("#{command_name}: #{e.message}")
|
|
59
59
|
nil
|
|
60
60
|
end
|
|
61
61
|
|
data/lib/samagotchi/config.rb
CHANGED
|
@@ -8,11 +8,14 @@ require_relative "config_text_edit"
|
|
|
8
8
|
require_relative "log"
|
|
9
9
|
require_relative "model_ref"
|
|
10
10
|
require_relative "paths"
|
|
11
|
-
require_relative "llm_context_strategy"
|
|
12
|
-
require_relative "host_model"
|
|
13
11
|
require_relative "config/restart_watch"
|
|
14
12
|
|
|
15
13
|
module Samagotchi
|
|
14
|
+
# Which LLM context strategy a turn runs under; it reads Config.
|
|
15
|
+
autoload :LLMContextStrategy, File.expand_path("llm_context_strategy", __dir__)
|
|
16
|
+
# A hosts.<name>.models entry and its price; they read ConfigFile.
|
|
17
|
+
autoload :HostModel, File.expand_path("host_model", __dir__)
|
|
18
|
+
autoload :ModelPrice, File.expand_path("model_price", __dir__)
|
|
16
19
|
# Parses the thinking: levels of host and model entries (it needs Config).
|
|
17
20
|
autoload :Thinking, File.expand_path("thinking", __dir__)
|
|
18
21
|
# One hosts: entry; it needs ConfigFile's readers.
|
|
@@ -89,6 +92,12 @@ module Samagotchi
|
|
|
89
92
|
next_history_access next_matching_turn_display_poll_write
|
|
90
93
|
].freeze
|
|
91
94
|
|
|
95
|
+
# llm_context.policy's default: the sentence forget_outputs' description
|
|
96
|
+
# carries (ToolDeclarations::FORGET_OUTPUTS_POLICY), the plan's D7 line,
|
|
97
|
+
# adapted from CLM's steering example.
|
|
98
|
+
DEFAULT_LLM_CONTEXT_POLICY = "Tidy at subtask boundaries: once a subtask is done, forget its tool outputs and note " \
|
|
99
|
+
"what it established; keep anything you'll still edit against."
|
|
100
|
+
|
|
92
101
|
ENTRIES = [
|
|
93
102
|
# universal – env+config+cli
|
|
94
103
|
Entry.new(key: "default.model", yaml_path: %w[default model], type: :string, default: nil, expose: %i[env config cli], lifecycle: :new_session),
|
|
@@ -261,7 +270,7 @@ module Samagotchi
|
|
|
261
270
|
# and hosts.<name>.llm_context_budget_tokens come first. Kept as written: LLMContextStrategy.parse_budget reads it.
|
|
262
271
|
Entry.new(key: "llm_context.budget_tokens", yaml_path: %w[llm_context budget_tokens], type: :string, default: nil, expose: %i[env config], lifecycle: :next_matching_turn_display_poll_write),
|
|
263
272
|
# The policy sentence forget_outputs' description carries (the forget layer); blank: none.
|
|
264
|
-
Entry.new(key: "llm_context.policy", yaml_path: %w[llm_context policy], type: :string, default:
|
|
273
|
+
Entry.new(key: "llm_context.policy", yaml_path: %w[llm_context policy], type: :string, default: DEFAULT_LLM_CONTEXT_POLICY, expose: %i[env config], lifecycle: :worker_start),
|
|
265
274
|
|
|
266
275
|
# How much models think: off|low|medium|high|default, parsed by Thinking
|
|
267
276
|
# (a :string, since YAML reads an unquoted off as false). The CLI flag
|
|
@@ -399,7 +408,7 @@ module Samagotchi
|
|
|
399
408
|
# appended_prompt_files, Hooks::Loader, ConfigFile.bundle_settings.
|
|
400
409
|
FREE_FORM_MAPS = %w[model_aliases hooks bundles memories append_system_prompt].freeze
|
|
401
410
|
# Maps of named entries and the keys an entry may hold
|
|
402
|
-
# (
|
|
411
|
+
# (HostConfig.all, ModelSettings.all).
|
|
403
412
|
MAP_ENTRY_KEYS = {
|
|
404
413
|
"hosts" => %w[host port url api api_key_env auth dialect profile first_token_timeout vision sampling thinking
|
|
405
414
|
reasoning_history enabled remote window_tokens steer_cut_after llm_context_strategy llm_context_apply llm_context_budget_tokens models].freeze,
|
|
@@ -408,6 +417,8 @@ module Samagotchi
|
|
|
408
417
|
}.freeze
|
|
409
418
|
# The keys a hosts.<name>.models entry may hold (HostModel.parse_map).
|
|
410
419
|
HOST_MODEL_KEYS = %w[price served].freeze
|
|
420
|
+
# The keys a hosts.<name>.models.<id>.price may hold (ModelPrice.parse).
|
|
421
|
+
MODEL_PRICE_KEYS = %w[input cache_read cache_write output].freeze
|
|
411
422
|
# Section keys beyond the registry's: guardrails' YAML rules (Engine#guardrail_rules).
|
|
412
423
|
# And toolkit's kits (Toolkit.kits), and bundles' judgments' settings (SystemOne::JudgmentSettings).
|
|
413
424
|
SECTION_EXTRA_KEYS = { "guardrails" => %w[rules disable].freeze, "toolkit" => %w[kits].freeze, "system_one" => %w[judgments].freeze }.freeze
|
|
@@ -887,9 +898,9 @@ module Samagotchi
|
|
|
887
898
|
def price_key_problems(prefix, price)
|
|
888
899
|
return [] unless price.is_a?(Hash)
|
|
889
900
|
|
|
890
|
-
(price.keys.map(&:to_s) -
|
|
901
|
+
(price.keys.map(&:to_s) - MODEL_PRICE_KEYS).map do |k|
|
|
891
902
|
# A renamed key is compared by its new name, so that name is the hint.
|
|
892
|
-
unknown_key_message("#{prefix}#{k}", RENAMED_PRICE_KEYS.fetch(k, k),
|
|
903
|
+
unknown_key_message("#{prefix}#{k}", RENAMED_PRICE_KEYS.fetch(k, k), MODEL_PRICE_KEYS, prefix: prefix)
|
|
893
904
|
end
|
|
894
905
|
end
|
|
895
906
|
|
|
@@ -1172,71 +1183,6 @@ module Samagotchi
|
|
|
1172
1183
|
HOST_NAME_RE = /\A[a-z0-9][a-z0-9._-]*\z/i
|
|
1173
1184
|
ENV_NAME_RE = /\A[A-Za-z_][A-Za-z0-9_]*\z/
|
|
1174
1185
|
|
|
1175
|
-
# config.yml's hosts: by lowercased name; an entry that is disabled or
|
|
1176
|
-
# invalid (warned) is left out. No entry left: "default", from
|
|
1177
|
-
# server.host/port.
|
|
1178
|
-
# @return [Hash{String => HostConfig}]
|
|
1179
|
-
def hosts_config(env: ENV, path: global_path(env: env))
|
|
1180
|
-
data = read_yaml(env: env, path: path)
|
|
1181
|
-
raw_hosts = data[HOSTS_KEY] if data.is_a?(Hash)
|
|
1182
|
-
|
|
1183
|
-
normalized = {}
|
|
1184
|
-
if raw_hosts.is_a?(Hash)
|
|
1185
|
-
raw_hosts.each do |raw_name, raw_cfg|
|
|
1186
|
-
host = HostConfig.parse(raw_name, raw_cfg)
|
|
1187
|
-
normalized[host.name] = host if host
|
|
1188
|
-
end
|
|
1189
|
-
warn_duplicate_host_models(normalized)
|
|
1190
|
-
end
|
|
1191
|
-
normalized["default"] = default_host_config(data, env) if normalized.empty?
|
|
1192
|
-
normalized
|
|
1193
|
-
rescue StandardError => e
|
|
1194
|
-
Log.error(:config, "hosts_config_failed", error: e.class.name, message: e.message)
|
|
1195
|
-
{}
|
|
1196
|
-
end
|
|
1197
|
-
|
|
1198
|
-
# The host chi uses when hosts: names none: server.host/port.
|
|
1199
|
-
def default_host_config(data, env)
|
|
1200
|
-
opts = { file_data: data, env: env, cli_overrides: Samagotchi::Config.cli_overrides }
|
|
1201
|
-
host = Samagotchi::Config.resolve("server.host", **opts).to_s.strip
|
|
1202
|
-
host = "localhost" if host.empty?
|
|
1203
|
-
port = Samagotchi::Config.resolve("server.port", **opts).to_i
|
|
1204
|
-
port = 8080 if port <= 0 || port > 65_535
|
|
1205
|
-
HostConfig.new(name: "default", host: host, port: port)
|
|
1206
|
-
end
|
|
1207
|
-
private_class_method :default_host_config
|
|
1208
|
-
|
|
1209
|
-
# Two hosts declaring one id under hosts.<name>.models: a bare id goes
|
|
1210
|
-
# where HostRegistry#host_for_model sends it (the default host when it
|
|
1211
|
-
# declares the id, else the first in hosts: order); say so once.
|
|
1212
|
-
def warn_duplicate_host_models(hosts)
|
|
1213
|
-
declared = Hash.new { |h, k| h[k] = [] }
|
|
1214
|
-
hosts.each { |name, cfg| cfg.models.each { |down, model| declared[down] << [name, model.id] } }
|
|
1215
|
-
declared.each_value do |owners|
|
|
1216
|
-
next if owners.size < 2
|
|
1217
|
-
|
|
1218
|
-
names = owners.map(&:first)
|
|
1219
|
-
id = owners.first.last
|
|
1220
|
-
goes = names.include?("default") ? "default" : "#{names.first} unless the default host lists it"
|
|
1221
|
-
warn_once "Warning: #{names.map { |n| "hosts.#{n}.models" }.join(" and ")} #{names.size == 2 ? "both" : "all"} declare #{id}; " \
|
|
1222
|
-
"a bare #{id} goes to #{goes}"
|
|
1223
|
-
end
|
|
1224
|
-
end
|
|
1225
|
-
private_class_method :warn_duplicate_host_models
|
|
1226
|
-
|
|
1227
|
-
# The names (lowercased) of the hosts entries with enabled: false, which
|
|
1228
|
-
# hosts_config leaves out, from config.yml's hosts:.
|
|
1229
|
-
# @return [Array<String>]
|
|
1230
|
-
def disabled_host_names(env: ENV, path: global_path(env: env))
|
|
1231
|
-
data = read_yaml(env: env, path: path)
|
|
1232
|
-
raw = data[HOSTS_KEY] if data.is_a?(Hash)
|
|
1233
|
-
return [] unless raw.is_a?(Hash)
|
|
1234
|
-
|
|
1235
|
-
raw.filter_map { |name, cfg| name.to_s.strip.downcase if cfg.is_a?(Hash) && HostConfig.disabled?(cfg) }
|
|
1236
|
-
rescue StandardError # degrade-ok: Config↔Log: Log resolves its file and level through Config
|
|
1237
|
-
[]
|
|
1238
|
-
end
|
|
1239
|
-
|
|
1240
1186
|
# Resolve the `recap:` section through the Config registry so scalar
|
|
1241
1187
|
# recap settings share the single precedence path (CLI > ENV > file >
|
|
1242
1188
|
# default). Returns false when explicitly disabled (file `recap: false`
|
|
@@ -1420,47 +1366,6 @@ module Samagotchi
|
|
|
1420
1366
|
{}
|
|
1421
1367
|
end
|
|
1422
1368
|
|
|
1423
|
-
# The top-level `models:` map: per-model settings keyed by model id or
|
|
1424
|
-
# alias (downcased), e.g. `models: {my-alias: {profile: qwen36}}`, each a
|
|
1425
|
-
# ModelSettings; an entry that is not a map is skipped.
|
|
1426
|
-
# @return [Hash{String => ModelSettings}]
|
|
1427
|
-
def model_settings(env: ENV, path: global_path(env: env))
|
|
1428
|
-
data = read_yaml(env: env, path: path)
|
|
1429
|
-
raw = data[MODELS_KEY] if data.is_a?(Hash)
|
|
1430
|
-
return {} unless raw.is_a?(Hash)
|
|
1431
|
-
|
|
1432
|
-
raw.each_with_object({}) do |(k, v), result|
|
|
1433
|
-
key = k.to_s.strip.downcase
|
|
1434
|
-
settings = ModelSettings.parse(key, v)
|
|
1435
|
-
result[key] = settings if settings
|
|
1436
|
-
end
|
|
1437
|
-
rescue StandardError # degrade-ok: Config↔Log: Log resolves its file and level through Config
|
|
1438
|
-
{}
|
|
1439
|
-
end
|
|
1440
|
-
|
|
1441
|
-
# The first +names+ (model as typed, alias-resolved, bare, …) whose
|
|
1442
|
-
# models: entry sets +field+ (a ModelSettings member; another name
|
|
1443
|
-
# raises ArgumentError): a ModelSetting (key, value), or nil.
|
|
1444
|
-
# VisionSupport and SamplingSettings look a model up the same way.
|
|
1445
|
-
# @param models [Hash{String => ModelSettings}, nil] model_settings by default
|
|
1446
|
-
# @return [ModelSetting, nil]
|
|
1447
|
-
def model_setting(names, field, models: nil)
|
|
1448
|
-
raise ArgumentError, "unknown models: field #{field.inspect}" unless ModelSettings.members.include?(field)
|
|
1449
|
-
|
|
1450
|
-
models ||= model_settings
|
|
1451
|
-
Array(names).map { |name| name.to_s.strip.downcase }.reject(&:empty?).uniq.each do |key|
|
|
1452
|
-
value = models[key]&.public_send(field)
|
|
1453
|
-
return ModelSetting.new(key: key, value: value) unless value.nil?
|
|
1454
|
-
end
|
|
1455
|
-
nil
|
|
1456
|
-
end
|
|
1457
|
-
|
|
1458
|
-
# +raw+ parsed against config.yml's hosts (or +hosts+) and aliases.
|
|
1459
|
-
# @return [ModelRef]
|
|
1460
|
-
def model_ref(raw, env: ENV, path: global_path(env: env), hosts: nil)
|
|
1461
|
-
ModelRef.parse(raw, hosts: hosts || hosts_config(env: env, path: path), aliases: model_aliases(env: env, path: path))
|
|
1462
|
-
end
|
|
1463
|
-
|
|
1464
1369
|
# Words /model takes as arguments (clear/default/none/off reset the
|
|
1465
1370
|
# runtime model), so no alias may be named after one.
|
|
1466
1371
|
RESERVED_MODEL_ALIASES = %w[clear default none off].freeze
|
|
@@ -428,8 +428,7 @@ module Samagotchi
|
|
|
428
428
|
session = @access.show(id)
|
|
429
429
|
Target.new(session_id: id, project_root: session.project_root, cwd: session.working_directory, label: id[0, 8])
|
|
430
430
|
rescue ArgumentError => e
|
|
431
|
-
|
|
432
|
-
error_line("#{command_name}: #{message}")
|
|
431
|
+
error_line("#{command_name}: #{e.message}")
|
|
433
432
|
nil
|
|
434
433
|
end
|
|
435
434
|
|
|
@@ -50,11 +50,11 @@ module Samagotchi
|
|
|
50
50
|
# and vision look a model up), else hosts.<name>.window_tokens.
|
|
51
51
|
# @param target [HostRegistry::ModelTarget]
|
|
52
52
|
# @param names [Array<String>] HostRegistry#lookup_names
|
|
53
|
-
# @param models [Hash{String => ModelSettings}, nil]
|
|
53
|
+
# @param models [Hash{String => ModelSettings}, nil] ModelSettings.all (specs)
|
|
54
54
|
# @return [Resolved, nil]
|
|
55
55
|
def setting(target, names:, models: nil)
|
|
56
|
-
models ||=
|
|
57
|
-
tokens =
|
|
56
|
+
models ||= ModelSettings.all
|
|
57
|
+
tokens = ModelSettings.find(names, :window_tokens, models: models)&.value
|
|
58
58
|
return Resolved.new(tokens: tokens, source: :model_setting) if positive_integer?(tokens)
|
|
59
59
|
|
|
60
60
|
tokens = target&.entry&.window_tokens
|
|
@@ -335,7 +335,7 @@ module Samagotchi
|
|
|
335
335
|
# are left out. @return [Array<Hash>] name:, hosts:, state:
|
|
336
336
|
def host_keys
|
|
337
337
|
config_env = { "XDG_CONFIG_HOME" => launch_config_home }
|
|
338
|
-
hosts =
|
|
338
|
+
hosts = HostConfig.all(env: config_env).values.select(&:api_key_env).group_by(&:api_key_env)
|
|
339
339
|
hosts.map do |name, entries|
|
|
340
340
|
state = if HostKeys.from_keychain(name, env: @env) then :keychain
|
|
341
341
|
elsif !@env[name].to_s.strip.empty? then :shell_only
|
|
@@ -5,7 +5,7 @@ module Samagotchi
|
|
|
5
5
|
# The attached-context candidate judge (TurnStages::ContextJudge,
|
|
6
6
|
# forwarded): prepare_turn starts its pass, run_turn's ensure shows the
|
|
7
7
|
# cards it held, and a plugin's late candidate asks it. Reads
|
|
8
|
-
# @context_judge, @side_judgments, @host_registry.
|
|
8
|
+
# @context_judge, @side_judgments, @host_registry, @turn_state, @session.
|
|
9
9
|
module ContextJudgePart
|
|
10
10
|
private
|
|
11
11
|
|
|
@@ -13,19 +13,27 @@ module Samagotchi
|
|
|
13
13
|
attr_reader :context_judge
|
|
14
14
|
|
|
15
15
|
# A late proposal no owner prompt mentions (Plugin::Host
|
|
16
|
-
# #candidate_proposed
|
|
17
|
-
|
|
16
|
+
# #candidate_proposed, maybe while the plugins load, before a session
|
|
17
|
+
# or the event log is there): TurnStages::ContextJudge#late_candidates
|
|
18
|
+
# on the session the Engine is on (none, or a review session: nothing,
|
|
19
|
+
# before anything else is read), the owner's prompts so far, the
|
|
20
|
+
# recorder and the sessions dir, all read now on the caller's thread.
|
|
21
|
+
def judge_late_candidates
|
|
22
|
+
session = @session
|
|
23
|
+
return if session.nil? || session.review?
|
|
18
24
|
|
|
19
|
-
|
|
25
|
+
@context_judge.late_candidates(session, messages: plugin_messages, recorder: system_one_recorder,
|
|
26
|
+
state_dir: session_state_dir)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The stage over this Engine's side asks (built in
|
|
20
30
|
# #build_tools_and_plugins, before the plugins load: one may propose a
|
|
21
|
-
# candidate as it loads).
|
|
31
|
+
# candidate as it loads). Its two callables are the asks thread's late
|
|
32
|
+
# reads: the session the Engine is on and where a card goes, both as
|
|
33
|
+
# an answer comes.
|
|
22
34
|
def build_context_judge
|
|
23
|
-
TurnStages::ContextJudge.new(
|
|
24
|
-
|
|
25
|
-
recording: ->(client, judgment, session, mode:) { system_one_recording(client, judgment, session, mode: mode) },
|
|
26
|
-
session: -> { session }, messages: -> { plugin_messages }, turn_running: -> { turn_running? },
|
|
27
|
-
show_card: ->(**card) { show_card(**card) }
|
|
28
|
-
)
|
|
35
|
+
TurnStages::ContextJudge.new(side_judgments: @side_judgments, host_registry: @host_registry, turn_state: @turn_state,
|
|
36
|
+
show_card: ->(**card) { show_card(**card) }, current_session: -> { @session })
|
|
29
37
|
end
|
|
30
38
|
end
|
|
31
39
|
end
|
|
@@ -46,7 +46,7 @@ module Samagotchi
|
|
|
46
46
|
from_cwd: note[:from_cwd], text: note[:text], context_source: note[:context_source],
|
|
47
47
|
created_at: note[:created_at], objectives: entry }.compact)
|
|
48
48
|
announce_objectives(session) if entry
|
|
49
|
-
@objectives_stage.note(session, note)
|
|
49
|
+
@objectives_stage.note(session, note, recorder: system_one_recorder, state_dir: session_state_dir)
|
|
50
50
|
message
|
|
51
51
|
end
|
|
52
52
|
end
|