samagotchi 0.140.0 → 0.142.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.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +64 -1
  3. data/README.md +1 -1
  4. data/bin/chi +2 -0
  5. data/docs/architecture.md +14 -285
  6. data/docs/bundle-authoring.md +1 -0
  7. data/docs/cli.md +15 -7
  8. data/docs/configuration.md +57 -18
  9. data/docs/context.md +68 -6
  10. data/docs/guardrails.md +20 -11
  11. data/docs/hooks.md +9 -7
  12. data/docs/internals/attached-context.md +226 -0
  13. data/docs/internals/background-tasks.md +1 -1
  14. data/docs/internals/bridge.md +380 -0
  15. data/docs/internals/bundles.md +1 -0
  16. data/docs/internals/context-telemetry.md +1 -1
  17. data/docs/internals/engine.md +24 -18
  18. data/docs/internals/participants.md +13 -12
  19. data/docs/internals/remote-sessions.md +5 -2
  20. data/docs/internals/{tool-guardrails.md → tool-output-limits.md} +21 -2
  21. data/docs/memory.md +31 -1
  22. data/docs/plugins.md +162 -2
  23. data/docs/sessions.md +53 -28
  24. data/docs/system-one.md +122 -0
  25. data/docs/testing.md +3 -1
  26. data/lib/samagotchi/bridge.rb +2 -0
  27. data/lib/samagotchi/bundles/check-in/manifest.yml +6 -3
  28. data/lib/samagotchi/bundles/check-in/plugin.rb +101 -4
  29. data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -2
  30. data/lib/samagotchi/bundles/claude-peer/scripts/chi-peer-SKILL.md +14 -0
  31. data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -2
  32. data/lib/samagotchi/bundles/codex-peer/scripts/chi-peer-SKILL.md +15 -1
  33. data/lib/samagotchi/bundles/core/manifest.yml +2 -2
  34. data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
  35. data/lib/samagotchi/bundles/system/manifest.yml +4 -4
  36. data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
  37. data/lib/samagotchi/bundles/system/self_map.md +1 -1
  38. data/lib/samagotchi/config.rb +74 -14
  39. data/lib/samagotchi/context_candidate_choice.rb +8 -0
  40. data/lib/samagotchi/context_candidates_log.rb +25 -1
  41. data/lib/samagotchi/context_command.rb +32 -9
  42. data/lib/samagotchi/context_judge.rb +6 -2
  43. data/lib/samagotchi/context_move.rb +31 -0
  44. data/lib/samagotchi/context_sources.rb +248 -10
  45. data/lib/samagotchi/engine.rb +68 -7
  46. data/lib/samagotchi/extension_load.rb +3 -2
  47. data/lib/samagotchi/guardrail_wiring.rb +36 -31
  48. data/lib/samagotchi/guardrails/chi_dirs.rb +7 -4
  49. data/lib/samagotchi/guardrails/path_resolve.rb +1 -1
  50. data/lib/samagotchi/guardrails/protected_paths.rb +7 -4
  51. data/lib/samagotchi/guardrails_command.rb +3 -3
  52. data/lib/samagotchi/judgments_command.rb +155 -0
  53. data/lib/samagotchi/kernel_loop.rb +13 -1
  54. data/lib/samagotchi/llm/chat_loop.rb +74 -10
  55. data/lib/samagotchi/llm/errors.rb +7 -5
  56. data/lib/samagotchi/llm/stray_tool_call.rb +61 -0
  57. data/lib/samagotchi/log_line.rb +1 -1
  58. data/lib/samagotchi/memory_bundle/entry_size.rb +100 -0
  59. data/lib/samagotchi/plugin/api.rb +33 -2
  60. data/lib/samagotchi/plugin/attached_context.rb +14 -2
  61. data/lib/samagotchi/plugin/context.rb +54 -1
  62. data/lib/samagotchi/plugin/judgments.rb +72 -0
  63. data/lib/samagotchi/plugin/loader.rb +3 -2
  64. data/lib/samagotchi/plugin/peer.rb +53 -0
  65. data/lib/samagotchi/plugin/sessions.rb +9 -2
  66. data/lib/samagotchi/run_options.rb +1 -0
  67. data/lib/samagotchi/self_report.rb +36 -0
  68. data/lib/samagotchi/send_command.rb +3 -1
  69. data/lib/samagotchi/session.rb +4 -0
  70. data/lib/samagotchi/session_access/contract.rb +3 -1
  71. data/lib/samagotchi/session_access/local.rb +24 -3
  72. data/lib/samagotchi/session_access/remote.rb +19 -8
  73. data/lib/samagotchi/session_access.rb +26 -6
  74. data/lib/samagotchi/session_commands.rb +3 -0
  75. data/lib/samagotchi/session_continue.rb +9 -2
  76. data/lib/samagotchi/session_manager.rb +13 -2
  77. data/lib/samagotchi/session_retention.rb +10 -0
  78. data/lib/samagotchi/side_judgments.rb +12 -1
  79. data/lib/samagotchi/system_one/answer.rb +63 -11
  80. data/lib/samagotchi/system_one/approval_label.rb +56 -0
  81. data/lib/samagotchi/system_one/approval_question.rb +2 -0
  82. data/lib/samagotchi/system_one/client.rb +43 -17
  83. data/lib/samagotchi/system_one/fake_backend.rb +1 -1
  84. data/lib/samagotchi/system_one/http_backend.rb +12 -4
  85. data/lib/samagotchi/system_one/judgment_settings.rb +61 -0
  86. data/lib/samagotchi/system_one/judgments.rb +260 -0
  87. data/lib/samagotchi/system_one/log.rb +4 -2
  88. data/lib/samagotchi/system_one/recording.rb +117 -0
  89. data/lib/samagotchi/system_one/runs.rb +179 -0
  90. data/lib/samagotchi/system_one/runs_store.rb +230 -0
  91. data/lib/samagotchi/system_one.rb +9 -0
  92. data/lib/samagotchi/testing/boundary.rb +5 -1
  93. data/lib/samagotchi/testing/bundle.rb +9 -2
  94. data/lib/samagotchi/testing/host.rb +2 -1
  95. data/lib/samagotchi/testing/judgments.rb +185 -0
  96. data/lib/samagotchi/testing/recorder.rb +5 -0
  97. data/lib/samagotchi/testing/recording_api.rb +1 -1
  98. data/lib/samagotchi/tool_output_cut.rb +26 -6
  99. data/lib/samagotchi/tool_response.rb +1 -1
  100. data/lib/samagotchi/tool_runner.rb +63 -4
  101. data/lib/samagotchi/toolkit_suggest.rb +11 -2
  102. data/lib/samagotchi/tools/memory.rb +28 -0
  103. data/lib/samagotchi/turn_note.rb +9 -0
  104. data/lib/samagotchi/version.rb +1 -1
  105. data/lib/samagotchi/web/app.rb +14 -8
  106. data/lib/samagotchi/web/public/chains.js +10 -2
  107. data/lib/samagotchi/web/session_access_routes.rb +7 -2
  108. data/lib/samagotchi/web/session_create.rb +1 -0
  109. metadata +17 -2
