samagotchi 0.141.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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -1
  3. data/README.md +1 -1
  4. data/bin/chi +2 -0
  5. data/docs/architecture.md +12 -283
  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 +2 -2
  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 +1 -1
  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 +122 -1
  23. data/docs/sessions.md +50 -26
  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/core/manifest.yml +2 -2
  30. data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
  31. data/lib/samagotchi/bundles/system/manifest.yml +4 -4
  32. data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
  33. data/lib/samagotchi/bundles/system/self_map.md +1 -1
  34. data/lib/samagotchi/config.rb +74 -14
  35. data/lib/samagotchi/context_candidate_choice.rb +8 -0
  36. data/lib/samagotchi/context_candidates_log.rb +25 -1
  37. data/lib/samagotchi/context_judge.rb +6 -2
  38. data/lib/samagotchi/engine.rb +68 -7
  39. data/lib/samagotchi/extension_load.rb +3 -2
  40. data/lib/samagotchi/guardrail_wiring.rb +36 -31
  41. data/lib/samagotchi/guardrails/chi_dirs.rb +7 -4
  42. data/lib/samagotchi/guardrails/path_resolve.rb +1 -1
  43. data/lib/samagotchi/guardrails/protected_paths.rb +7 -4
  44. data/lib/samagotchi/guardrails_command.rb +3 -3
  45. data/lib/samagotchi/judgments_command.rb +155 -0
  46. data/lib/samagotchi/kernel_loop.rb +13 -1
  47. data/lib/samagotchi/llm/chat_loop.rb +74 -10
  48. data/lib/samagotchi/llm/errors.rb +7 -5
  49. data/lib/samagotchi/llm/stray_tool_call.rb +61 -0
  50. data/lib/samagotchi/log_line.rb +1 -1
  51. data/lib/samagotchi/memory_bundle/entry_size.rb +100 -0
  52. data/lib/samagotchi/plugin/api.rb +33 -2
  53. data/lib/samagotchi/plugin/context.rb +49 -1
  54. data/lib/samagotchi/plugin/judgments.rb +72 -0
  55. data/lib/samagotchi/plugin/loader.rb +3 -2
  56. data/lib/samagotchi/run_options.rb +1 -0
  57. data/lib/samagotchi/self_report.rb +36 -0
  58. data/lib/samagotchi/session_access/contract.rb +1 -1
  59. data/lib/samagotchi/session_commands.rb +3 -0
  60. data/lib/samagotchi/session_manager.rb +13 -2
  61. data/lib/samagotchi/session_retention.rb +10 -0
  62. data/lib/samagotchi/side_judgments.rb +12 -1
  63. data/lib/samagotchi/system_one/answer.rb +63 -11
  64. data/lib/samagotchi/system_one/approval_label.rb +56 -0
  65. data/lib/samagotchi/system_one/approval_question.rb +2 -0
  66. data/lib/samagotchi/system_one/client.rb +43 -17
  67. data/lib/samagotchi/system_one/fake_backend.rb +1 -1
  68. data/lib/samagotchi/system_one/http_backend.rb +12 -4
  69. data/lib/samagotchi/system_one/judgment_settings.rb +61 -0
  70. data/lib/samagotchi/system_one/judgments.rb +260 -0
  71. data/lib/samagotchi/system_one/log.rb +4 -2
  72. data/lib/samagotchi/system_one/recording.rb +117 -0
  73. data/lib/samagotchi/system_one/runs.rb +179 -0
  74. data/lib/samagotchi/system_one/runs_store.rb +230 -0
  75. data/lib/samagotchi/system_one.rb +9 -0
  76. data/lib/samagotchi/testing/bundle.rb +9 -2
  77. data/lib/samagotchi/testing/host.rb +2 -1
  78. data/lib/samagotchi/testing/judgments.rb +185 -0
  79. data/lib/samagotchi/testing/recorder.rb +5 -0
  80. data/lib/samagotchi/testing/recording_api.rb +1 -1
  81. data/lib/samagotchi/tool_output_cut.rb +26 -6
  82. data/lib/samagotchi/tool_response.rb +1 -1
  83. data/lib/samagotchi/tool_runner.rb +63 -4
  84. data/lib/samagotchi/toolkit_suggest.rb +11 -2
  85. data/lib/samagotchi/tools/memory.rb +28 -0
  86. data/lib/samagotchi/turn_note.rb +9 -0
  87. data/lib/samagotchi/version.rb +1 -1
  88. metadata +16 -2
data/docs/memory.md CHANGED
@@ -225,7 +225,7 @@ memory's line are left out), so their size is paid on every request. Nothing cap
225
225
  or trims them; chi measures them (tokens estimated as characters /
226
226
  `context.chars_per_token`) and shows the size:
227
227
 
