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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +46 -1
- data/README.md +1 -1
- data/bin/chi +2 -0
- data/docs/architecture.md +12 -283
- data/docs/bundle-authoring.md +1 -0
- data/docs/cli.md +15 -7
- data/docs/configuration.md +57 -18
- data/docs/context.md +2 -2
- data/docs/guardrails.md +20 -11
- data/docs/hooks.md +9 -7
- data/docs/internals/attached-context.md +226 -0
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bridge.md +380 -0
- data/docs/internals/bundles.md +1 -0
- data/docs/internals/context-telemetry.md +1 -1
- data/docs/internals/engine.md +24 -18
- data/docs/internals/participants.md +13 -12
- data/docs/internals/remote-sessions.md +1 -1
- data/docs/internals/{tool-guardrails.md → tool-output-limits.md} +21 -2
- data/docs/memory.md +31 -1
- data/docs/plugins.md +122 -1
- data/docs/sessions.md +50 -26
- data/docs/system-one.md +122 -0
- data/docs/testing.md +3 -1
- data/lib/samagotchi/bridge.rb +2 -0
- data/lib/samagotchi/bundles/check-in/manifest.yml +6 -3
- data/lib/samagotchi/bundles/check-in/plugin.rb +101 -4
- data/lib/samagotchi/bundles/core/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/config.rb +74 -14
- data/lib/samagotchi/context_candidate_choice.rb +8 -0
- data/lib/samagotchi/context_candidates_log.rb +25 -1
- data/lib/samagotchi/context_judge.rb +6 -2
- data/lib/samagotchi/engine.rb +68 -7
- data/lib/samagotchi/extension_load.rb +3 -2
- data/lib/samagotchi/guardrail_wiring.rb +36 -31
- data/lib/samagotchi/guardrails/chi_dirs.rb +7 -4
- data/lib/samagotchi/guardrails/path_resolve.rb +1 -1
- data/lib/samagotchi/guardrails/protected_paths.rb +7 -4
- data/lib/samagotchi/guardrails_command.rb +3 -3
- data/lib/samagotchi/judgments_command.rb +155 -0
- data/lib/samagotchi/kernel_loop.rb +13 -1
- data/lib/samagotchi/llm/chat_loop.rb +74 -10
- data/lib/samagotchi/llm/errors.rb +7 -5
- data/lib/samagotchi/llm/stray_tool_call.rb +61 -0
- data/lib/samagotchi/log_line.rb +1 -1
- data/lib/samagotchi/memory_bundle/entry_size.rb +100 -0
- data/lib/samagotchi/plugin/api.rb +33 -2
- data/lib/samagotchi/plugin/context.rb +49 -1
- data/lib/samagotchi/plugin/judgments.rb +72 -0
- data/lib/samagotchi/plugin/loader.rb +3 -2
- data/lib/samagotchi/run_options.rb +1 -0
- data/lib/samagotchi/self_report.rb +36 -0
- data/lib/samagotchi/session_access/contract.rb +1 -1
- data/lib/samagotchi/session_commands.rb +3 -0
- data/lib/samagotchi/session_manager.rb +13 -2
- data/lib/samagotchi/session_retention.rb +10 -0
- data/lib/samagotchi/side_judgments.rb +12 -1
- data/lib/samagotchi/system_one/answer.rb +63 -11
- data/lib/samagotchi/system_one/approval_label.rb +56 -0
- data/lib/samagotchi/system_one/approval_question.rb +2 -0
- data/lib/samagotchi/system_one/client.rb +43 -17
- data/lib/samagotchi/system_one/fake_backend.rb +1 -1
- data/lib/samagotchi/system_one/http_backend.rb +12 -4
- data/lib/samagotchi/system_one/judgment_settings.rb +61 -0
- data/lib/samagotchi/system_one/judgments.rb +260 -0
- data/lib/samagotchi/system_one/log.rb +4 -2
- data/lib/samagotchi/system_one/recording.rb +117 -0
- data/lib/samagotchi/system_one/runs.rb +179 -0
- data/lib/samagotchi/system_one/runs_store.rb +230 -0
- data/lib/samagotchi/system_one.rb +9 -0
- data/lib/samagotchi/testing/bundle.rb +9 -2
- data/lib/samagotchi/testing/host.rb +2 -1
- data/lib/samagotchi/testing/judgments.rb +185 -0
- data/lib/samagotchi/testing/recorder.rb +5 -0
- data/lib/samagotchi/testing/recording_api.rb +1 -1
- data/lib/samagotchi/tool_output_cut.rb +26 -6
- data/lib/samagotchi/tool_response.rb +1 -1
- data/lib/samagotchi/tool_runner.rb +63 -4
- data/lib/samagotchi/toolkit_suggest.rb +11 -2
- data/lib/samagotchi/tools/memory.rb +28 -0
- data/lib/samagotchi/turn_note.rb +9 -0
- data/lib/samagotchi/version.rb +1 -1
- 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-
|
|
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
|
|
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-
|
|
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.
|
data/docs/system-one.md
ADDED
|
@@ -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 [
|
|
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`
|
data/lib/samagotchi/bridge.rb
CHANGED
|
@@ -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.
|
|
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:
|
|
10
|
-
requires_chi: ">= 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.
|