@@ -149,7 +149,7 @@ or `chi web` restart.
149
149
  |---|---|---|
150
150
  | `default.model` | New session | Existing sessions keep their saved model; change one with `/model`. |
151
151
  | `default.input`, `no_default_input`, `no_interrupt` | Next CLI run | These set up that run's first prompt or turn limit. |
152
- | `default.max_tokens`, `max_tool_output_chars`, `read.*`, `execute.*` except `execute.description`, `image.*` | Next matching request or tool use | An operation already in progress keeps its resolved limits. |
152
+ | `default.max_tokens`, `max_tool_output_chars`, `memory.read_max_chars`, `read.*`, `execute.*` except `execute.description`, `image.*` | Next matching request or tool use | An operation already in progress keeps its resolved limits. |
153
153
  | `server.host`, `server.port`, `hosts.*` | Worker start | Includes host addresses, APIs, declared models and host-level settings. The web model picker refreshes `hosts:` on file change; a running worker does not. |
154
154
  | `server.open_timeout`, `server.read_timeout`, `server.first_token_timeout` | Worker start | The worker's clients retain the timeouts set when they were built. |
155
155
  | `model_aliases` | Next model lookup | A session keeps its resolved model unless it is explicitly switched. |
@@ -166,9 +166,10 @@ or `chi web` restart.
166
166
  | `toolkit.kits`, `execute.description`, `llm_context.policy` | Worker start | These are held by the engine's toolkit or tool registry. |
167
167
  | `toolkit.suggest.mode`, `toolkit.suggest.threshold`, `toolkit.suggest.defer_turns`, `toolkit.suggest.defer_tokens` | Worker start | Suggestion settings are config-only and are resolved when the worker runs its audit or on-mode ask. |
168
168
  | `skip_agents_md` | Prompt rebuild | A running engine caches its built prompt; a new worker or `/model` rebuild uses the changed value. |
169
- | `context.*` except `context.every_seconds`, `llm_context.*` except `llm_context.policy`, `steer.cut_after`, `memory.index_warn_tokens`, `status.line`, `web.participants.*` | Next matching turn, display, poll or write | Readers resolve these settings when they perform that operation. Session-specific LLM context and thinking values remain saved in the session. A participant's grant keeps the expiry and cap it was issued with; `web.participants.*` is what the next invite gets, and the cap and queue of a grant from before caps. |
169
+ | `context.*` except `context.every_seconds`, `llm_context.*` except `llm_context.policy`, `steer.cut_after`, `memory.index_warn_tokens`, `memory.entry_warn_tokens`, `status.line`, `web.participants.*` | Next matching turn, display, poll or write | Readers resolve these settings when they perform that operation. Session-specific LLM context and thinking values remain saved in the session. A participant's grant keeps the expiry and cap it was issued with; `web.participants.*` is what the next invite gets, and the cap and queue of a grant from before caps. |
170
170
  | `context.every_seconds` | Next context poll | The poller checks it for each source that has no explicit interval. |
171
171
  | `guardrails.*` | Next tool call or approval | Guardrail rules and disable lists are reloaded when the config file changes; rule settings take effect on that evaluation. `guardrails.parent_answers` is checked when a parent answers a question. |
172
+ | `system_one.host`, `system_one.model`, `system_one.timeout_ms`, `system_one.runs`, `system_one.judgments` | Next tool call or approval | Each ask reads them. |
172
173
  | `hooks`, `bundles` | Worker start | Hooks, plugins and their bundle settings load with the engine. |
173
174
  | `recap.*` | Worker start | The engine builds its recap job at start, and again when it binds a session with its own `recap.enabled` or `recap.sentences` (`--set`, see "Per-session settings") and when `/recap on\|off\|default` changes the session's own value. |
174
175
  | `session.shared` | Next `chi` invocation | It selects attached-worker or in-process mode for that invocation. |
@@ -185,7 +186,7 @@ or `chi web` restart.
185
186
 
186
187
  ### Per-session settings
187
188
 