228
- - `chi self` has a `memory index` row:
228
+ - `chi self` has a `memory index` row (and a `big memories` row, see [A memory's size](#a-memorys-size)):
229
229
  `system ~2.0k tokens (54 lines), project ~1.4k tokens (35 lines)`, with
230
230
  `, over 2500` inside the parentheses for a scope over
231
231
  `memory.index_warn_tokens`.
@@ -252,6 +252,36 @@ may each get it. Bundle installs and uninstalls and hand edits
252
252
  change index lines without a note; `chi self`'s row shows where an index
253
253
  stands.
254
254
 
255
+ ## A memory's size
256
+
257
+ `memory_read` returns a memory whole: `max_tool_output_chars` does not cut it (see [Tool output
258
+ guardrails](internals/tool-output-limits.md#the-runners-cut-max_tool_output_chars)), so each read of a memory pays its
259
+ size. Two limits stay:
260
+
261
+ - `memory.read_max_chars` (default `0`, no fixed cap) caps a `memory_read` result at that many characters.
262
+ - An overflow guard: with that at 0 and a known context window, a result over half the window is cut. A context
263
+ overflow fails the turn and rolls its work back, and a retry repeats the read. With an unknown window there is no guard.
264
+ It checks one result, not the context already used.
265
+
266
+ A cut result ends with `[cut: N of M chars; the rest is in the memory's .md file in <dir>: read it with start_line]`;
267
+ `read` the file with `start_line` for the rest.
268
+
269
+ `memory.entry_warn_tokens` (default `5000`, about 20K characters at the default `context.chars_per_token`; `0` turns it
270
+ off) is the size a write may take one memory to before the model is told. When a `memory_write` with content, or a
271
+ `write`/`edit` on a memory file, takes a memory from at or under the limit to over it, the tool result ends with one note
272
+ (a memory that did not exist counts as 0, so a first big write gets it):
273
+
274
+ > Note: memory 'NAME' is now ~N tokens (over memory.entry_warn_tokens 5000), paid on each read. When you next have a moment, tighten it; split out only parts that are read separately, since each new memory adds an index line to every prompt. Don't remove or merge memories unless the user asks.
275
+
276
+ Once per crossing, as the index's note: a write while the memory is already over gets none. None for a
277
+ description-only write, a remove, the verbatim `index` write, a model overlay, a `model_notes_*` memory (it has its own
278
+ warning), a bundle install or a hand edit. It is independent of `memory.index_warn_tokens`: with that at 0 this note
279
+ still comes, and a write that crosses both has both notes. Nothing is said when a memory is read: that would repeat on
280
+ every read. `chi self` has a `big memories` row instead: the memories over the limit in both scopes, largest first
281
+ (5, then `+N more`), `none` when there are none, a bundle's tagged `from <bundle>` (the shipped
282
+ `config_modification_protocol` is over on every install; the fix is upstream's). This is how a
283
+ memory already over the limit gets seen.
284
+
255
285
  ## Skills
256
286
 
257
287
  A **skill** is a memory named `skill_<name>` that holds the steps of a
data/docs/plugins.md CHANGED
@@ -787,6 +787,11 @@ file can't make itself a peer's, and uninstalling the bundle ends it.
787
787
  Subclasses of `Samagotchi::Plugin::Peer` (below) declare their `OUTBOX`
788
788
  source. Needs chi 0.67.0 (guard it with `chi.respond_to?(:peer_source)`).
789
789
 
790
+ ### `chi.judgment(name, questions:, positive: nil, negative: nil, shared_host: :skip)`
791
+
792
+ A cheap System 1 question this bundle asks with `ctx.judge` or `ctx.judge_later`: see
793
+ [Judgments](#judgments-chijudgment-and-ctxjudge).
794
+
790
795
  ### `chi.ctx`
791
796
 
792
797
  The plugin's context (the `ctx` its handlers get), for `register` itself:
@@ -844,6 +849,7 @@ full label, `plugin.rb (bundle my-bundle)`.
844
849
  | `ctx.stop_turn(reason)` | stop the running turn after a warn notice with the reason; true when it stopped one now. In a `:before_tool_call` block it also denies that call |
845
850
  | `ctx.stop_generation(reason)` | cut the generation that is streaming: the turn goes on and the model is asked again ([hooks.md](hooks.md#watching-the-stream)). Shows nothing: post your own notice. True when it cut one now |
846
851
  | `ctx.ask_model(messages:, prompt:, …)` | a side answer from the session's model: see [Side answers](#side-answers-ctxask_model) |
852
+ | `ctx.judge(name, state:)`, `ctx.judge_later(name, state:) { |result| … }`, `ctx.judgments` | a cheap System 1 question this bundle declared, saved as a run: see [Judgments](#judgments-chijudgment-and-ctxjudge) |
847
853
  | `ctx.http` | an HTTP client for calling services (`get`, `post_json`, `request`): see [Calling services](#calling-services-ctxhttp) |
848
854
  | `ctx.sessions` | fork, send to, read, list and stop other sessions: see [Other sessions](#other-sessions-ctxsessions) |
849
855
  | `ctx.toolkit` | the name of the session's toolkit (`full` for a session from before toolkits); nil with no session |
@@ -983,6 +989,104 @@ Each call counts in the session's side requests, under the bundle's name: `/stat
983
989
  web's ctx tooltip (`2 (btw 1, recap 1) · …`), with its tokens and its cost (reported, else estimated from the target
984
990
  model's price), never in the session's own totals.
985
991
 
992
+ ## Judgments: `chi.judgment` and `ctx.judge`
993
+
994
+ A judgment is a small question for [System 1](system-one.md), the cheap judgment model: a yes/no with a probability, a
995
+ choice, or a score, in about the time a tool call takes. Where `ctx.ask_model` gets free text from the session's model,
996
+ a judgment gets a number from the model the user set for this, and every ask is saved, so a later comparison or
997
+ fine-tune has what was sent and what came of it. It is chi's own layer (approvals use the same `Client` and runs) and
998
+ gives a bundle no new power. Needs chi 0.142.0 (guard it with `chi.respond_to?(:judgment)`).
999
+
1000
+ ```ruby
1001
+ def register(chi)
1002
+ chi.judgment "progress", questions: { "progress" => { type: "noul", instructions: "Is this turn still making progress?" } },
1003
+ positive: "keep_going", negative: "stop", shared_host: :skip
1004
+
1005
+ chi.on(:after_tool_call) do |_event, ctx|
1006
+ @run = ctx.judge_later("progress", state: { prompt: @prompt, calls: @calls }) do |result|
1007
+ next if result.no_judgment? || result.audit?
1008
+
1009
+ ctx.notify("this turn looks stuck") if result.p < 0.2
1010
+ end
1011
+ end
1012
+
1013
+ chi.command("/going", "Say the turn is fine") do |_args, ctx|
1014
+ ctx.judgments.label(@run, "keep_going", by: "command")
1015
+ nil
1016
+ end
1017
+ end
1018
+ ```
1019
+
1020
+ **It is off until the user says so.** The full name is `<bundle>.<name>` (`check-in.progress`), and it asks nothing until
1021
+ config.yml's [`system_one.judgments`](configuration.md#bundles-judgments) gives it a mode, `audit` or `on`, which it can
1022
+ also give a host and a model of its own. Neither `audit` nor `on` changes anything by itself: `result.audit?` is the bundle's
1023
+ to honour (in audit, look and don't act). A full name is dot-separated words of a-z, 0-9, `_` and `-`, so a bundle whose
1024
+ name has a capital or a dot fails to load on this call. `approvals`, `context`, `guardrails` and `toolkit` as a first word and
1025
+ `.replay` at the end are core's.
1026
+
1027
+ `chi.judgment`:
1028
+
1029
+ - `questions:` is a map of id to `{type:, instructions:, criteria:}`: `"noul"`, `"choice"` (criteria: `{name => description or
1030
+ nil}`) or `"score"` (criteria: a list of levels). Or a callable given `ctx`, called at each ask, where they vary per
1031
+ session. A plain map is checked when declared, a bad one failing the plugin's load; a callable's result at each ask, raising there.
1032
+ - `positive:` and `negative:` name the label values that say the answer was right and wrong (a word or a list). They
1033
+ are saved in each run for a later comparison, and don't limit `label`.
1034
+ - `shared_host:` says what to do while a turn runs on the same local server (the same `hosts:` entry or the same URL
1035
+ root, and not a remote one), where an ask evicts the turn's cached prompt: `:skip`, the default, saves the run with `asked: false`, so a
1036
+ later replay can answer it; `:ask` asks anyway. Between turns and on a remote host, it asks either way. A turn's hooks count as in the
1037
+ turn, `:after_turn` and `:session_end` too: an ask there would still evict the prefix the next turn wants.
1038
+
1039
+ `ctx.judge(name, state:)` asks and waits, at most `system_one.timeout_ms`. `name` is as declared, without the bundle.
1040
+ `state:` is a String, Hash or Array: what the question is about, sent to the System 1 host and saved as sent (bytes that
1041
+ aren't UTF-8 replaced). It stops when the turn is stopped, as `ctx.http` does. **It returns a Result, and doesn't raise for
1042
+ anything that goes wrong on the way:**
1043
+
1044
+ | On the Result | |
1045
+ |---|---|
1046
+ | `no_judgment?` | no answers |
1047
+ | `answers` | `{id => answer}`: `.noul`, `.choice`, `.score` (a score is the probability-weighted level index, not a level number), `.level` (the name of the most likely level, when the response has a legend), `.confidence`, `.probabilities` |
1048
+ | `p(id)` | a `noul` question's probability of yes (nil for no answer, or another type); no id: the only question's |
1049
+ | `audit?`, `mode` | the judgment's mode: `"off"`, `"audit"` or `"on"` |
1050
+ | `run_id` | for `ctx.judgments`; nil when no run was saved |
1051
+ | `reason` | why there's no answer, below; nil with answers |
1052
+
1053
+ | `reason` | When | A run? |
1054
+ |---|---|---|
1055
+ | `:off` | no mode set | no |
1056
+ | `:no_host` | no `system_one.host`, nor one of its own, or a config still on the moved `guardrails.system_one` keys | no |
1057
+ | `:no_session` | no session bound yet | no |
1058
+ | `:failed` | the host couldn't be reached, timed out, refused, or sent something unusable | yes, with `failure` |
1059
+ | `:cancelled` | the turn was stopped during the ask | yes, with `failure: "cancelled"` |
1060
+ | `:shared_host` | `shared_host: :skip`, and a turn is running on it | yes, `asked: false` |
1061
+ | `:busy` | `judge_later` only: this judgment's last one still runs | yes, `asked: false` |
1062
+
1063
+ An undeclared name or a bad `state:` (not a String, Hash or Array, or nested too deep) raises `ArgumentError`, as `ask_model` does for a bad model, and so does `ctx.judge` in a
1064
+ `:generation_progress` handler, whose stream it would hold.
1065
+
1066
+ `ctx.judge_later(name, state:) { |result| … }` asks on its own thread and **returns the run's id at once** (nil when no run will be
1067
+ saved). One per judgment runs at a time. The block is optional, and gets the Result once, on another thread, however
1068
+ the ask went, also when nothing was asked. A block that raises is logged.
1069
+
1070
+ - **The block's `ctx.*` act on the session as it is then**, not as the handler's event: the turn may have ended, or the
1071
+ worker moved to another session. Compare `ctx.session_id` with the one you started from before acting.
1072
+ - A `judge_later` still running when chi shuts down gets a second, then it and its run are lost. On `chi -p
1073
+ --non-interactive` this is the usual end of a slow ask.
1074
+ - Snapshot what you need: the block sees only what it closes over.
1075
+
1076
+ `ctx.judgments.label(run_id, value, by: nil, strength: nil, question: nil)` saves what came of a run, as a later line on it;
1077
+ `ctx.judgments.acted(run_id, what)` saves what the bundle did about it. Both return whether they saved anything. `value`
1078
+ and `what` are the bundle's own words (up to 200 characters), mapped only at analysis. `by:` says what said it, always after
1079
+ the bundle's name (`check-in:card`). `strength:` is `"strong"` (the user said so) or `"weak"` (they may have clicked out of habit).
1080
+ A `nil` id saves nothing and isn't an error, so a run that wasn't saved needs no check; an id of anything this bundle didn't declare raises `ArgumentError`.
1081
+
1082
+ **What's saved.** A run holds the state as sent: prompts, commands and whatever else the bundle put there, in the
1083
+ user's state dir, for as long as sessions last ([System 1's saved runs](system-one.md)). Nothing is saved for a scratch
1084
+ session or in a participant's turn, later lines included (those asks still go to the host). `system_one.runs: off` stops all of it.
1085
+
1086
+ **Cost.** A judgment stays out of the session's spend, as all of System 1's do, and **a bundle's judgments on a hosted host aren't under a
1087
+ cost cap**. At about $0.000015 a question that's accepted; ask at a human pace all the same, not per stream batch. The tokens a host reports are
1088
+ saved in the run (`usage`).
1089
+
986
1090
  ## Calling services: `ctx.http`
987
1091
 
988
1092
  ```ruby
@@ -1313,6 +1417,11 @@ bundle.notices.map(&:text) # also cards, steers, s
1313
1417
  bundle.questions.last.effects # an ask_user's tags as given: { "Continue" => :spend }
1314
1418
  bundle.answer({ selected: "yes" }) # the next ask_user's answer
1315
1419
  bundle.model_answers << "a summary" # the next ask_model's answer
1420
+ bundle.judgments.answer("progress", noul: 0.9) # ctx.judge's answer from now on; question: for one of several, or a block given the state
1421
+ bundle.judgments.fail("progress") # a failed backend; mode("progress", "off"), no_host("progress"), shared_host_busy too
1422
+ bundle.judgments.wait # judge_later's threads and blocks are done
1423
+ bundle.judgments.asks.last.state # the asks that reached System 1: name, state, questions, cancel
1424
+ bundle.judgments.runs("progress").last["asked"] # the runs as chi saves them; labels("progress"), acted_lines("progress") too
1316
1425
  bundle.sessions.on(:children) { |all:| [] } # ctx.sessions, scripted and recorded
1317
1426
  bundle.http.stub(:post, "http://127.0.0.1:9000/classify", json: { label: "safe" }) # ctx.http, never the network
1318
1427
  bundle.set(running: false) # the session's values: cwd:, messages:, cancelled: …
@@ -1958,6 +2067,18 @@ and goes with the session; another session starts from config.yml:
1958
2067
  | `/checkin mode ask` / `nudge` / `notify` | the mode |
1959
2068
  | `/checkin nudge` / `later` / `stop` | the card's actions, also by hand |
1960
2069
 
2070
+ **Asking System 1 (audit).** Each check-in also asks the cheap judgment model
2071
+ `check-in.progress` — "Is this turn still making progress toward what the user
2072
+ asked?" — on a side thread, with the owner's prompt, the calls since the last
2073
+ check-in (each cut as it is captured, about 4 KB in all), the count and the
2074
+ elapsed time. It is off until you give it a mode in
2075
+ [`system_one.judgments`](configuration.md#bundles-judgments), and nothing is
2076
+ acted on: the answer is saved, and so is what the user then did with the card
2077
+ (`nudge`, `keep_going`, `stop`, or `turn_stopped` when the card was still open
2078
+ when the turn ended), as a label on that run for a later look. The card, the
2079
+ nudge and the notice are the same either way. See
2080
+ [System 1's saved runs](system-one.md).
2081
+
1961
2082
  ## The slow-commands bundle
1962
2083
 
1963
2084
  `chi bundle install slow-commands` installs the bundle shipped with chi (`chi
@@ -2651,7 +2772,7 @@ for one model family, loaded into the prompt only for the models a note's
2651
2772
  prefix and against its model key, case-insensitively, and `*` also matches
2652
2773
  `/`: `*deepseek-v4*` covers `deepseek/deepseek-v4.1-flash` on any provider,
2653
2774
  MLX and GGUF names, but not `deepseek-ai/DeepSeek-R1-Distill-Qwen-32B`.
2654
- - **Tested.** `spec/model_ids.yml` lists real id forms and the exact notes
2775
+ - **Tested.** `lib/samagotchi/bundles/model-notes/spec/model_ids.yml` lists real id forms and the exact notes
2655
2776
  each loads (negative rows included); the spec installs the bundle and
2656
2777
  resolves each through `sandbox.model_notes_for`
2657
2778
  ([Testing a bundle](#testing-a-bundle-experimental)). It also checks each
data/docs/sessions.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Sessions
2
2
 
3
- Sessions are plain files — no DB. Each session is `~/.local/state/samagotchi/sessions/<uuid>.json` (XDG-aware via `XDG_STATE_HOME`) plus a sidecar dir `<uuid>/` with `input/`/`notes/`/`output/`/`owner.lock`/`bridge.json` (`Session.session_dir`), and `tool_output/` once a long `execute` result was cut: its whole output, which the cut result names (`ToolOutputFiles`, the newest 50 kept; see [Tool output guardrails](internals/tool-guardrails.md#the-runners-cut-max_tool_output_chars)). Besides the messages and turn state, the JSON keeps `written_by` (the chi version that saved it last; see *Which chi a worker runs*), `used_memory_names` (what the session read), `preloaded_memory_names` (`--memory`) and `muted_memory_names` (`--mute`), the last two written before the worker starts so a respawn builds the same prompt, `prompt_notes`, the model notes the system prompt carried at its last build (`[{name, scope, chars, digest}]`, see [Memory](memory.md#model-notes)), `parent_id`, the session that delegated this one (see *Delegating*; `null` otherwise), and `continues`, the session this one continues (the previous link of a chain, see *Session chains*; `null` otherwise); files from before those keys read as empty lists, no parent and no previous link. `model_name` is the resolved model ref (an alias applied, `host:id` when it names a host), so a resumed session keeps its model when an alias is retargeted; `model_typed` is the name it was given as when that differs (an alias), for the `models:` lookup after a resume (`null` otherwise).
3
+ Sessions are plain files — no DB. Each session is `~/.local/state/samagotchi/sessions/<uuid>.json` (XDG-aware via `XDG_STATE_HOME`) plus a sidecar dir `<uuid>/` with `input/`/`notes/`/`output/`/`owner.lock`/`bridge.json` (`Session.session_dir`), and `tool_output/` once a long `execute` result was cut: its whole output, which the cut result names (`ToolOutputFiles`, the newest 50 kept; see [Tool output guardrails](internals/tool-output-limits.md#the-runners-cut-max_tool_output_chars)). Besides the messages and turn state, the JSON keeps `written_by` (the chi version that saved it last; see *Which chi a worker runs*), `used_memory_names` (what the session read), `preloaded_memory_names` (`--memory`) and `muted_memory_names` (`--mute`), the last two written before the worker starts so a respawn builds the same prompt, `prompt_notes`, the model notes the system prompt carried at its last build (`[{name, scope, chars, digest}]`, see [Memory](memory.md#model-notes)), `parent_id`, the session that delegated this one (see *Delegating*; `null` otherwise), and `continues`, the session this one continues (the previous link of a chain, see *Session chains*; `null` otherwise); files from before those keys read as empty lists, no parent and no previous link. `model_name` is the resolved model ref (an alias applied, `host:id` when it names a host), so a resumed session keeps its model when an alias is retargeted; `model_typed` is the name it was given as when that differs (an alias), for the `models:` lookup after a resume (`null` otherwise).
4
4
 
5
5
  **Message times:** an entry of `messages` may carry `at`, when it happened: a local-offset ISO time with milliseconds (`2026-10-24T14:32:05.120+02:00`). User prompts, lines merged into a turn and steers, reminders, context and task notes, and the model's replies have one (a note's is its `created_at`); tool results, `!cmd` messages and chi's own turn notes don't, nor does anything saved before chi stamped messages (no backfill). It is for the UIs only (the gap dividers, a bubble's time): it never goes to the model, so it changes nothing in the requests or their prompt cache. The one time the model reads is the reminders': due reminders go into a turn as a system message headed `[SYSTEM: REMINDERS DUE 2026-10-24 14:32 local]` (the turn's local time; `ReminderQueue#inject!`), written into the saved content then, so a later request sends the same text.
6
6
 
@@ -45,6 +45,8 @@ A new session field (the rule in `Session::FIELDS`' comment): only top-level key
45
45
 
46
46
  <a id="archiving-a-session"></a>**Archiving a session:** `chi sessions archive ID...`, `/archive` in a terminal and the Web UI's `archive` go through `SessionManager.archive_session`: it writes `<id>/archived` (`{"archived_at": …}`; not a session.json field, which every save rewrites) for the session and its delegated children. `Session.list` leaves archived sessions out unless `include_archived:`, and everything built on it follows: `chi sessions list` (`--archived` shows them, `[archived]`), the summaries (`--live`, json/tsv, the desktop panel, the agent's `list_sessions`) and the retention prune, which neither deletes nor counts them against `max_count`. `children_of` (max_children, `delegate_result`) still sees them, and `--resume`/`--attach ID`, `chi send`/`chi note ID` reach them as before. Refused: a turn running in the session or in one of its children (naming the child; 409 `busy` on the web), a prompt queued in `input/` (it would run on the next resume) or a live worker's step-limit question still open, in it or a child (409 `busy` too), a plain REPL owning it (`owned_by_tui`), a `chi scratch` session (`scratch`). A live idle worker is stopped first, and the marker is written even if it is still shutting down. Input a human typed brings a session back (`{"unarchived_at": …}`): a prompt from the web, an attached or plain terminal or `chi send` (origin `web:*`, `tui:*`, `cli:send`, none), steering merged into a running turn, a `/continue` answer or a question answered (the step-limit question too); a delegate's follow-up, a plugin's turn, a reminder, the idle recap, a note and just opening it don't. A child's input brings only the child back. The retention sweep ages an unarchived session from when it was unarchived, so unarchiving an old session doesn't let the next sweep take it. `/archive` leaves the session first, as `/exit` does, then archives it: in an attached terminal the worker is asked to exit (one that stays up for another UI is stopped by the archive; a running turn, a queued prompt or an open step-limit question refuses it), and an empty session is discarded instead; `chi scratch` refuses it. Typed while a turn runs, `/archive` waits for the turn's end (an attached terminal for the commands queued behind it too) and then archives. `chi sessions unarchive ID...` and the web's `unarchive` bring back the whole family. Web routes: `POST /api/sessions/:id/archive` (200 `{status, session_id, archived, stopped, discarded}`, ids) and `POST /api/sessions/:id/unarchive` (200 `{status, session_id, unarchived}`). The session hub and `GET /api/sessions` keep archived sessions, `archived: true` in their summaries: the page leaves them out of the strip, its count and the list at render, and the `archived` toggle beside the all-sessions search shows them, dimmed with an `archived` badge. The info bar marks a session whose live worker runs an older chi than the newest installed (`worker_version` in its summary): `chi 0.18.1 · Restart`, which asks to confirm and calls `POST /api/sessions/:id/restart` (a refusal's reason shows in a toast); a worker from before restarts shows `chi sessions stop ID` instead. The info bar reads `continue → · archive · stop · delete` (`unarchive` on an archived one; no `continue →` on a link continued already, see *Session chains*; `continue →` is a plain button like `archive`, in the accent on an archived session, where it is the thing to do); archiving the open session stays on it, with an Undo toast.
47
47
 
48
+ **System 1's saved runs** last as long as sessions: the sweep ages them by day file, and deleting a session purges its runs ([system-one.md](system-one.md)).
49
+
48
50
  **Lazy sweep:** automatic prune runs only while `chi web` runs, at most once per `session.sweep_interval_hours` (default 24): the web's session hub gives it a chance on its periodic full scan, and `GET /api/sessions` does too. No cron, and a terminal-only chi never sweeps. Manual prune is always available.
49
51
 
50
52
  <a id="cli"></a>**CLI:**
@@ -111,6 +113,51 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
111
113
  - A `chi scratch` session (`"scratch": true`) is deleted when its REPL ends; one a killed process left behind is deleted by the next sweep, `prune` or `clean`, whatever its age or status, once nobody owns it. `list` shows it as `[scratch]` (every text form; `scratch: true` in `--format json`); `chi web` never shows one. See [CLI: Scratch sessions](cli.md#scratch-sessions).
112
114
  - For ad-hoc manual QA use `SAMAGOTCHI_ENV=test XDG_STATE_HOME=/tmp/chi-test-$USER chi ...` to isolate from real state; the flag also marks sessions that `chi web` or an attached `chi` spawn (their workers inherit the environment), so `clean` finds them if they land in the real state.
113
115
 
116
+ ## Session file
117
+
118
+ `Session::FIELDS` is the table of `<uuid>.json`, in the order `Session#save` writes it (`Session.from_h` and `#to_h` both go by it; 41 keys). A key a file lacks reads as the default below; `review`, `first_turn_id`, `verdicts` and `verify_fresh` are written only when set (`OPTIONAL_FIELDS`). A key this chi doesn't know is kept and written back as it was (see the rule above). "Who writes it": the Engine runs in the session's worker (or the REPL); the rest are named.
119
+
120
+ | Key | Meaning | Written by |
121
+ |-----|---------|------------|
122
+ | `metadata_version` | The file format version (`METADATA_VERSION`, 3). | `Session#save` |
123
+ | `written_by` | The chi version that saved the file last (see *Which chi a worker runs*). | `Session#save` |
124
+ | `id` | The uuid; also the file name. Required. | `Session.new_session` |
125
+ | `mode` | `assist`, the only mode. Required. | `SessionManager.spawn_session` |
126
+ | `model_name` | The resolved model ref (an alias applied; `host:id` when it names a host), so a resume keeps the model. Required. | creation, `/model` |
127
+ | `model_typed` | The name it was typed as when that differs (an alias), for the `models:` lookup after a resume; `null` otherwise. | creation, `/model` |
128
+ | `working_directory` | The folder the session runs in. Required. | creation |
129
+ | `messages` | The conversation (symbol keys in memory, strings on disk). While a turn runs, a save writes the turn's checkpoint here instead (`TurnCheckpoint`). | the Engine, per turn |
130
+ | `created_at`, `updated_at` | ISO times; `updated_at` is stamped by every save and orders the lists. Required. | `Session#save` |
131
+ | `status` | `idle`, `running`, `error` or `stopped` (turn state, not liveness; a stop is also the `<id>/stopped` marker). | the worker / Engine; the marker by `chi sessions stop` |
132
+ | `last_prompt` | The newest prompt; a session created with a first prompt holds it here until its worker takes it; after an `error` status, the reason. | `SessionManager.spawn_session`, the Engine, `Session.mark_error` |
133
+ | `first_preview` | The first user message as a one-line preview (whitespace collapsed, 80 characters), or the first context note's (`note: …`) until a message is typed. Cached so lists and `Session.resolve_id`'s ambiguity message need not read `messages`. | `Session#save` (`compute_first_preview!`) |
134
+ | `title` | The name the user gave (`/title`, whitespace collapsed, 80 characters); shown in place of `first_preview` (`display_title`); a continue's new link inherits it. The recap's auto title is not stored here. See [Session titles](#session-titles). | `/title` (`TitleCommand`), `SessionManager` |
135
+ | `test_run` | Tagged by `SAMAGOTCHI_ENV=test`, `RACK_ENV=test` or `CI` (see Test-session hygiene). | creation |
136
+ | `pending_question` | The question the worker waits on (`PendingQuestion`), `null` with none. | `QuestionDesk` |
137
+ | `used_memory_names` | The memories the session read. | the Engine |
138
+ | `project_root` | The project the session started in, stored because a worktree may be deleted later; `null` outside a repo. | creation |
139
+ | `preloaded_memory_names`, `muted_memory_names` | `--memory` and `--mute`, written before the worker starts so a respawn builds the same prompt. | creation |
140
+ | `prompt_notes` | The model notes the system prompt carried at its last build (`[{name, scope, chars, digest}]`, see [Memory](memory.md#model-notes)). | the Engine, at each prompt build |
141
+ | `parent_id` | The session that delegated this one or forked it (`ctx.sessions.fork`); `null` otherwise. See [Delegating](#delegating). | `SessionManager.spawn_session`; `ChildMove` writes a `parent.json` beside it instead of this file |
142
+ | `scratch` | A `chi scratch` session, deleted when its REPL ends. | creation |
143
+ | `last_turn` | How the last turn ended (`LastTurn`), for a parent's wait and the web's notifications. | the Engine, at a turn's end |
144
+ | `verdicts` | Verdict cards recorded by completed Verify turns, in append order. | `Engine#record_verdict`, `VerifyFresh` |
145
+ | `delegate` | Started by the `delegate` tool (a fork has a `parent_id` but isn't one). | `Tools::Delegate` via `spawn_session` |
146
+ | `llm_context` | The session's own LLM context values (strategy, apply rule, budget), over the model's; `null` with none. | creation (`--llm-context`), `/llm-context`; copied by a fork and a continue, not a delegate child |
147
+ | `continues` | The previous link of a chain; `null` otherwise. See [Session chains](#session-chains). | `SessionContinue` |
148
+ | `thinking` | The session's own thinking level (`off`/`low`/`medium`/`high`), over the process default and the model's. | creation (`--thinking`), `/thinking`; kept by a continue and a fork |
149
+ | `reasoning_history` | Its own `reasoning_history` level (`drop`/`turn`/`all`). | `/reasoning-history`; carried like `thinking` |
150
+ | `appended_prompt_files` | The absolute paths of the files `--append-system-prompt` put into the system prompt (config.yml's `append_system_prompt:` entries are not stored); kept by respawns, copied by a fork and a continue (`SessionSetup`). | creation |
151
+ | `toolkit` | The toolkit it was created on, as named (`{name, add, remove}`); `null` is `full`. See [Toolkits](configuration.md#toolkits). | creation, `/toolkit` |
152
+ | `toolkit_origin` | Why it is on that kit: `default`, `chosen` (also the reading of a file from before the field), `inherited` or `suggested`. | creation, `/toolkit`, the toolkit suggestion |
153
+ | `bundle_settings`, `config_settings` | `--set`: `BUNDLE.KEY` values by bundle and the session-settable core keys; kept by a respawn, a continue and a fork, not by a delegate child. | creation |
154
+ | `interrupted_turn` | The mark of a turn a worker checkpointed and never finished (`InterruptedTurn`); the next worker settles it and sets it to `null`. | the worker |
155
+ | `parent_answers` | Its own `guardrails.parent_answers` (it may only tighten config.yml's). | creation (`--parent-answers`), `/parent-answers` |
156
+ | `working_set_pins` | The working-set items the user pinned (`/working-set pin`); kept by a respawn, a continue and a fork. | the Engine's command |
157
+ | `review` | A review session's subjects and source (`SessionReview`), see [Review sessions](#review-sessions). | `ReviewStart` |
158
+ | `first_turn_id` | The id the creating prompt's turn gets, so a wait can name it (`chi send --new --wait`, `--wait --turn T ID`); `null` for a session created idle. | `SessionManager.spawn_session` |
159
+ | `verify_fresh` | A Verify fresh child's requester, rules and claim (`VerifyFreshOrigin`). | `VerifyFresh.start`, once |
160
+
114
161
  ## Context notes
115
162
 
116
163
  A context note is text pushed into a session as background: not a prompt, and it starts no turn.
@@ -311,30 +358,7 @@ sessions](cli.md#review-sessions)); both start idle and print the `chi share` li
311
358
 
312
359
  ## Toolkits
313
360
 
314
- A session file's `toolkit` (`{"name", "add", "remove"}`) is the toolkit it was created on, as named: config's
315
- `toolkit.default`, `toolkit.child` for a delegate child, or `chi --toolkit` / `chi send --new --toolkit` (with
316
- `--tool` / `--no-tool`). It is never resolved again from config: a respawned or woken worker declares the same
317
- tools. `null` (a session from before toolkits) is `full`. A continue (`continue →`, `chi send --new --continues`)
318
- and a fork (`ctx.sessions.fork`) keep it, as they keep the thinking level; a delegate child gets the kit its
319
- `delegate` call names (`toolkit:`, any kit the parent knows) or `toolkit.child`; one its worker can't resolve
320
- declares every tool (full). The file stores only the name: a kit's tools and prompt text come from its definition.
321
- `/toolkit` changes it, saved at once, from the next turn's start. See
322
- [Toolkits](configuration.md#toolkits).
323
-
324
- `toolkit_origin` records why the session is on that kit: `default` for the configured kit, `chosen` for a
325
- user-selected kit (including `/toolkit` and `ctx.use_toolkit`), `inherited` for a continue or fork, or `suggested`
326
- when `toolkit.suggest.mode: on` switched it ([Toolkits](configuration.md#toolkits)). A session file from before the
327
- field reads as `chosen`. Only a `default` session is ever suggested a kit.
328
-
329
- A `find_tools` result on a chat host saves the tools its hit loads as `loaded: ["list_sessions", "mcp_fs_read"]` on
330
- its `tool_response` entry (wire names: a built-in's, an MCP tool's `mcp_<server>_<tool>`), whatever the host: a
331
- later turn on `api: anthropic` (after a `/model` switch, a resume) sends the load as references, any other host
332
- ignores it. The names stay deferred for the session's life, across respawns, `--resume` and forks.
333
-
334
- A plugin's `ctx.load_tools` leaves a system entry `{"role": "system", "kind": "tool_change", "content": "[tools loaded:
335
- delegate (bundle)]", "tool_change": {"add": ["delegate"], "by": "bundle"}}` after the turn's user message, only on a
336
- host that takes tool changes (`api: anthropic`). It is never the system head, counts as conversation, is hidden in
337
- the web and TUI histories, and goes to no other wire; its names stay deferred for the session's life like a load's.
361
+ A session's `toolkit` and `toolkit_origin` are in the [Session file](#session-file) table. What a toolkit is, how it is saved at creation, what a continue, a fork and a delegate child get, `/toolkit`, and how `find_tools` loads and `ctx.load_tools` tool changes are saved in the conversation: [Toolkits](configuration.md#toolkits).
338
362
 
339
363
  ## Parent answers
340
364
 
@@ -425,7 +449,7 @@ mise.toml pins Ruby 3.4 gets that `ruby`, `gem` and `bundle` in its tools while
425
449
 
426
450
  ## Delegating
427
451
 
428
- The `delegate` tool hands a task to a **child session**: an ordinary chi session in a worker of its own, started in the parent's `working_directory` with the task, verbatim, as its first user message, on the parent's model unless the call names one (`model:` takes a name or an alias, resolved as `--model` is; an unknown host comes back as the tool's `Error:` line; an id the host's saved model list doesn't have starts the child with a `Warning: host 'h' doesn't list model 'x' (did you mean: …?)` line in the result). Children run in parallel and only their **final reply** comes back to the parent: the child's newest `output/<timestamp>.txt` file, which the worker writes at the end of each turn that produced visible text, cut head-and-tail above 64 KB (`DelegateWait.cut`); a reply longer than `max_tool_output_chars` keeps its start and its end (the conclusion), with a gap line naming that `output/` file, where the whole reply is (`ToolOutputCut`'s `:head_tail_ref`; see [Tool output guardrails](internals/tool-guardrails.md#the-runners-cut-max_tool_output_chars)). Nothing of the child's trace enters the parent's context. Nothing is hidden: a child shows in `chi sessions list` (`↳ <parent>`), the web (folded into its parent's card on the strip and in all sessions, whose `▸ N delegates` chip lists the delegates at any depth, see Web UI above; a card of its own with a `↳` chip when its parent isn't listed; `↑ <parent id>` in its info bar, its tooltip `delegated by session …`; the parent's info bar carries a `⑂ N` chip counting its delegates, with how many run, wait, are done, failed, stopped or idle in the tooltip and the waiting ones named in the attention colour; a click opens all sessions), the terminal's status row (a parent's `⑂ 2 running · 1 waiting`, see [Status Line](cli.md#status-line)) and the `list_sessions` tool (`child`; the parent shows as `parent` in the child's own list); the user can `chi --attach <child id>` and steer it mid-run, and `chi send` and `send_note` reach it like any session.
452
+ The `delegate` tool hands a task to a **child session**: an ordinary chi session in a worker of its own, started in the parent's `working_directory` with the task, verbatim, as its first user message, on the parent's model unless the call names one (`model:` takes a name or an alias, resolved as `--model` is; an unknown host comes back as the tool's `Error:` line; an id the host's saved model list doesn't have starts the child with a `Warning: host 'h' doesn't list model 'x' (did you mean: …?)` line in the result). Children run in parallel and only their **final reply** comes back to the parent: the child's newest `output/<timestamp>.txt` file, which the worker writes at the end of each turn that produced visible text, cut head-and-tail above 64 KB (`DelegateWait.cut`); a reply longer than `max_tool_output_chars` keeps its start and its end (the conclusion), with a gap line naming that `output/` file, where the whole reply is (`ToolOutputCut`'s `:head_tail_ref`; see [Tool output guardrails](internals/tool-output-limits.md#the-runners-cut-max_tool_output_chars)). Nothing of the child's trace enters the parent's context. Nothing is hidden: a child shows in `chi sessions list` (`↳ <parent>`), the web (folded into its parent's card on the strip and in all sessions, whose `▸ N delegates` chip lists the delegates at any depth, see Web UI above; a card of its own with a `↳` chip when its parent isn't listed; `↑ <parent id>` in its info bar, its tooltip `delegated by session …`; the parent's info bar carries a `⑂ N` chip counting its delegates, with how many run, wait, are done, failed, stopped or idle in the tooltip and the waiting ones named in the attention colour; a click opens all sessions), the terminal's status row (a parent's `⑂ 2 running · 1 waiting`, see [Status Line](cli.md#status-line)) and the `list_sessions` tool (`child`; the parent shows as `parent` in the child's own list); the user can `chi --attach <child id>` and steer it mid-run, and `chi send` and `send_note` reach it like any session.
429
453
 
430
454
  - `delegate(task, model:, session:, cwd:, wait:, timeout:)`: starts a child and, by default, waits for its reply. `wait: false` is the usual way for work that takes a while, several children side by side, or to keep talking with the user: it returns at once, the parent ends its turn or works on, and the reply comes back by itself as a *delegate report* (below), in a turn of its own if the parent is idle. The tool descriptions tell the model so, and to wait (`wait: true`, `delegate_result`) only when its turn can't go on without the reply. `timeout`, default 600 s, leaves a slow child running; 0 checks once without waiting. With `session:` (a child's id or prefix) the task goes to that child as a follow-up instead, delivered like `chi send` (client id `delegate:<parent short id>`, shown as `delegate>` in the child's UIs); only this session's children take one. The wait (`wait: true`, a later `delegate_result`) and the delegate report are for the reply of the child's turn the follow-up went to: its running turn, which it merges into, or the turn of its own it runs when the child is idle (`turn` in `delegates.json`, set when the child's worker names `turn_ids`), not a newer reply of a turn queued behind it. A follow-up merged as a steer into a turn the child started at that moment (a wake, a reminder, another message) gets that turn's reply or question. A child on its first turn, or with an older worker, gives its next reply, as before.
431
455
  - `cwd:` starts the child in another folder of the parent's repository: a linked worktree (`git worktree add ../<repo>-<task> -b <branch>`, which the model runs with `execute` first, under guardrails; chi runs no git here) or a subfolder; a relative path is taken from the parent's folder. The folder must exist, have the parent's project root (the same common git dir, `MemoryPaths.project_root`) and be in a work tree: a git dir (`.git`, `.git/objects`, a bare layout's `.bare`) or a bare layout's container is refused (`git rev-parse --is-inside-work-tree`, read-only, decides). Outside git only a subfolder of the parent's folder is allowed. Otherwise the tool returns `Error: cwd must be a folder of this session's repository …` or `… is not in a work tree …`. A follow-up (`session:`) refuses `cwd:`: the child keeps its folder. The child's guardrails measure from its own worktree, and project memories, `list_sessions` and the web's project are the same as the parent's.
@@ -0,0 +1,122 @@
1
+ # System 1's saved runs
2
+
3
+ System 1 is the cheap judgment model chi asks small questions: approvals
4
+ ([guardrails.md](guardrails.md#system-1-experimental)), toolkit suggestions
5
+ ([configuration.md](configuration.md#toolkits)), context candidates
6
+ ([context.md](context.md)) and bundles' own ([plugins.md](plugins.md#judgments-chijudgment-and-ctxjudge)). Each use keeps its own mode, gates and log. What they share is
7
+ one host (`system_one.host`, `.model`, `.timeout_ms`) and one layer that saves every ask, so a later comparison or fine-tune has what
8
+ was sent and what came of it. The runs don't leave the machine; a hosted System 1 host sees each state as it is asked.
9
+
10
+ ## The common layer
11
+
12
+ `SystemOne::Client` asks. `SystemOne::Recording` wraps it where a use builds its
13
+ client (`Engine#system_one_recording`): the same duck type, the same answers and
14
+ the same raises. A cancelled ask saves its run, then raises; a run that can't be saved is
15
+ one warn and never changes the answer. A use that needs the run's id at once passes it
16
+ (`client.ask(..., run_id: id)`); one client serves several threads.
17
+
18
+ | Judgment | Asked by | Later lines |
19
+ |---|---|---|
20
+ | `approvals.allow` | `GuardrailWiring#system_one_judge` | gates, acted (the judgment line's decision), labels |
21
+ | `toolkit.suggest` | `ToolkitSuggest.judge` | acted (the log's `did`), no labels |
22
+ | `context.candidate` | `ContextJudge.ask` | labels |
23
+ | `<bundle>.<name>` | `SystemOne::Judgments` (`ctx.judge`, `ctx.judge_later`) | the bundle's: labels, acted |
24
+
25
+ Not saved: a toolkit warm-up (it goes straight to the backend), `chi guardrails replay`,
26
+ chi web's availability check, a scratch session's asks or anything about them, and a participant's turn (never judged by core; a bundle's ask there still goes, unsaved).
27
+ An owner's turn in a review session is judged, so it is saved. Test runs are saved with `test_run: true`.
28
+
29
+ ## Bundles' judgments
30
+
31
+ A bundle declares a judgment with `chi.judgment` and asks through `SystemOne::Judgments`, the Engine's (`Plugin::Host#judgments`), on the
32
+ same `Client` and `Recording`: the API is in [plugins.md](plugins.md#judgments-chijudgment-and-ctxjudge), the settings in
33
+ [configuration.md](configuration.md#bundles-judgments).
34
+
35
+ Only bundles' judgments have settings of their own (`JudgmentSettings`). Core's take no host, model or mode here, whatever
36
+ `system_one.judgments` says: `Record` and `Similar` are model-blind (`Record#initialize` filters by rule only), so a model swapped
37
+ for approvals alone would inherit a clean record and could auto-allow. Before any such swap, `Record` and `Similar` have to go by model.
38
+
39
+ Core's uses keep their own threads (`ToolkitSuggest::Pending`, `ContextJudge`, the approval path) and contention; only the ask-and-save
40
+ seam is common. A bundle's `judge_later` runs on the Engine's `SideJudgments` under its full name as the kind, so `SideJudgments#join`
41
+ at shutdown covers it, and a run cut by that one-second limit is lost. Its asks stay out of the session's spend, as all of System 1's do, and no
42
+ cost cap covers them on a hosted host.
43
+
44
+ ## The store
45
+
46
+ `<state dir>/system_one/runs/<judgment>/YYYY-MM-DD.jsonl` (`SystemOne::RunsStore`): one file per UTC day,
47
+ 0600 in 0700 folders, one append under `flock` per line. Each judgment's questions are stored
48
+ once per digest pair in `versions.jsonl` beside them.
49
+
50
+ A line over 16 KB has its state cut, as a string, and is marked `truncated`.
51
+
52
+ Agents can't write here: `write`, `edit` and `memory_write` are denied (`system-one-runs`), a shell
53
+ command naming the folder is `touches: chi_dirs`, and it is in `ChiDirs.protected`, so only the user
54
+ may answer an ask about it. A label here may one day train a model.
55
+
56
+ ### What a run holds
57
+
58
+ A `run` line: `id`, `judgment`, `at`, `instructions_digest`, `criteria_digest`, `state` as sent, `asked`,
59
+ `origin` (`live`), `mode` (the use's own: toolkit and context build an `on` client in audit too),
60
+ `host`, `model` (resolved), `served_model`, `answers` (raw, as the backend sent them), `usage`,
61
+ `failure` and `failure_text`, `latency_ms`, `session_id`, `chain_link`, `session_model`, `test_run`, `truncated`.
62
+ Nil fields are left out. A bundle's run may add `positive` and `negative`, the label values it declared.
63
+
64
+ A skipped ask is a run too, written at once: `asked: false` and `skipped` (`shared_host`, or `busy` for a `judge_later` whose last one still
65
+ runs), with its state, host and model but no answers, so a later replay can answer it with no eviction today.
66
+
67
+ **That means prompts, commands, diffs and any secret in them are in these files.** A state is whatever the
68
+ use sends System 1: an approval's command (up to 2000 chars), diff and earlier examples, a toolkit's
69
+ first prompt, a context candidate's owner prompts. **check-in's sends tool output** — the calls of a turn
70
+ since its last check-in, each cut as it is captured, and what each tool returned — the first class of data
71
+ approvals and context never send: on a hosted `system_one.host` it reaches that host with the ask, so a
72
+ hosted System 1 sees the machine's tool output, not just what the agent asked about.
73
+
74
+ `chain_link` is the session's parent, else the session it continues. It isn't the chain's root: following a
75
+ chain takes a session listing (`SessionChain`), too slow on an approval's path. An export splitting by chain
76
+ follows the links.
77
+
78
+ A run's id is `<judgment>/<uuid>`, carrying its judgment. Approvals use it as the judgment line's id in
79
+ `judgments.jsonl`, so `verdict.judgment[:id]` and `/guardrails wrong` carry it with no new field; toolkit's
80
+ and context's log lines have it as `run_id`. An id from before runs names no judgment and labels no run.
81
+
82
+ ### Later lines
83
+
84
+ Later lines name their run by `run_id` and may sit in a later day's file, or in theory before
85
+ the run: a join happens at read, and a line whose run is gone is dropped.
86
+
87
+ - `label` {`value`, `by`, `strength`, `question`, `at`}.
88
+ - Approvals (`SystemOne::ApprovalLabel`, the one reading of an answer for the log and the run):
89
+ `allow_once`, `allow_wide`, `deny` by `user`, strong; `should_have_asked` from `/guardrails wrong` or the
90
+ web's **should have asked**, strong; `deny` by `parent`, weak, only where the parent could have allowed; a
91
+ parent's allow is no label; `none` for a cancelled ask, which is not an answer.
92
+ - Context: `attached`, `declined` by `command` (`/context attach|decline`, the card's buttons), `declined`,
93
+ `detached` by `user` (a removal seen at an owner turn's start), on the latest `judged` line's run, question
94
+ `about`. A chip removed after the last turn isn't seen, and an attach by hand is no label.
95
+ - Toolkit has none: `did: chosen` doesn't say what was picked. "Covers" stays derived in `chi toolkit report`.
96
+ - A bundle's: its own word, raw, by `<bundle>` or `<bundle>:<what>`, through `ctx.judgments.label`. A bundle reaches only its own runs.
97
+ check-in's are the card's actions that took effect (`nudge`, `keep_going`, `stop`, weak — a Nudge asks for a status, not a
98
+ verdict, and Keep going may be habit) and `turn_stopped` by `turn` when the turn ended with the card still open.
99
+ - `acted` {`what`, `at`}: approvals' decision (`auto`, `asked`, `audit`), toolkit's `did`, a bundle's own word.
100
+ - `gates` {`rule`, `rule_listed`, `self_protect_refused`, `threshold`, `at`}: approvals', at ask time,
101
+ since neither can be recomputed later.
102
+
103
+ Labels are biased, and a later comparison must handle it: an auto-allowed call has no answer unless
104
+ flagged, and a stored wide approval never reaches the judge at all.
105
+
106
+ ## Retention and deleting
107
+
108
+ Runs last as long as sessions. The session sweep deletes day files older than `session.retention_days` (0: forever),
109
+ then the oldest until the total is under 100 MB; today's stay. A judgment with no day file left loses its folder.
110
+
111
+ `chi sessions delete` (and the web's delete, and `/exit --delete`) purges that session's runs and their later lines; a sweep
112
+ doesn't, and neither does discarding a session its worker left empty. A purge can't reach text a later run of another session
113
+ embedded: an approval's state carries earlier asks as examples.
114
+
115
+ To stop saving: `system_one.runs: off` in config.yml (what's there still ages out). To delete it all:
116
+ `rm -rf <state dir>/system_one`.
117
+
118
+ ## Looking at it
119
+
120
+ `chi judgments ls` lists each judgment's digests, runs, labels (each run's last, per question), failures, models and size. `chi judgments report NAME
121
+ [--days N]` gives one judgment's counts, p histogram and labels by value (each run's last, per question). Both only read.
122
+ A use's own report stays: `chi context report`, `chi toolkit report`, `/guardrails`.
data/docs/testing.md CHANGED
@@ -40,6 +40,8 @@ that way locally: `npx playwright run-server --port 3333`, then `PW_TEST_CONNECT
40
40
 
41
41
  No spec reads your own setup. The suite points `XDG_CONFIG_HOME` and `XDG_STATE_HOME` at temp folders,
42
42
  clears your `SAMAGOTCHI_*` environment, and keeps every spec off the network (WebMock).
43
+ `SAMAGOTCHI_TAILSCALE_CLI` points at a file that does not exist (`spec_helper.rb`), so no spec reaches a real `tailscale`; a
44
+ `chi web --tunnel tailscale` spec writes a fake one and sets the variable.
43
45
 
44
46
  Goldens (`*_golden_spec.rb`, the prompt snapshots) hold no machine state, so they pass on any runner: no absolute
45
47
  paths (replace them with placeholders), no PATH tools (the suite pins `rg` as found, `spec_helper.rb`; an example
@@ -194,7 +196,7 @@ callback the spec can wait on), not another local fix in the spec.
194
196
  ### Talking to a real Bridge
195
197
 
196
198
  A session's Bridge answers only a request that names its secret (`X-Chi-Bridge`, from the worker's `bridge.json`;
197
- see [architecture.md](architecture.md)). A spec that writes its own request to a real Bridge (`Net::HTTP`, a raw
199
+ see [internals/bridge.md](internals/bridge.md)). A spec that writes its own request to a real Bridge (`Net::HTTP`, a raw
198
200
  socket) names it too: `include BridgeAuth` (spec/support/bridge_auth.rb, required by the spec that needs it) gives
199
201
  `bridge_auth(session_dir_or_sidecar)`, the header as a Hash, and `bridge_auth_line(...)` for a raw request; or read
200
202
  `port` and `secret` from the sidecar in one go, as `WorkerLink.client` does. A client made with `BridgeClient.new`
@@ -23,6 +23,7 @@ require_relative "bridge/turn_accumulator"
23
23
  require_relative "session"
24
24
  require_relative "engine"
25
25
  require_relative "system_one/log"
26
+ require_relative "system_one/approval_label"
26
27
  require_relative "session_commands"
27
28
  require_relative "queued_command"
28
29
  require_relative "steer"
@@ -815,6 +816,7 @@ module Samagotchi
815
816
  return [{}, 409, { error: "already_labelled" }] unless entry["answer"].nil?
816
817
  return [{}, 500, { error: "label_failed", detail: "could not write the System 1 label" }] unless log.label(judgment_id, "should_have_asked")
817
818
 
819
+ SystemOne::ApprovalLabel.save(SystemOne::Runs.for_sessions_dir(@state_dir), judgment_id, SystemOne::ApprovalLabel::WRONG)
818
820
  [{}, 200, { status: "labelled", id: judgment_id }]
819
821
  end
820
822
 
@@ -1,10 +1,13 @@
1
1
  ---
2
2
  name: check-in
3
- version: 0.2.4
3
+ version: 0.3.0
4
4
  scope: system
5
5
  description: "Checks on a long turn: after N tool calls with no answer, a card asks the user (Nudge / Keep going / Stop), or the model is nudged to say what it has found"
6
6
  trust_level: reviewed
7
7
  plugin:
8
8
  file: plugin.rb
9
- sha256: sha256:42fe075c664d256ecb8623b76a25d5c389e42c0534a41c56fe17b073d521e095
10
- requires_chi: ">= 0.3.0"
9
+ sha256: sha256:fbb7165631c8a8096d900511a95874fe61e72ad8c6f1b45c5c829a285084f25b
10
+ requires_chi: ">= 0.142.0"
11
+ # check-in.progress asks System 1 (chi.judgment, new in 0.142.0): the marker
12
+ # below raises the floor at the release (docs/releasing.md), since chi on main
13
+ # is 0.141.0 and would otherwise refuse its own bundle.