188
- A few core keys can be set for one session: `chi --set KEY=VALUE` (or `chi send --new --set KEY=VALUE`, or
189
+ A few core keys can be set for one session (the registry entries marked `session: true`, `Config::Entry#session?`): `chi --set KEY=VALUE` (or `chi send --new --set KEY=VALUE`, or
189
190
  `POST /api/sessions` `settings`) at its creation, and `recap.enabled` later too with `/recap on`, `/recap off` or `/recap default`
190
191
  (at once; `default` removes the session's own value, see [Session recap](cli.md#session-recap)). The value is saved in the session file (`config_settings`), so a
191
192
  worker respawn, a continue and a fork keep it; a delegate child starts without it, on config.yml's. It goes first:
@@ -1526,14 +1527,14 @@ toolkit:
1526
1527
  `undecided` for greetings or check-ins. Only a default-origin session with at least two described candidates is
1527
1528
  asked; chosen or inherited toolkits, delegate children, review sessions and participant turns are skipped. After
1528
1529
  `undecided`, later owner prompts may be asked again within both defer limits. The default is `off`. System 1's
1529
- host, model and timeout come from `guardrails.system_one.*`; the suggestion threshold and defer limits are its own
1530
+ host, model and timeout come from `system_one.*`; the suggestion threshold and defer limits are its own
1530
1531
  settings below. Suggestions are
1531
1532
  kept in `toolkit_suggestions.jsonl`; `chi toolkit report` grades them against the session's tool calls. One ask
1532
1533
  runs at a time (a turn that comes while one still runs goes unasked, with an info line), and a kit that doesn't
1533
1534
  resolve (a missing base, a cycle) is left out of the candidates. A worker that exits waits about a second for a
1534
1535
  running ask, so a one-shot `-p --non-interactive` run may end before its suggestion is logged.
1535
1536
  - **On mode.** `toolkit.suggest.mode: on` asks the same question on the same eligible turns, but the turn waits for
1536
- the answer before it declares any tool, for at most `guardrails.system_one.timeout_ms` from the ask's start (it
1537
+ the answer before it declares any tool, for at most `system_one.timeout_ms` from the ask's start (it
1537
1538
  starts right after the turn does, so plugin and MCP start-up hide most of it). A described kit picked with
1538
1539
  confidence at or above `toolkit.suggest.threshold` becomes the session's toolkit, saved with `toolkit_origin:
1539
1540
  suggested`, and the turn's first request declares it; the notice reads
@@ -1541,7 +1542,7 @@ toolkit:
1541
1542
  pick, `undecided`, no answer, a failing System 1, which warns once as `[samagotchi:toolkit]`) keeps the default
1542
1543
  kit. No answer in time or none at all says so in a warn notice, `toolkit: full (no suggestion: System 1 took over
1543
1544
  5000 ms) · /toolkit to change` (or `System 1 gave no answer`), and a timeout logs a `toolkit_suggest_timeout`
1544
- warning: a slow System 1 host wants a larger `guardrails.system_one.timeout_ms`. A Stop during the wait (or before it, even with the answer in) ends the turn before its first request, and
1545
+ warning: a slow System 1 host wants a larger `system_one.timeout_ms`. A Stop during the wait (or before it, even with the answer in) ends the turn before its first request, and
1545
1546
  nothing switches. After an `undecided`, a later prompt within the defer limits may switch the session; that notice
1546
1547
  says `from this turn`, and that request re-reads the whole prompt (system prompt, tools and conversation, which
1547
1548
  the defer limits keep small) without `/toolkit`'s cache confirmation. `/toolkit` or `ctx.use_toolkit` marks the
@@ -1591,7 +1592,7 @@ toolkit:
1591
1592
  - **A new web chat: 🧰.** The start page's 🧰 (beside ⚙; a bottom sheet on a phone) picks the new session's toolkit.
1592
1593
  **Auto**, checked by default, sends nothing: the session is on `toolkit.default` with `toolkit_origin: default`, so
1593
1594
  with `toolkit.suggest.mode: on` its first message may switch it to a suggested kit. Auto is listed only when that
1594
- can happen as far as chi web can tell (on mode, a `guardrails.system_one.host`, two described config kits); bundle
1595
+ can happen as far as chi web can tell (on mode, a `system_one.host`, two described config kits); bundle
1595
1596
  and memory kits can add candidates it doesn't see. A kit picked is sent as `POST /api/sessions`'s `toolkit` and
1596
1597
  saved as `chosen`: no suggestion, the default kit included (its row says `default`); without Auto, the default's
1597
1598
  row is the one checked and sends nothing. The list holds the built-ins and config kits a new session can start
@@ -1640,6 +1641,21 @@ toolkit:
1640
1641
  changes what saved sessions on it send after an upgrade: the next request reads the prompt again once, as a
1641
1642
  tool's schema change does. So can an edit to `toolkit.kits`: a session's worker reads the kits once when it starts, so a running session keeps its
1642
1643
  tools and the edit reaches it at its next worker start (after an idle exit, `chi sessions restart`).
1644
+ - **Why a session is on its kit: `toolkit_origin`.** The session file records `default` (the configured kit),
1645
+ `chosen` (a user-selected kit, `/toolkit` and `ctx.use_toolkit` included), `inherited` (a continue or a fork) or
1646
+ `suggested` (`toolkit.suggest.mode: on` switched it). A file from before the field reads as `chosen`, so an old
1647
+ default session is never newly suggested a kit; only a `default` session is ever suggested one. A delegate child
1648
+ gets the kit its `delegate` call names (`toolkit:`, any kit the parent knows), else `toolkit.child`; a kit its worker
1649
+ can't resolve declares every tool.
1650
+ - **Loads saved in the conversation.** A `find_tools` result on a chat host saves the tools its hit loads as
1651
+ `loaded: ["list_sessions", "mcp_fs_read"]` on its `tool_response` entry (wire names: a built-in's, an MCP tool's
1652
+ `mcp_<server>_<tool>`), whatever the host: a later turn on `api: anthropic` (after a `/model` switch, a resume) sends
1653
+ the load as references, any other host ignores it. A plugin's `ctx.load_tools` leaves a system entry
1654
+ `{"role": "system", "kind": "tool_change", "content": "[tools loaded: delegate (bundle)]", "tool_change": {"add":
1655
+ ["delegate"], "by": "bundle"}}` after the turn's user message, only on a host that takes tool changes
1656
+ (`api: anthropic`; [wire details](internals/anthropic.md#tool-changes-ctxload_tools)). It is never the system head,
1657
+ counts as conversation, is hidden in the web and TUI histories and goes to no other wire. Either way the names stay
1658
+ deferred for the session's life, across respawns, `--resume` and forks.
1643
1659
 
1644
1660
  ## LLM context: the forget layer
1645
1661
 
@@ -1762,7 +1778,7 @@ its own). The first target is a long session such as a day-long coordinator, whe
1762
1778
 
1763
1779
  `context.candidates.mode: audit` asks System 1 whether a session's work is about each candidate a bundle proposed
1764
1780
  (see [The judge](context.md#the-judge)); `on` also acts on the answer: attaches at `threshold`, a card from
1765
- `offer_threshold` ([On mode](context.md#on-mode)). It uses `guardrails.system_one.host` and `.model`, but its own mode: System
1781
+ `offer_threshold` ([On mode](context.md#on-mode)). It uses `system_one.host` and `.model`, but its own mode: System
1766
1782
  1's `mode` (approvals) can stay `off`. Jev, the hosted judgment model, is served through OpenRouter
1767
1783
  (`/api/v1/systemone`), so OpenRouter can be the System 1 host:
1768
1784
 
@@ -1772,10 +1788,9 @@ hosts:
1772
1788
  url: https://openrouter.ai/api/v1
1773
1789
  api: openai
1774
1790
  api_key_env: OPENROUTER_API_KEY
1775
- guardrails:
1776
- system_one:
1777
- host: openrouter
1778
- model: typesafe/jev-1.14
1791
+ system_one:
1792
+ host: openrouter
1793
+ model: typesafe/jev-1.14
1779
1794
  context:
1780
1795
  candidates:
1781
1796
  mode: audit
@@ -1785,6 +1800,27 @@ A remote host is asked beside the turn. A System 1 host that is the model's own
1785
1800
  turn's last request, and the turn-end warm-up follows them, re-warming the prefix they evicted (see
1786
1801
  [prompt caching](internals/prompt-caching.md)).
1787
1802
 
1803
+
1804
+ ## Bundles' judgments
1805
+
1806
+ A bundle can declare its own cheap System 1 questions ([Judgments](plugins.md#judgments-chijudgment-and-ctxjudge)). Each is off until
1807
+ you set its mode under its full name, `<bundle>.<name>`, in `system_one.judgments`:
1808
+
1809
+ ```yaml
1810
+ system_one:
1811
+ host: splash
1812
+ judgments:
1813
+ check-in.progress:
1814
+ mode: audit # off (the default) | audit | on
1815
+ host: openrouter # optional, this judgment alone; else system_one.host
1816
+ model: typesafe/jev-1.14 # optional, this judgment alone; else system_one.model, or on a host of its own that host's first model
1817
+ ```
1818
+
1819
+ A name no loaded bundle declares gets a notice at the session's first turn. Core's judgments (`approvals.*`, `context.*`, `guardrails.*`,
1820
+ `toolkit.*`) take no settings here and warn if named: `SystemOne::Record` and `Similar` don't look at the model, so a
1821
+ model swapped for approvals alone would inherit a clean record and could auto-allow.
1822
+ A bundle's judgments on a hosted host aren't under any cost cap: they stay out of the session's spend, as all of System 1's do.
1823
+
1788
1824
  ## Opening files from chi web
1789
1825
 
1790
1826
  In `chi web`, a file tool's row (read, edit, write) and the lines of its diff are refs to that file. They look like
@@ -2190,12 +2226,13 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
2190
2226
  | `guardrails.small_models` | `auto` | | Which models get the `models: small` rules: `auto` (32B or less by the size in the name, an MoE's active size), a list of globs, or `[]`. config.yml only. See [Guardrails](guardrails.md#rules-for-some-models). |
2191
2227
  | `guardrails.parent_answers` | `soft` | | Which options a parent agent (`chi answer`) may pick on a session's question, by their effect: `hard` (a deny, a Stop, a plain answer), `standard` (+ Continue and other spending options), `soft` (+ "Allow once"); never a wider scope or an allow on chi's own config. An invalid value warns and is `hard`. A session's own `parent_answers` (`chi --parent-answers`, `/parent-answers`) may only tighten it. Replaces `guardrails.parent_approvals` and `turn.parent_continue`. config.yml only: no environment variable (a parent can still pick the config dir with `XDG_CONFIG_HOME`). A convention for an honest parent, not a security boundary. See [Guardrails](guardrails.md#answers-from-a-parent-agent). |
2192
2228
  | `guardrails.system_one.mode` | `off` | | System 1, the cheap judgment that answers an ask before the user sees it: `off` (the default), `audit` (judge every ask, log it, never decide), `on` (also allowed to allow a call once, behind the gates in [System 1](guardrails.md#system-1-experimental)). An unknown value warns and is `off`. Experimental. config.yml only. See [Guardrails](guardrails.md#system-1-experimental). |
2193
- | `guardrails.system_one.host` | none | | The `hosts:` entry serving `/v1/systemone` (a local Splash, or the hosted Jev API); unset: no judgment. config.yml only. |
2194
- | `guardrails.system_one.model` | none | | The judgment model; unset uses the host's first model, and a host that names none has no judgment. config.yml only. |
2195
2229
  | `guardrails.system_one.rules` | none | | The rule ids System 1 may answer for in `on` mode: `"git-push|git-rebase"` or a YAML list, parsed like `guardrails.small_models`. Nothing is auto-allowed while it is unset; the `chi-answer-piped`, `chi-answer-http` and `chi-broadcast` rules never are, however they are listed. config.yml only. |
2196
2230
  | `guardrails.system_one.threshold` | `0.8` | | The probability an auto-allow needs. config.yml only. |
2197
2231
  | `guardrails.system_one.min_record` | `5` | | How many clean records a rule needs before System 1 is trusted for it: labelled asks of that rule at the threshold the user allowed. config.yml only. |
2198
- | `guardrails.system_one.timeout_ms` | `2000` | | Milliseconds one judgment may take; a slower one is no judgment and the ask goes to the user. config.yml only. |
2232
+ | `system_one.host` | none | | The `hosts:` entry serving `/v1/systemone` (a local Splash, or the hosted Jev API); unset: no judgment, by any use of System 1, so nothing auto-allows and every ask goes to the user. Moved from `guardrails.system_one.host`: while any of host, model and timeout_ms is still under `guardrails.system_one`, chi warns and nothing is judged. config.yml only. |
2233
+ | `system_one.model` | none | | The judgment model; unset uses the host's first model, and a host that names none has no judgment. Moved from `guardrails.system_one.model`. config.yml only. |
2234
+ | `system_one.timeout_ms` | `2000` | | Milliseconds one judgment may take; a slower one is no judgment and the ask goes to the user. Moved from `guardrails.system_one.timeout_ms`. config.yml only. |
2235
+ | `system_one.runs` | `on` | | Save each System 1 ask and what came of it, under the state dir, as long as sessions last (see [System 1's saved runs](system-one.md)); `off` stops saving. config.yml only. |
2199
2236
  | `log.file` | state dir | yes | See "Debug Log File". |
2200
2237
  | `log.disable` | `false` | yes | No file logging. |
2201
2238
  | `log.level` | `info` | yes | `debug`, `info`, `warn`, `error`. |
@@ -2218,13 +2255,13 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
2218
2255
  | `context.every_seconds` | `300` | | How often an attached context source's command runs when it has no `--every` (seconds, at least 30). See [Attached context](context.md). |
2219
2256
  | `context.wake` | `true` | yes | A source whose update says `wake: true` may start a turn in a live, idle session (one per source per 10 minutes, a peer source up to `context.peer_turns` in a row, within `session.max_wakes`); so may a session's pushed source's first push. `false`: updates wait as notes for the next turn. See [Waking](context.md#waking). |
2220
2257
  | `context.peer_turns` | `3` | yes | Wake turns in a row a peer agent's answers may start (a peer bundle's outbox source, such as `codex-outbox`), with no message typed in the web or a TUI in between (`chi send` doesn't count); within `session.max_wakes`. Then answers wait as notes, and the UIs hear it once. An answer whose summary ends its verdicts with `VERDICT OK` wakes once to close the round, counted toward `session.max_wakes` only. `0`: a peer source wakes like any other. See [Waking](context.md#waking). |
2221
- | `context.candidates.mode` | `off` | | The candidate judge: `off` (the default: no conversation text goes to a System 1 host), `audit` (asks System 1 whether the session's work is about each proposed candidate and logs it in `context_candidates.jsonl`; attaches nothing), `on` (the same asks, acted on: attached at `threshold`, a card from `offer_threshold`; see [On mode](context.md#on-mode)). Its host and model are `guardrails.system_one.host` / `.model`. config.yml only. See [The judge](context.md#the-judge). |
2258
+ | `context.candidates.mode` | `off` | | The candidate judge: `off` (the default: no conversation text goes to a System 1 host), `audit` (asks System 1 whether the session's work is about each proposed candidate and logs it in `context_candidates.jsonl`; attaches nothing), `on` (the same asks, acted on: attached at `threshold`, a card from `offer_threshold`; see [On mode](context.md#on-mode)). Its host and model are `system_one.host` / `.model`. config.yml only. See [The judge](context.md#the-judge). |
2222
2259
  | `context.candidates.threshold` | `0.8` | | The probability at which the judge attaches a candidate in on mode (a judged line's `would: attach`). config.yml only. |
2223
2260
  | `context.candidates.offer_threshold` | `0.6` | | The probability from which on mode shows the candidate's card, once (`would: offer`, below `threshold`). config.yml only. |
2224
2261
  | `context.candidates.reask_after_turns` | `3` | | Owner prompts after a judged ask before the judge's one re-ask of that candidate. config.yml only. |
2225
2262
  | `toolkit.default` | `full` | | The toolkit a new session is stored on: the tools its prompts declare (`full`, `lean`, `child`, one of `toolkit.kits` or a bundle's). `chi --toolkit` picks one per session. See "Toolkits". |
2226
2263
  | `toolkit.child` | `full` | | A delegate child's toolkit, unless its `delegate` call names one (`toolkit:`). See "Toolkits". |
2227
- | `toolkit.suggest.mode` | `off` | | `audit` asks System 1 for a toolkit suggestion without changing the session; `on` waits for it (up to `guardrails.system_one.timeout_ms`) on an eligible turn and starts the session on a confidently suggested kit (Toolkits, On mode). The System 1 host, model and timeout come from `guardrails.system_one.*`. config.yml only. |
2264
+ | `toolkit.suggest.mode` | `off` | | `audit` asks System 1 for a toolkit suggestion without changing the session; `on` waits for it (up to `system_one.timeout_ms`) on an eligible turn and starts the session on a confidently suggested kit (Toolkits, On mode). The System 1 host, model and timeout come from `system_one.*`. config.yml only. |
2228
2265
  | `toolkit.suggest.threshold` | `0.5` | | Minimum confidence for a non-default toolkit suggestion. config.yml only. |
2229
2266
  | `toolkit.suggest.defer_turns` | `3` | | Maximum owner-prompt number for another ask after `undecided` (the owner's own prompts: not steers, notes, reminders or `!` command output). config.yml only. |
2230
2267
  | `toolkit.suggest.defer_tokens` | `5000` | | Maximum estimated conversation size for another ask after `undecided` (characters / 4). config.yml only. |
@@ -2235,7 +2272,9 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
2235
2272
  | `hosts.<name>.models.<id>.served` | none | | The models the host may answer that id with (exact ids, any case), or `any`: no served-model warning for those. See "Models a host serves but doesn't list". |
2236
2273
  | `hosts.<name>.models.<id>.price` | none | | The model's price on that host, USD per 1M tokens: `input`, `output`, optional `cache_read` and `cache_write`. Estimates the cost a provider doesn't report. See "Prices". |
2237
2274
  | `hosts.<name>.remote` | by address | | `true`/`false`: treat the host as a remote provider or a local server. See "Remote or local". |
2238
- | `max_tool_output_chars` | `10000` | yes | Characters of each tool output kept in the conversation (both loops). A longer one is cut: an `execute` result keeps its first and last lines with a gap line between them and is saved whole in the session's folder (`sessions/<id>/tool_output/`; the gap line names the file, and the model reads it there instead of running the command again); any other tool's keeps its head and ends with `[cut: N of M chars; read it in parts]`. See [Tool output guardrails](internals/tool-guardrails.md#the-runners-cut-max_tool_output_chars). A top-level key (see below). |
2275
+ | `memory.entry_warn_tokens` | `5000` | yes | The tokens one memory may hold (every read of it pays them; about 20K characters at the default `context.chars_per_token`) before a `memory_write`, `write` or `edit` that takes it over gets a note, once per crossing, asking the model to tighten it; `0`: no note. A memory that did not exist counts as 0 tokens. Not for a description-only write, a remove, `index`, a model overlay or a `model_notes_*` memory. Independent of `memory.index_warn_tokens`. See [Memory](memory.md). |
2276
+ | `memory.read_max_chars` | `0` | yes | The most characters one `memory_read` result holds. `0`: no fixed cap, a memory is read whole (`max_tool_output_chars` does not apply to `memory_read`, in any form: one name, a comma list, a blank name). A positive value cuts a longer result and ends it with `[cut: N of M chars; the rest is in the memory's .md file in <dir>: read it with start_line]`. `memory:` is its own section. See [Memory](memory.md). |
2277
+ | `max_tool_output_chars` | `10000` | yes | Characters of each tool output kept in the conversation (both loops). A longer one is cut: an `execute` result keeps its first and last lines with a gap line between them and is saved whole in the session's folder (`sessions/<id>/tool_output/`; the gap line names the file, and the model reads it there instead of running the command again); any other tool's keeps its head and ends with `[cut: N of M chars; read it in parts]`. `memory_read` is the exception: `memory.read_max_chars` caps it. See [Tool output guardrails](internals/tool-output-limits.md#the-runners-cut-max_tool_output_chars). A top-level key (see below). |
2239
2278
  | `cache.warmup` | `auto` | | `auto`: after a turn, send the next turn's prompt (up to the next message) to a local llama.cpp host on the native loop, so the next turn prefills only its message; never a remote or `api: openai` host. `off`: never. See [prompt caching](internals/prompt-caching.md#the-turn-end-warm-up). |
2240
2279
  | `cache.ttl` | `5m` | | How long Anthropic keeps the prompt-cache breakpoints of a Claude model on a remote host: `5m` (Anthropic's default) or `1h`. A 1h cache write costs 2× the input price (5m: 1.25×), a read the same 0.1×. See "Remote or local". |
2241
2280
  | `cache.key` | `off` | | `session`: send the session id as `prompt_cache_key` to OpenAI's API and OpenRouter, which route and cache by it; no other host gets it. `off`: none. |
data/docs/context.md CHANGED
@@ -89,8 +89,8 @@ web) without touching the other sessions.
89
89
  ## The commands
90
90
 
91
91
  ```
92
- chi context add NAME (--cmd CMD | --push) [--every SECONDS] [--why TEXT] [--hint TEXT] TARGET
93
- chi context add URL [--why TEXT] [--hint TEXT] TARGET a URL an installed bundle's provider knows
92
+ chi context add NAME (--cmd CMD | --push) [--every SECONDS] [--why TEXT] [--hint TEXT] [--[no-]movable] TARGET
93
+ chi context add URL [--why TEXT] [--hint TEXT] [--[no-]movable] TARGET a URL an installed bundle's provider knows
94
94
  chi context push NAME [-m TEXT] [TARGET] new text for a --push source (stdin without -m)
95
95
  chi context ls [TARGET] [--format json]
96
96
  chi context show NAME [--json] [TARGET]
@@ -313,6 +313,68 @@ lower.) Every other source keeps the line above. `context.peer_turns: 0` turns
313
313
  the loop off: a peer source then wakes like any other. `context_read` names a
314
314
  peer source in its header (`Peer: your peer agent Codex's answers …`).
315
315
 
316
+ ## Movable context
317
+
318
+ A session's own sources go with it to the next session of its work: the next
319
+ link of a [chain](sessions.md#session-chains) (`chi send --new --continues`,
320
+ the web's Continue →) and a bundle's fork
321
+ ([`ctx.sessions.fork`](plugins.md#other-sessions-ctxsessions)). A topic switch goes
322
+ to a fresh session with its own context, never through a continue.
323
+
324
+ Each source says whether it moves:
325
+
326
+ - **A command source moves** (`--cmd`, a URL's source): chi runs its command
327
+ itself, in the new session too.
328
+ - **A pushed source stays** (`--push`): its writer (`pbpaste | chi context
329
+ push notes <id>`, a peer agent) holds the old session's id.
330
+ - `chi context add … --movable` / `--no-movable` (either kind) says it
331
+ outright: `--movable` on a pushed source means its writer finds the new id
332
+ itself. A bundle passes `movable:` to `ctx.context.attach`.
333
+
334
+ What moves is copied before the new session's worker starts: the source, its
335
+ last text (so its first note quotes it without waiting for a fetch, and its
336
+ interval holds), and the session's choices: every removed (declined) and
337
+ muted marker. Candidates don't move. The new link gets the usual *Attached*
338
+ note for each source; a fork starts where its seed's notes left off, so a
339
+ source that changed since gets an *Updated* note. A second chi note, after
340
+ the chain's, names what moved and what stayed:
341
+
342
+ > Context moved with this session: pr-412, ci. Left with 3f2a9c1e: notes (pushed).
343
+
344
+ `chi send --new --continues` and the web's Continue toast say the same in a
345
+ few words, and `chi context ls` marks a session's source that stays
346
+ (`· stays`; `--format json` has the derived `movable`). Project sources are
347
+ shared already, and a review fork (`chi review --from`) and a delegate take none.
348
+
349
+ A push into a continued session is refused, nothing written, with the next
350
+ link's id (`chi context push` exits 1; chi web answers 410
351
+ `context_continued`):
352
+
353
+ ```
354
+ chi context push: 3f2a9c1e was continued as 7b1d…: notes stays with the session it was attached to; attach it to 7b1d… (or pair again with 7b1d…) and push there
355
+ ```
356
+
357
+ An archived session that nobody continued takes pushes as before.
358
+
359
+ ### Peers and a continue
360
+
361
+ A peer's pairing ([claude-peer, codex-peer](plugins.md#the-claude-peer-bundle):
362
+ `<peer>-inbox` and `<peer>-outbox`, both pushed) stays with the old session,
363
+ and the peer pairs again with the new one. The continue keeps what stayed in
364
+ the new session (`left.json` beside its sources: each name, its last pushed
365
+ hint, the old id; `ctx.context.left_behind`), and the new session's worker
366
+ posts the peer one `moved` message to the inbox left behind, once:
367
+ `chi <old> · moved · continued as <new id>`. The peer's chi-peer skill pairs
368
+ again with the new id, the same way it paired before. Without an inbox (an
369
+ outbox-only pairing through a remote), or when the message didn't arrive, the
370
+ peer's next push to the old id is refused with the new id, and the skill pairs
371
+ again and pushes there. The second note says which, not whether it worked:
372
+
373
+ > Left with 3f2a9c1e: claude-inbox, claude-outbox (Claude's pairing: chi asks Claude to pair again with this session).
374
+
375
+ or, for an outbox alone, `codex-outbox (Codex pairs again when it next
376
+ answers)`. A fork takes no pairing and posts nothing.
377
+
316
378
  ## Candidates
317
379
 
318
380
  A bundle can **propose** a source instead of attaching it
@@ -359,8 +421,8 @@ review session refuses them); an unknown name, or one attached or declined
359
421
  already, is an error that says so. They are what the judge's card's buttons
360
422
  run (see [On mode](#on-mode)).
361
423
 
362
- Forks and continues start without candidates: the bundle proposes again in
363
- the new session.
424
+ Forks and continues take no candidates: the bundle proposes again in the
425
+ new session (see [Movable context](#movable-context) for the sources).
364
426
 
365
427
  ### The judge
366
428
 
@@ -370,8 +432,8 @@ about each proposed candidate — "Is this session's work about the
370
432
  candidate?" — and logs the answer, so you can see how it would do before
371
433
  anything depends on it; `audit` attaches nothing. With `on` the answer acts
372
434
  ([On mode](#on-mode)). The default is `off`: no conversation text goes to a System 1 host unless you
373
- turn this on, whatever `guardrails.system_one` says. The host and model are
374
- `guardrails.system_one.host` / `.model` (Jev through OpenRouter: see
435
+ turn this on, whatever `guardrails.system_one.mode` says. The host and model are
436
+ `system_one.host` / `.model` (Jev through OpenRouter: see
375
437
  [The candidate judge](configuration.md#the-candidate-judge)).
376
438
 
377
439
  ```yaml
data/docs/guardrails.md CHANGED
@@ -283,8 +283,9 @@ Built in, for `write`, `edit` and `memory_write` (symlinks resolved; see
283
283
  [How paths resolve](#how-paths-resolve)):
284
284
 
285
285
  - deny: the approval store's dir, installed bundles (`memories/.bundles/`),
286
- and [attached context](context.md) sources (`<state dir>/context/`: a
287
- source's command runs later, outside the gate; `chi context` changes them);
286
+ [attached context](context.md) sources (`<state dir>/context/`: a
287
+ source's command runs later, outside the gate; `chi context` changes them)
288
+ and [System 1's saved runs](system-one.md) (`<state dir>/system_one/`);
288
289
  - ask (once or for the session): `config.yml`, `home.json` (the [chi home](configuration.md#the-chi-home): the Ruby every chi launch boots), the chi shim (`$XDG_STATE_HOME/samagotchi/bin/chi`, the `chi` every tool runs: `chi-shim`) and the plain hooks dir.
289
290
 
290
291
  `execute` can still reach them; the guardrails bundle asks about shell
@@ -745,7 +746,7 @@ repo is in count as outside.
745
746
 
746
747
  `touches: chi_dirs` (shell tools) matches a command that names a path in chi's
747
748
  own folders: the config folder (`config.yml`, `memories/`), the hooks folder,
748
- the approval store, installed bundles (`memories/.bundles`), the session repo's
749
+ the approval store, attached context, System 1's saved runs, installed bundles (`memories/.bundles`), the session repo's
749
750
  git hooks (`core.hooksPath` honoured) or any `.git/hooks` (a submodule's `.git/modules/<name>/hooks` too)
750
751
  outside a tmp folder.
751
752
  Each word is read as a path against the folder the command is in by then
@@ -1045,25 +1046,32 @@ reaching them — see [Answering for the user](#answering-for-the-user-on-mode).
1045
1046
  `Samagotchi::SystemOne` (lib/samagotchi/system_one/) speaks the TypeSafe
1046
1047
  "System One" judgment API: `POST <host>/v1/systemone` with
1047
1048
  `{model, state, questions}` and answers of type `noul` (a probability),
1048
- `choice` or `score`. The host is a `hosts:` entry with `api: openai` — a
1049
+ `choice` or `score`. A `score` answer's number is not a level: it is the
1050
+ probability-weighted level index (Σ index × its probability), so a two-level
1051
+ question's score is exactly the probability of its second level. The response
1052
+ also sends `legend` (level index => name) and `probabilities` per index; chi
1053
+ keeps the score as sent and reads the level with the most probability as
1054
+ `answer.level`. A score that isn't a number, or is past the last level, is no
1055
+ answer. The host is a `hosts:` entry with `api: openai` — a
1049
1056
  local Splash server, or the hosted Jev API — and its key is that host's own
1050
1057
  `api_key_env:`.
1051
1058
 
1052
1059
  ```yaml
1060
+ system_one: # what every use of System 1 shares
1061
+ host: splash # the hosts: entry serving /v1/systemone
1062
+ model: Qwen3.8-Flash-Next-GSQ-RCO-GGUF:Q2_0 # else the host's first model
1063
+ timeout_ms: 2000 # a slower judgment is no judgment
1053
1064
  guardrails:
1054
- system_one:
1065
+ system_one: # approvals' own
1055
1066
  mode: off # off (default) | audit (judge, log, never decide) | on
1056
- host: splash # the hosts: entry serving /v1/systemone
1057
- model: Qwen3.8-Flash-Next-GSQ-RCO-GGUF:Q2_0 # else the host's first model
1058
1067
  rules: [git-push, git-rebase] # the rule ids it may answer for
1059
1068
  threshold: 0.8 # the probability an auto-allow needs
1060
1069
  min_record: 5 # clean records a rule needs before it is trusted
1061
- timeout_ms: 2000 # a slower judgment is no judgment
1062
1070
  ```
1063
1071
 
1064
- `SystemOne::Client.from_config(host_registry: ...)` reads these on every
1072
+ `SystemOne::Client.from_config(host_registry:, mode:)` reads these on every
1065
1073
  call, so a config.yml change takes effect on the next ask. `mode: off` (the
1066
- default) or no `host` means there is no client at all. Anything else is no
1074
+ default) or no `system_one.host` means there is no client at all. Anything else is no
1067
1075
  judgment and one `Log.warn(:guardrails, "system_one_failed", …)`: an
1068
1076
  unreachable host, a host that isn't `api: openai`, a status other than 200,
1069
1077
  a body that isn't JSON, a question with no answer, a request longer than
@@ -1118,7 +1126,8 @@ offers no examples. A participant's turn (review or pair) is never judged:
1118
1126
  there is no user there to ask, and its asks go to the owner. The judgment runs before the card appears, so an ask is
1119
1127
  slower by up to `timeout_ms` (2000 ms by default).
1120
1128
 
1121
- Each judgment is a line in `judgments.jsonl`, beside `approvals.json` in
1129
+ Each ask is also saved whole, with what the user answered, as a [run](system-one.md); the line's `id` is the
1130
+ run's. Each judgment is a line in `judgments.jsonl`, beside `approvals.json` in
1122
1131
  `$XDG_STATE_HOME/samagotchi/guardrails/` (`SystemOne::Log`): 0600, appended
1123
1132
  under an exclusive `flock` (several workers write it), and rewritten —
1124
1133
  keeping the newest half of its lines — once it passes 5 MB. A line is never
data/docs/hooks.md CHANGED
@@ -62,7 +62,7 @@ The plugin class must respond to `#call(event)` — duck-typed, no base class re
62
62
  | `:generation_progress` | While the response streams, in batches (see [Watching the stream](#watching-the-stream)) | `{ type: :generation_progress, iteration: N, thinking: "..." (new since the last fire), text: "..." (new visible text), thinking_chars: N, text_chars: N (this generation so far), elapsed_ms: N, restarted: true (only on the first fire after a dropped stream was asked again) }` |
63
63
  | `:before_tool_call` | Before tool dispatch (and before `tool_call_started`) | `{ type: :before_tool_call, iteration: N, call: {...}, params: "...", guardrail: Verdict, context: {...}, targets: {...}, blocked: false, block_reason: nil }` |
64
64
  | `:question_asked` | A question of the model's own (`ask_user_question`) or an approval of its tool call is about to open, on the turn's thread (not a hook's or plugin's `ask_user`, a relayed delegate approval, the step-limit question, a `--non-interactive` run or a stored approval: none of those fire it) | `{ type: :question_asked, kind: "question" \| "approval", text: "..." (the reply that asks, as the asking slot shows it: no thoughts, no tool-call markup), question: {...} (the question's fields, frozen: question/options/header…, an approval's approval: facts too; no id yet), refs: (see [Refs on a question](#refs-on-a-question-display-only)) }` |
65
- | `:after_tool_call` | After tool execution | `{ type: :after_tool_call, iteration: N, tool: "read", output: "..." (the text the model gets: capped at max_tool_output_chars; a cut one has a [cut: …] line, at its end, or for an execute result between its head and its tail, the whole output saved in the session's tool_output/ folder), status: "ok" \| "error" \| "blocked" \| "stopped", call: {...} (the call that ran, as the gate left it), elapsed_ms: N (the tool's own run, without an approval wait; nil for a denied call), result: "..." (the tool's own text: no "[read]" lead, no "ran as:" line, never cut) }`; `stopped` is a `task_wait` the turn's Stop ended (the task runs on) or whose task was stopped. Match on `result:`, not `output:`: it is what the tool said (`Error: command timed out after 120s` for an execute killed at its limit in kill mode, `Samagotchi::Tools::Execute::TIMEOUT_PREFIX`; `Still running at the 120s limit: …` for one handed over as a task, `Samagotchi::Tools::Execute::HANDED_OVER`) |
65
+ | `:after_tool_call` | After tool execution | `{ type: :after_tool_call, iteration: N, tool: "read", output: "..." (the text the model gets: capped at max_tool_output_chars (a memory_read's cap is memory.read_max_chars, 0: none); a cut one has a [cut: …] line, at its end, or for an execute result between its head and its tail, the whole output saved in the session's tool_output/ folder), status: "ok" \| "error" \| "blocked" \| "stopped", call: {...} (the call that ran, as the gate left it), elapsed_ms: N (the tool's own run, without an approval wait; nil for a denied call), result: "..." (the tool's own text: no "[read]" lead, no "ran as:" line, never cut) }`; `stopped` is a `task_wait` the turn's Stop ended (the task runs on) or whose task was stopped. Match on `result:`, not `output:`: it is what the tool said (`Error: command timed out after 120s` for an execute killed at its limit in kill mode, `Samagotchi::Tools::Execute::TIMEOUT_PREFIX`; `Still running at the 120s limit: …` for one handed over as a task, `Samagotchi::Tools::Execute::HANDED_OVER`) |
66
66
  | `:session_end` | After each turn `:after_turn` fires for, after it (turn-level lifecycle) | `{ type: :session_end, session_id: "..." }` |
67
67
 
68
68
  Every event also carries the hook runtime (next section): `hook:` (the label
@@ -76,12 +76,14 @@ cheap).
76
76
 
77
77
  ## Participants' turns
78
78
 
79
- A [participant's](internals/participants.md#turns) review turn fires the same hooks, and every event of it carries
80
- `author: "p:<id>"` (the owner's carry no `author:` key, as before); a plugin reads the same as `ctx.turn_author`
81
- (nil for the owner's turns and between turns). **A hook runs with the owner's power whatever the author**: it may
82
- read, write and run what the owner's chi may, and `notify`, `ask_user` and `steer` reach the owner. A bundle that
83
- acts on a turn's text (a prompt rewrite, a fetch of what it names) should check `author:` first: the text may be a
84
- participant's. The review boundary checks a tool call after the hooks, on the call they leave.
79
+ A [participant's](internals/participants.md#turns) turn — a review turn or a pair turn — fires the same hooks, and
80
+ every event of it carries `author: "p:<id>"` (the owner's carry no `author:` key, as before); a plugin reads the
81
+ same as `ctx.turn_author` (nil for the owner's turns and between turns). The mode does not change that: `author:`
82
+ comes from who wrote the turn, not from `ParticipantMode`. **A hook runs with the owner's power whatever the
83
+ author**: it may read, write and run what the owner's chi may, and `notify`, `ask_user` and `steer` reach the
84
+ owner. A bundle that acts on a turn's text (a prompt rewrite, a fetch of what it names) should check `author:`
85
+ first: the text may be a participant's. The participant's boundary (the review or pair one) checks a tool call
86
+ after the hooks, on the call they leave.
85
87
 
86
88
  ## What a hook can do: the runtime
87
89