samagotchi 0.59.0 → 0.61.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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +89 -1
  3. data/README.md +3 -2
  4. data/bin/chi +23 -2
  5. data/docs/architecture.md +11 -6
  6. data/docs/cli.md +9 -3
  7. data/docs/configuration.md +62 -12
  8. data/docs/hooks.md +82 -5
  9. data/docs/internals/engine.md +50 -13
  10. data/docs/internals/gemma4-contract.md +1 -0
  11. data/docs/internals/prompt-caching.md +6 -3
  12. data/docs/memory.md +1 -1
  13. data/docs/plugins.md +57 -4
  14. data/docs/sessions.md +7 -3
  15. data/docs/sub-agent.md +2 -2
  16. data/docs/testing.md +4 -2
  17. data/lib/samagotchi/answer_display.rb +4 -3
  18. data/lib/samagotchi/answer_refs.rb +210 -0
  19. data/lib/samagotchi/bootstrap_command.rb +2 -1
  20. data/lib/samagotchi/bridge/card_store.rb +6 -1
  21. data/lib/samagotchi/bridge/turn_accumulator.rb +2 -0
  22. data/lib/samagotchi/bundles/agents-md/manifest.yml +11 -0
  23. data/lib/samagotchi/bundles/agents-md/plugin.rb +198 -0
  24. data/lib/samagotchi/bundles/check-in/manifest.yml +2 -2
  25. data/lib/samagotchi/bundles/check-in/plugin.rb +27 -3
  26. data/lib/samagotchi/bundles/core/manifest.yml +2 -1
  27. data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
  28. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  29. data/lib/samagotchi/config.rb +33 -5
  30. data/lib/samagotchi/continue_offer.rb +5 -2
  31. data/lib/samagotchi/engine.rb +101 -23
  32. data/lib/samagotchi/hooks/registry.rb +3 -1
  33. data/lib/samagotchi/interrupted_turn.rb +39 -0
  34. data/lib/samagotchi/iteration_limit.rb +3 -2
  35. data/lib/samagotchi/kernel_loop.rb +40 -12
  36. data/lib/samagotchi/llm/backend.rb +6 -1
  37. data/lib/samagotchi/llm/chat_loop.rb +16 -8
  38. data/lib/samagotchi/llm/model_result.rb +11 -2
  39. data/lib/samagotchi/llm/native_backend.rb +3 -2
  40. data/lib/samagotchi/log_subscriber.rb +8 -4
  41. data/lib/samagotchi/plugin/context.rb +16 -3
  42. data/lib/samagotchi/prompt_files.rb +127 -0
  43. data/lib/samagotchi/ref.rb +141 -0
  44. data/lib/samagotchi/reply_wait.rb +6 -2
  45. data/lib/samagotchi/session.rb +53 -5
  46. data/lib/samagotchi/session_manager.rb +64 -6
  47. data/lib/samagotchi/session_metrics.rb +8 -2
  48. data/lib/samagotchi/session_setup.rb +11 -5
  49. data/lib/samagotchi/system_prompt.rb +71 -15
  50. data/lib/samagotchi/terminal_ui/attach_launcher.rb +18 -9
  51. data/lib/samagotchi/terminal_ui/formatting.rb +14 -0
  52. data/lib/samagotchi/terminal_ui.rb +21 -5
  53. data/lib/samagotchi/turn_checkpoint.rb +128 -0
  54. data/lib/samagotchi/turn_flow.rb +22 -3
  55. data/lib/samagotchi/version.rb +1 -1
  56. data/lib/samagotchi/web/app.rb +5 -0
  57. data/lib/samagotchi/web/public/answer_refs.js +217 -0
  58. data/lib/samagotchi/web/public/app.js +15 -18
  59. data/lib/samagotchi/web/public/favicon.svg +1 -0
  60. data/lib/samagotchi/web/public/index.html +6 -3
  61. data/lib/samagotchi/web/public/ref_menu.js +9 -1
  62. data/lib/samagotchi/web/public/ref_scan.js +5 -3
  63. data/lib/samagotchi/web/public/refs.js +129 -15
  64. data/lib/samagotchi/web/public/stream_handlers.js +5 -3
  65. data/lib/samagotchi/web/public/turn_view.js +17 -7
  66. data/lib/samagotchi/worker.rb +169 -21
  67. data/lib/samagotchi/worker_signals.rb +81 -0
  68. metadata +11 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 399c5b5b4ad3423bd266545c1f190e9e269f070a26bc48350640bf774eea7c4b
4
- data.tar.gz: 26bfce90ed8fe75c15c777943f07699803f70160f54559ae057ba31eb168d956
3
+ metadata.gz: a3f828171b95ef34cef1b89706f21a34822ea73dae63e55b2219767961241f7c
4
+ data.tar.gz: b12760887f0613c4e087f824f653e8ccc20e0602de55ef69fd8aed69a897a33c
5
5
  SHA512:
6
- metadata.gz: 042a3460c486987f7bd120fa6615c5cdd65d1ca6da0edfe9548746b83d9b66c6ad532b7088c20af082751b9bfad64252fba93123845fb36cfa0875d1a54ec0bf
7
- data.tar.gz: f441e37232ea17b6af0ca047aea5331d7e062b3563382656fb323118f8607459cf97540858fccae714691aabfe989dd666f8de379f2024bc7a4b024864b2a694
6
+ metadata.gz: e68486688dfe56739e1d05a68549d050f3cb73c369536c5fa097d832baabcfb42ba41917580ad9d0a4707ab3933550247177e92b0184dee2493b467be5773bad
7
+ data.tar.gz: 630eca4f31585d00938ad89b77275d25969384301b43c71415e20fac95be9fb22dfa60695f59afd1fe612e4cad8de94ead8ab949464c6ab7f21eb18fb8667357
data/CHANGELOG.md CHANGED
@@ -8,6 +8,92 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.61.0] - 2026-10-12
12
+
13
+ ### Added
14
+
15
+ - Hooks and plugins can attach refs to the answer: `event[:refs].call(match:, kind:, target:, label:, actions:)` on
16
+ `:after_turn`, stored as `display_refs` on the answer (display only: the model never sees them). The web pairs them
17
+ with the answer's links and text: their actions join the ref's menu, live and after a reload. Caps: 200 refs and
18
+ 256 KiB per answer; a refused ref is logged as `refs_rejected`. See
19
+ [docs/hooks.md](docs/hooks.md#refs-on-the-answer-display-only).
20
+
21
+ ### Changed
22
+
23
+ - A ref's values are capped (`Ref`, `RefAction`): a label at most 200 characters, at most 8 actions, a target id
24
+ 200 and a path 4,096 characters; an action's id 64 and label 100. An action URL with userinfo
25
+ (`https://user:pass@host/`) is refused, and the web page refuses one too.
26
+
27
+ ### Fixed
28
+
29
+ - A worker that gets TERM, HUP or INT (`chi sessions stop`, a closed terminal, a process-tree teardown) stops its
30
+ running turn as a Stop with reason `signal` and saves it, then exits; before, the turn and its prompt were lost.
31
+ A stopped session's worker now saves the turn it kept too.
32
+ - A worker killed mid-turn (SIGKILL, a crash) no longer loses the whole turn: the worker writes the turn so far to
33
+ the session file with an `interrupted_turn` mark (the prompt as the turn starts, then its iteration boundaries,
34
+ at most every 10 s), so it loses only what came after its last written boundary. The next worker (or a
35
+ `--no-shared` REPL resuming the session) ends that turn as canceled with a note the model reads
36
+ (`cancelled (worker-stopped)`), so the session is idle again.
37
+ - Workers outlive whoever spawned them: a worker leaves the spawner's process tree (forked again, `setsid`,
38
+ reparented to init), so a tool tearing down a background `chi send --wait`, or a closing terminal, no longer
39
+ kills it mid-turn (run #43 lost a 169-step turn that way).
40
+ - A card shown during a turn by a worker that has since exited comes back read-only: no buttons. A stale check-in
41
+ card's Nudge steered the next worker's turn on every click. Cards shown between turns (github-pr's offer, btw,
42
+ coordinator) keep their buttons.
43
+ - check-in nudges once per check-in: repeated Nudge clicks (or `/checkin nudge`) on the same check-in answer
44
+ "check-in: already nudged at N tool calls" instead of steering the turn again. Run #43 got 12 identical nudges.
45
+ - Web: a ref's `▾` sits in room the ref makes on hover, never over the next words.
46
+
47
+ Update with `chi update`: the system bundle 0.61.0 and check-in 0.2.4 (nudges once per check-in). Then restart
48
+ `chi web`.
49
+
50
+ ## [0.60.0] - 2026-10-11
51
+
52
+ ### Added
53
+
54
+ - chi web has a favicon: χ on a blue-to-violet rounded square. Safari showed a "1" monogram before.
55
+ - `chi --append-system-prompt FILE` (repeatable) and a config.yml `append_system_prompt:` list: a file's text goes
56
+ into the system prompt after AGENTS.md, in the cached part. A relative config entry is looked up where chi runs
57
+ (`append_system_prompt: [CLAUDE.md]`); the flag's files are saved in the session and kept by forks and continues.
58
+ `/stats` names the prompt's files on a `prompt files:` line.
59
+ - An analytics `resumed` mark on the turn record of a step-limit Continue (`resumed: true`; every other turn has
60
+ `resumed: false`). `continue` also marks a reminder's and a wake's turn, so `script/model_notes_report` now counts
61
+ Continues from `resumed`, falling back to `continue` record by record for older files. `:turn_started` and the
62
+ `before_turn` hook payload carry `resumed` too.
63
+ - A hook's or plugin's notice can be marked `fallback_for: :editor`: a line naming a file the viewer could open from
64
+ a ref. The web leaves it out for a viewer on this machine with `web.editor` set; a phone and the terminals show it.
65
+ - `ctx.prompt_files` for plugins: the files whose text is in the session's system prompt now — the AGENTS.md chain
66
+ and the appended files — as absolute paths in prompt order.
67
+ - The `agents-md` bundle, in the `core` profile (core 0.3.0: `chi update` installs it for existing core users):
68
+ a nested `AGENTS.md` of the session's checkout — one in a directory the session doesn't run in, which the
69
+ system prompt never sees — is steered into the conversation the first time a tool (`read`, `write` or
70
+ `edit`) touches a file under its directory, outermost first, once per session, and never re-injected after a
71
+ resume or a woken worker. It never stops generation or thinking, and leaves the chain's and appended
72
+ `AGENTS.md` files alone (`ctx.prompt_files`). Files in another repository, a nested repository or a
73
+ submodule are ignored. Opt out with `skip_agents_md: true`, as with the chain; `bundles: agents-md:`
74
+ settings: `tools`, `max_chars`. See "The agents-md bundle" in docs/plugins.md.
75
+
76
+ ### Changed
77
+
78
+ - The default step limit (`turn.max_iterations`) is 300, up from 100: most long build turns now finish in one turn
79
+ instead of stopping at the step-limit question. Loops are caught by the loop-guard bundle, and check-in's card
80
+ (at 50 tool calls, then every 50) still says a turn is long without stopping it. `--no-interrupt` stays 1000.
81
+ - Started in a subdirectory, chi loads every `AGENTS.md` from the top of the git work tree down to the current
82
+ directory, outermost first, instead of the subdirectory's replacing the root's. A further file is headed with its
83
+ directory ("Project specific description for pkg/:"); a session with one `AGENTS.md` gets the same prompt as before.
84
+
85
+ ### Fixed
86
+
87
+ - A step-limit Continue no longer drops the turn's reasoning: on a chat host (`reasoning_history: turn`) the
88
+ continued turn sends the earlier replies' reasoning back as before the limit, and on a native host
89
+ (`/completion`) the prompt keeps the run's thought blocks, those before a mid-turn steer too. The model keeps its
90
+ thinking, and the first request after the Continue matches the last one before the limit (before the fix, a local
91
+ 27B read most of a 17k prompt again: 24.9 s to the first token instead of 4.0 s). Continue's text is a steer, and on a chat host still starts
92
+ the reasoning afresh from it. After a `/model` change while the question stands, the Continue starts afresh.
93
+
94
+ Update with `chi update`: the system bundle 0.60.0, the core profile 0.3.0 (now with agents-md) and the new
95
+ agents-md 0.1.0 bundle.
96
+
11
97
  ## [0.59.0] - 2026-10-11
12
98
 
13
99
  ### Added
@@ -2576,7 +2662,9 @@ and long-lived sessions.
2576
2662
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
2577
2663
  hotkey panel that send selected text or the clipboard to your sessions.
2578
2664
 
2579
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.59.0...HEAD
2665
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.61.0...HEAD
2666
+ [0.61.0]: https://github.com/dm1try/samagotchi/compare/v0.60.0...v0.61.0
2667
+ [0.60.0]: https://github.com/dm1try/samagotchi/compare/v0.59.0...v0.60.0
2580
2668
  [0.59.0]: https://github.com/dm1try/samagotchi/compare/v0.58.0...v0.59.0
2581
2669
  [0.58.0]: https://github.com/dm1try/samagotchi/compare/v0.57.0...v0.58.0
2582
2670
  [0.57.0]: https://github.com/dm1try/samagotchi/compare/v0.56.0...v0.57.0
data/README.md CHANGED
@@ -76,8 +76,9 @@ It finds out whether the server is llama.cpp or OpenAI-compatible, picks the
76
76
  model (or asks, when there are several), sends one test request and writes
77
77
  the config. With a config already there, it adds a `hosts:` entry and keeps
78
78
  the rest of the file. Then it installs the `core` bundles (loop-guard,
79
- check-in, guardrails, slow-commands: the safety set for a small local model) and, on a
80
- terminal, offers `dev` (known-names, mcp, btw, skills, source-links). Edit the
79
+ check-in, guardrails, slow-commands, agents-md: the safety set for a small
80
+ local model) and, on a terminal, offers `dev` (known-names, mcp, btw, skills,
81
+ source-links). Edit the
81
82
  file later as in [Configure](#configure); `chi bootstrap --help` has the
82
83
  options.
83
84
 
data/bin/chi CHANGED
@@ -82,7 +82,7 @@ ARGV.each do |arg|
82
82
  base = arg.split("=", 2).first
83
83
  # If dashed version is a known flag (registry or hardcoded), reject underscore variant
84
84
  dashed = base.tr("_", "-")
85
- known = %w[--prompt --resume --attach --shared --no-shared --model --thinking --memory --mute --llm-context --llm-context-apply --llm-context-budget --no-interrupt --non-interactive --verbose --no-default-input --port --open --new-token] + Samagotchi::Config.cli_entries.map(&:cli_flag)
85
+ known = %w[--prompt --resume --attach --shared --no-shared --model --thinking --memory --mute --append-system-prompt --llm-context --llm-context-apply --llm-context-budget --no-interrupt --non-interactive --verbose --no-default-input --port --open --new-token] + Samagotchi::Config.cli_entries.map(&:cli_flag)
86
86
  if known.include?(dashed) || known.include?(base)
87
87
  warn "Unknown option: #{arg} (did you mean #{dashed}?)"
88
88
  exit 1
@@ -120,6 +120,8 @@ parser = OptionParser.new do |opts|
120
120
  "before the model's; default unsets it; /thinking changes it). With chi web: the default for the sessions " \
121
121
  "its workers run, below a session's own [env SAMAGOTCHI_THINKING_LEVEL]") { |l| options[:thinking] = l }
122
122
  opts.on("--memory NAME", "Preload a memory entry into the system prompt (repeatable; a comma list too)") { |m| (options[:memories] ||= []) << m }
123
+ opts.on("--append-system-prompt FILE", "Put FILE's text into the system prompt, after AGENTS.md (repeatable; saved in the session; " \
124
+ "config append_system_prompt: is the baseline)") { |f| (options[:append_system_prompt] ||= []) << f }
123
125
  opts.on("--mute NAME", "Hide a memory from this session: not in the prompt, refused by memory_read (repeatable; a comma list too)") { |m| (options[:muted] ||= []) << m }
124
126
  opts.on("--llm-context LAYERS", "This session's own LLM context strategy: none, or layers such as stale,forget (saved in the session, " \
125
127
  "before the model's; /llm-context changes it)") { |v| (options[:llm_context] ||= {})[:strategy] = v }
@@ -308,6 +310,23 @@ if options[:llm_context]
308
310
  end
309
311
  end
310
312
 
313
+ # --append-system-prompt: each file is checked here, before anything starts
314
+ # (the user named it, so a missing one is an error), and made absolute
315
+ # against this shell's cwd.
316
+ if options[:append_system_prompt]
317
+ require "samagotchi/prompt_files"
318
+ options[:appended_prompt_files] = options[:append_system_prompt].map do |value|
319
+ path, reason = Samagotchi::PromptFiles.check_cli_file(value)
320
+ if reason
321
+ warn "Error: --append-system-prompt #{value}: #{reason}"
322
+ exit 1
323
+ end
324
+ chars = Samagotchi::PromptFiles.large_size(path)
325
+ warn "Warning: --append-system-prompt #{value} is #{chars} chars; it is in every request" if chars
326
+ path
327
+ end.uniq
328
+ end
329
+
311
330
  chi_update_hint.call
312
331
  require "samagotchi/launch_mode"
313
332
  launch, launch_note = if scratch
@@ -341,7 +360,8 @@ if launch == :attached
341
360
  memories: options[:memories] || [],
342
361
  muted_memories: options[:muted] || [],
343
362
  llm_context: options[:llm_context] || {},
344
- thinking: options[:thinking])
363
+ thinking: options[:thinking],
364
+ appended_prompt_files: options[:appended_prompt_files] || [])
345
365
  rescue Samagotchi::TerminalUI::AttachLauncher::Error => e
346
366
  warn "Error: #{e.message}"
347
367
  exit 1
@@ -360,6 +380,7 @@ begin
360
380
  muted_memories: options[:muted] || [],
361
381
  llm_context: options[:llm_context] || {},
362
382
  thinking: options[:thinking],
383
+ appended_prompt_files: options[:appended_prompt_files] || [],
363
384
  non_interactive: options[:non_interactive],
364
385
  model_name: options[:model],
365
386
  scratch: scratch
data/docs/architecture.md CHANGED
@@ -97,7 +97,7 @@ terminal renders the events it gets over the Bridge.
97
97
  | Adapters | `Samagotchi::Client`, `LLM::OpenAIChat`, `LLM::HTTP` | Raw-prompt servers, the OpenAI chat API, and the HTTP both share. |
98
98
  | Tools | `lib/samagotchi/tools/*` | Execute, read, edit, write, memory, task_*, web_fetch, delegate/delegate_result, ask_user_question, reminders, send_note, list_sessions, context_read, forget_outputs, plus runtime/output-guardrails. |
99
99
  | Bundles | `lib/samagotchi/memory_bundle/*`, `BundleCommand` | `chi bundle`: install/upgrade (3-way merge), build, uninstall; the install record under `.bundles/<name>/` that the hook, guardrail and plugin loaders read ([internals/bundles.md](internals/bundles.md)). |
100
- | Background | `Samagotchi::Worker`, `Samagotchi::SessionManager` | `SessionManager` forks/spawns one worker process per session; its `Worker` builds the `Engine` and `Bridge` (no terminal rendering) and runs queued turns. |
100
+ | Background | `Samagotchi::Worker`, `Samagotchi::SessionManager` | `SessionManager` forks/spawns one worker process per session; its `Worker` builds the `Engine` and `Bridge` (no terminal rendering) and runs queued turns. The spawned process (own process group) forks the worker, which `setsid`s and is reparented to init once its starter exits (it waits until the worker holds `owner.lock`, so the spawner's `Process.detach` waiter tells a starting worker): nothing that walks the spawner's process tree reaches it (`SAMAGOTCHI_WORKER_DETACH=1`, `detach_from_spawner`). The lock's pid is the worker's; `spawn`'s `child_pid` is its starter's. |
101
101
  | Web | `Samagotchi::Web::App`, `Samagotchi::Web::Server`, `Samagotchi::Web::SessionHub` | Rack+WEBrick single-port `127.0.0.1:4567` (index.html + `/api/*` + SSE). The hub is chi web's projection of the session list, pushed to every tab over `GET /api/events`. |
102
102
  | Sessions | `Samagotchi::Session`, `SessionManager` | File `sessions/<uuid>.json` + sidecar `input/`/`output/`; retention 14d/500, `updated_at desc`, lazy sweep. |
103
103
 
@@ -177,7 +177,8 @@ forwards the raw `KernelLoop` events unchanged (the low-level contract) and adds
177
177
  higher-level events so UIs get clean turn boundaries without inferring them:
178
178
 
179
179
  - `:turn_started` — `{ session_id:, prompt:, turn_id:, cut_after: }` (`cut_after`: the seconds a cut waits for this
180
- model's thinking, `0` = never; plus `continue: true` for a continue turn, `images:` with the prompt's image refs)
180
+ model's thinking, `0` = never; plus `continue: true` for a continue turn, `resumed: true` for a step-limit Continue,
181
+ `images:` with the prompt's image refs)
181
182
  - `:turn_completed` — `{ result:, turn_summary:, display_pending: }` (`result`: the final
182
183
  `LLM::ModelResult`; `display_pending`: `after_turn` hooks may still present the answer)
183
184
  - `:turn_canceled` — `{ cancellation_reason:, cancelled_by:, duration_ms: }` (`cancelled_by`: the bundle or hook label that
@@ -296,7 +297,8 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
296
297
  (`GET /api/sessions/:id`, `?parts=1`) and the plugin's `list_sessions`; every message is copied.
297
298
  - `GET /session/:id/tail` — the page's light re-read (`Bridge#tail_frame`): `session_state_snapshot`,
298
299
  `answer` (the last message a UI shows as an answer, `AnswerTail`: the one message, copied alone),
299
- `cards` (`CardStore#list`), `event_seq`, `event_id`, all taken in one event-log hold; no message
300
+ `cards` (`CardStore#list`; a card an earlier worker showed during a turn comes back read-only, with no
301
+ actions: its turn no longer runs), `event_seq`, `event_id`, all taken in one event-log hold; no message
300
302
  list, so it costs the same however long the session is. `?turn_id=` answers that turn's answer
301
303
  (an unknown or missing id: the newest); the only route that reads its query.
302
304
  - `OPTIONS *` — CORS preflight (`Access-Control-Allow-Origin: *`).
@@ -317,11 +319,13 @@ it, so the web's info bar updates per generation without parsing chunk payloads.
317
319
 
318
320
  The page's frequent reads of `GET /api/sessions/:id` stay light too:
319
321
  - `?tail=1&recent=1` (each turn's end, a cancel, an answer display): `{tail, session: {id, status,
320
- used_memory_names}, messages: [the answer], markdown_warning, timing}`, from the worker's `/tail`
322
+ used_memory_names}, messages: [the answer, with its display and display_refs (re-read on an `answer_display` with a display or refs)], markdown_warning, timing}`, from the worker's `/tail`
321
323
  alone (no session file parsed, no `analytics.json`); `timing` carries the worker's recent records
322
324
  and `turn_count` (finished turns). The page merges the records by id (`timing.js mergeTiming`) and
323
325
  finishes the ended turn's own line by its `turn_id` (sent as `&turn_id=`, which also picks that
324
326
  turn's answer). A plain `?tail=1` (a tab opened before this) gets the whole timing.
327
+ The page binds an answer's `display_refs` to its bubble (`answer_refs.js`: on a reload's render and on this
328
+ read's re-render) and pairs them with the bubble's links and text before the `path:line` scan.
325
329
  - `?timing=1`: the whole timing (`analytics.json` merged with the worker's `/state`) and
326
330
  `turn_count`, when the page's merge came up short.
327
331
  - `?cards=1`: the cards, from `/tail`; the session file is only stat'ed for the 404.
@@ -394,7 +398,8 @@ stream with a fresh `ThoughtStreamSplitter` (Qwen's `<think>` and Gemma's
394
398
  `<|channel>thought … <channel|>` are thinking, tool-call bodies are dropped
395
399
  from `text:`). The chat loop's model turns keep their `tool_calls` and tool results their `tool_call_id`,
396
400
  so later requests and resumed sessions pair them, and their reasoning (`thinking:`), which goes back as
397
- `reasoning_content` on the running turn's replies (`reasoning_history`). It has its own system prompt
401
+ `reasoning_content` on the running turn's replies (`reasoning_history`; a step-limit Continue's
402
+ turn goes on from the run it resumes, `reasoning_since:`). It has its own system prompt
398
403
  (no raw-prompt tool declarations; the tools go as JSON schemas).
399
404
 
400
405
  `Engine#build_stream_event_handler` is the one seam both loops' streams pass
@@ -449,7 +454,7 @@ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/
449
454
 
450
455
  | Component | Main file(s) | What it is |
451
456
  |---|---|---|
452
- | Worker | `worker.rb` (`worker_idle_exit.rb`, `worker_inbound.rb`) | A session's background process: builds the `Engine` and `Bridge`, runs queued turns, exits when idle. |
457
+ | Worker | `worker.rb` (`worker_idle_exit.rb`, `worker_inbound.rb`, `worker_signals.rb`) | A session's background process: builds the `Engine` and `Bridge`, runs queued turns, exits when idle. A spawned one traps TERM, HUP and INT (`WorkerSignals`, installed by `run_session_loop` when `SAMAGOTCHI_WORKER_PROCESS=1`): the running turn stops as a Stop with reason `signal`, is saved, and the worker leaves (`:signal`, exit 0; a new worker only for input already queued). A running turn is checkpointed at its iteration boundaries (`TurnCheckpoint`, the `interrupted_turn` mark), and the next worker settles a turn its predecessor died in (`TurnCheckpoint.settle`). |
453
458
  | Config | `config.rb` | Every setting as an `Entry`: env, config.yml and CLI layers ([configuration.md](configuration.md)). |
454
459
  | MemoryBundle | `memory_bundle.rb`, `memory_bundle/{installer,builder,installed_bundle,plugin_ref,bundle_hook}.rb` (`Installer`, `Builder`, `InstalledBundle`, `PluginRef`, `BundleHook`) | Installing, building and reading bundles of memories, hooks and plugins. |
455
460
  | Plugin API | `plugin/api.rb` (`Plugin::Api`), `plugin/loader.rb` | What a bundle's plugin registers: tools, commands, hooks, services ([plugins.md](plugins.md)). |
data/docs/cli.md CHANGED
@@ -189,6 +189,7 @@ controls exit behavior (`--non-interactive`); `--resume` composes with both.
189
189
  | `--thinking LEVEL` | This session's own thinking level: `off`, `low`, `medium` or `high`, saved in the session and first in the order (`default` unsets it). On `--resume` or `--attach` it is set at once, through a `/thinking` on an attached worker. With `chi web` it is instead the default for the sessions its workers run, below a session's own (as env `SAMAGOTCHI_THINKING_LEVEL` is). See "Thinking" in configuration.md. |
190
190
  | `--profile NAME` | Prompt profile (`qwen36` or `gemma4`) for every model in this run, over config and the server's template (same as `--model-profile`, env `SAMAGOTCHI_MODEL_PROFILE`). See "Prompt profile" in configuration.md. |
191
191
  | `--memory NAME` | Preload a memory entry into the system prompt (repeatable; a comma list too). Merged under the config.yml `memories:` baseline. Works attached: the list is stored on the session, so its worker builds the same prompt on every respawn. |
192
+ | `--append-system-prompt FILE` | Put `FILE`'s text into the system prompt, after AGENTS.md and before the memory indexes (repeatable; relative to the shell's directory). Added after the config.yml `append_system_prompt:` list. A missing or unreadable file is an error, exit 1. Saved in the session, so a respawned worker, a fork and a continue keep it; ignored with `--attach`/`--resume` (one line, as `--memory`). See [Appended prompt files](configuration.md#appended-prompt-files). |
192
193
  | `--mute NAME` | Hide a memory from this session (repeatable; a comma list too): its index line is not in the prompt, `memory_read` refuses it, the identity auto-load skips it, and it is dropped from the preloads (config baseline or `--memory`). A name matches in both scopes (`gh-helper`, `project/gh-helper` and `gh-helper.md` all hide `gh-helper`). Nothing on disk changes. See [Muting a memory](#muting-a-memory). |
193
194
  | `--llm-context LAYERS` | This session's own LLM context strategy: `none`, or layers such as `stale` or `stale,forget` (comma, space or `\|` separated). Saved in the session, before the model's `llm_context_strategy`. With `--resume` it is set on the resumed session; on an attached or resumed worker it runs as `/llm-context` before the first prompt (after `--model`'s `/model`). A value that isn't one is an error before anything starts. See "LLM context: a session's own strategy" in configuration.md. |
194
195
  | `--llm-context-apply RULE` | This session's own apply rule: `payoff`, `next_request` or `turn_end`. |
@@ -504,7 +505,8 @@ back from the next turn on, and where that came from (`session`,
504
505
  comes first; `/reasoning-history default` unsets it. It is saved and carried as
505
506
  `/thinking`'s is, and a native host ignores it (the reply says so). A change to
506
507
  or from `all` once an earlier reply has thinking rewrites the earlier replies;
507
- `drop` ↔ `turn` costs nothing. `/stats` shows it on its `reasoning history:`
508
+ `drop` ↔ `turn` costs nothing. Under `turn` a step-limit Continue keeps the
509
+ reasoning of the turn it continues. `/stats` shows it on its `reasoning history:`
508
510
  line, the web as the `reasoning` chip. See "Reasoning history" in
509
511
  [configuration.md](configuration.md).
510
512
 
@@ -997,7 +999,7 @@ Behavior notes:
997
999
  ## Iteration Limit Behavior
998
1000
 
999
1001
  - A turn's step limit (model ↔ tool rounds) is `turn.max_iterations` in config.yml (env
1000
- `SAMAGOTCHI_TURN_MAX_ITERATIONS`, default `100`, an integer of 1 or more). `--no-interrupt` and
1002
+ `SAMAGOTCHI_TURN_MAX_ITERATIONS`, default `300`, an integer of 1 or more). `--no-interrupt` and
1001
1003
  `--non-interactive` turns get the larger of 1000 and it. Reminder turns follow it too.
1002
1004
  - Tool side effects that already ran before the limit are not rolled back.
1003
1005
  - `Samagotchi::KernelLoop#run` returns a resumable result object with the visible output plus the accumulated
@@ -1016,9 +1018,13 @@ Behavior notes:
1016
1018
  - In the plain REPL (`--no-shared`) the CLI asks at the `? ` prompt, with the choices listed under it: `yes` (Enter
1017
1019
  alone, or `/continue`) resumes, `no` stops, and `no, <explanation>` stops while keeping the reason in conversation
1018
1020
  context. Once answered, one line stays: `? The turn ran out of iterations. Continue it? → no, too slow`.
1021
+ - Continue goes on with the same turn: the model keeps the reasoning (thinking) of the run it continues, on chat and
1022
+ native hosts, so a local server's prompt cache stays warm. Continue's text is a steer: on a chat host the reasoning
1023
+ starts again from it. See "Reasoning history" in [configuration.md](configuration.md).
1019
1024
  - Stop keeps the interrupted turn's work in the conversation, as a Ctrl-C or a new prompt does, with a note for the
1020
1025
  model that it wasn't continued (or your reason); `!rollback` still erases it.
1021
1026
  - `--non-interactive` (`chi -p`) has nobody to ask: the turn ends there. A `chi send --wait` on a session nobody can
1022
1027
  answer (an older worker) reports `limit` (exit 1) with how to continue it: `chi send ID -m '/continue yes'`.
1023
1028
  - The check-in bundle's card closes as the turn ends, before the step-limit question opens, so the two never stand
1024
- together; with check-in's default `after: 50` and the limit of 100 a long turn usually shows a check-in card first.
1029
+ together; with check-in's default `after: 50` and the limit of 300 a long turn shows check-in cards (at 50, 100, …)
1030
+ well before it.
@@ -81,6 +81,13 @@ session:
81
81
  memories:
82
82
  # - system/user_preferences
83
83
  # - project/feature-env-template
84
+
85
+ # Files whose text goes into the system prompt after AGENTS.md (same as
86
+ # --append-system-prompt, which adds to these). A relative entry is looked up
87
+ # in the directory chi runs in; a missing one is skipped.
88
+ append_system_prompt:
89
+ # - CLAUDE.md
90
+ # - ~/notes/house-rules.md
84
91
  ```
85
92
 
86
93
  Behavior:
@@ -108,8 +115,8 @@ Besides the settings sections, the file holds maps that are read by their
108
115
  own subsystems: `hosts:` (below), `models:` (see "Prompt profile" and
109
116
  "Images"), `model_aliases:`, `hooks:` (see [Hooks](hooks.md)),
110
117
  `guardrails:` (see [Guardrails](guardrails.md)), `bundles:` (a bundle's
111
- settings for its hooks, see [Hooks: Settings](hooks.md#settings)) and
112
- `memories:`. Their entry names (host names, model ids, aliases) are yours to
118
+ settings for its hooks, see [Hooks: Settings](hooks.md#settings)),
119
+ `memories:` and `append_system_prompt:` (see "Appended prompt files"). Their entry names (host names, model ids, aliases) are yours to
113
120
  choose. The `memories:` list is the persistent baseline for preloaded memory entries —
114
121
  the same name shape as `--memory` (bare name or `scope/name`), merged under
115
122
  any per-run `--memory` values (config baseline first, deduped). A per-run
@@ -561,8 +568,13 @@ models:
561
568
  reasoning as `reasoning_content`, which is what Qwen's, Gemma 4's and DeepSeek's chat templates and the hosted APIs
562
569
  (OpenRouter takes it for DeepSeek, Qwen and other text-reasoning models) expect. The running turn starts at the
563
570
  last message you sent (a prompt, or a steer: as the chat template counts it) and never before the turn's own start,
564
- so a turn that goes on by itself (a reminder, a context wake, a continue after a stop) sends nothing of the one
565
- before it, an interrupted reply's partial thought included.
571
+ so a turn that goes on by itself (a reminder, a context wake, a delegate report's wake) sends nothing of the one
572
+ before it, an interrupted reply's partial thought included. A **step-limit Continue** is the same turn going on: it
573
+ keeps the reasoning of the run it continues (back to the first run of a chain of Continues), so its first request
574
+ matches the last one before the limit and the server's prompt cache stays warm. Continue's text, or a `!cmd` run
575
+ while the step-limit question stands, is a user message: the reasoning before it goes, as for any steer, and that
576
+ Continue reads the conversation again once (a re-prefill on a local server). After a `/model` change while the
577
+ question stands the Continue starts afresh (no other model's reasoning is sent).
566
578
  - `drop` sends none (what chi did before this setting).
567
579
  - `all` sends every reply's reasoning, earlier turns too. It sends only the field: a local template that leaves out
568
580
  earlier turns' reasoning (Qwen's) needs `sampling: {chat_template_kwargs: {preserve_thinking: true}}` as well.
@@ -586,7 +598,8 @@ models:
586
598
  | nothing thought yet, the same level, or a native host | none |
587
599
 
588
600
  - The native loop (`/completion`) ignores it: it always keeps the running turn's reasoning, a mid-turn steer included
589
- (the chat loop starts again after a steer).
601
+ (the chat loop starts again after a steer), and a step-limit Continue keeps the thoughts of the run it continues,
602
+ a mid-turn steer's earlier ones included.
590
603
  - When a host answers a request that sent reasoning back with an HTTP 400 about reasoning, chi asks again without it,
591
604
  sends none to that model from then on (until the worker restarts) and says so once. If the request without it is
592
605
  refused too, the thinking fields get the same fallback ("Thinking") and the reasoning goes back on the next step.
@@ -1074,13 +1087,50 @@ The helper reads these from its launch file: after editing them run
1074
1087
  If an AGENTS.md file is present in the project root, samagotchi injects its
1075
1088
  contents into the system prompt under a "Project specific description:" section.
1076
1089
  The project root is the top of the git work tree chi runs in (a linked
1077
- worktree's own checkout), or the current directory outside a repository. An
1078
- AGENTS.md in the current directory is read instead when there is one; only one
1079
- file is read.
1090
+ worktree's own checkout), or the current directory outside a repository.
1091
+
1092
+ Started in a subdirectory, chi loads the chain: every AGENTS.md from the work
1093
+ tree top down to the current directory, outermost first. The first keeps the
1094
+ plain header; each further one is headed with its directory, e.g. "Project
1095
+ specific description for pkg/:". Outside a repository only the current
1096
+ directory's AGENTS.md is read. Edits are picked up by a new process or a
1097
+ prompt rebuild (`/model`).
1080
1098
 
1081
- To skip loading AGENTS.md, set `skip_agents_md: true` at the top level of
1099
+ To skip loading AGENTS.md (all of the chain), set `skip_agents_md: true` at the top level of
1082
1100
  `config.yml` (env `SAMAGOTCHI_SKIP_AGENTS_MD=true`).
1083
1101
 
1102
+ Nested `AGENTS.md` files (in directories the session doesn't run in) are
1103
+ steered into the conversation on first touch by the `agents-md` bundle
1104
+ ([The agents-md bundle](plugins.md#the-agents-md-bundle)); `skip_agents_md`
1105
+ disables that steering too.
1106
+
1107
+ ## Appended prompt files
1108
+
1109
+ `append_system_prompt:` (a top-level list) and `chi --append-system-prompt FILE`
1110
+ (repeatable) put a file's text into the system prompt right after the AGENTS.md
1111
+ chain and before the memory indexes, in the stable part a prompt cache keeps.
1112
+ Each file is one section, `Appended from <path>:`, the path relative to the
1113
+ work tree top when inside it, else with `~`.
1114
+
1115
+ - The config list comes first, then the flag's files; a repeat, or a file the
1116
+ AGENTS.md chain already holds, is left out, and so is an empty file.
1117
+ - A config entry: `~` is expanded, and a relative one is looked up in the
1118
+ directory chi runs in, so `append_system_prompt: [CLAUDE.md]` loads a
1119
+ project's `CLAUDE.md` wherever there is one. A missing entry is skipped
1120
+ without a warning. The list is read when the session's engine starts (a new
1121
+ process picks up an edit).
1122
+ - `--append-system-prompt FILE`: relative to the shell's directory. A missing,
1123
+ unreadable or non-file `FILE` stops chi (`Error: --append-system-prompt FILE:
1124
+ no such file`); above 32,000 chars it warns once (`… is N chars; it is in
1125
+ every request`). The files are saved in the session (`appended_prompt_files`),
1126
+ so a respawned worker builds the same prompt, and a fork or a continue keeps
1127
+ them; a delegate child doesn't. With `--attach` or `--resume` the flag is
1128
+ ignored with one line, as `--memory` is; a REPL `--resume` (`--no-shared`)
1129
+ adds them for that run only.
1130
+ - The files' text is read at each prompt build, as AGENTS.md's is: an edit is
1131
+ picked up by a new process or a rebuild (`/model`). `/stats` lists the files
1132
+ in the prompt on its `prompt files:` line.
1133
+
1084
1134
  ## LLM context: the forget layer
1085
1135
 
1086
1136
  Experimental, off by default. With `forget` in a model's `llm_context.strategy` (next to `stale`), chi offers that
@@ -1219,8 +1269,8 @@ Every setting below takes the three forms described in
1219
1269
  `config.yml`, `SAMAGOTCHI_<DOTTED_NAME>` in the environment (not for one
1220
1270
  marked config.yml only) and, where the CLI
1221
1271
  column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
1222
- `model_aliases:`, `hooks:`, `guardrails:` rules, `bundles:`, `memories:`) are
1223
- described in their own sections.
1272
+ `model_aliases:`, `hooks:`, `guardrails:` rules, `bundles:`, `memories:`,
1273
+ `append_system_prompt:`) are described in their own sections.
1224
1274
 
1225
1275
  | Setting | Default | CLI | What it does |
1226
1276
  |---|---|---|---|
@@ -1306,7 +1356,7 @@ described in their own sections.
1306
1356
  | `retry.empty_answer` | `1` | | Times a turn asks again after an empty answer (at most 3, `0` = off). See "Llama Network Retry Behavior". |
1307
1357
  | `steer.cut_after` | `20` | | Seconds a generation must have streamed only thinking before a message sent as a cut (`/cut TEXT`, `chi send --cut`) cuts it; other messages go in at the next step; `0` = never. `models.<key>.steer_cut_after` and `hosts.<name>.steer_cut_after` come first; see [Messages during a running turn](#messages-during-a-running-turn-steer-and-cut). |
1308
1358
  | `models.<key>.steer_cut_after`, `hosts.<name>.steer_cut_after` | none | | A model's or host's seconds a cut waits for its thinking: whole seconds, `0` = never (e.g. a slow local thinker). `/model` shows the resolved one. See [Messages during a running turn](#messages-during-a-running-turn-steer-and-cut). |
1309
- | `turn.max_iterations` | `100` | | A turn's step limit: model ↔ tool rounds before it stops and asks to continue (an integer ≥ 1). `--no-interrupt` turns get the larger of 1000 and this. See [CLI: Iteration Limit Behavior](cli.md#iteration-limit-behavior). |
1359
+ | `turn.max_iterations` | `300` | | A turn's step limit: model ↔ tool rounds before it stops and asks to continue (an integer ≥ 1). `--no-interrupt` turns get the larger of 1000 and this. See [CLI: Iteration Limit Behavior](cli.md#iteration-limit-behavior). |
1310
1360
  | `turn.parent_continue` | `true` | | Whether a parent agent (`chi answer`) may answer Continue to a session's step-limit question; `false`: Stop only. config.yml only: no environment variable. See [Sub-agent](sub-agent.md) and [Guardrails](guardrails.md#approvals-from-a-parent-agent). |
1311
1361
  | `update.gem` | `true` | | `false`: `chi update` never installs a newer gem (`--no-gem` for one run). See [CLI: Updating](cli.md#updating). |
1312
1362
  | `update.bundles` | `true` | | `false`: `chi update` leaves the shipped bundles to `chi bundle upgrade` (`--no-bundles`). |
data/docs/hooks.md CHANGED
@@ -50,8 +50,8 @@ The plugin class must respond to `#call(event)` — duck-typed, no base class re
50
50
  | Event | When it fires | Event payload |
51
51
  |-------|--------------|---------------|
52
52
  | `:session_start` | The first turn this chi process runs for the session (so again after `--resume`, a worker that idle-exited and woke, or a restart) | `{ type: :session_start, session_id: "..." }` |
53
- | `:before_turn` | Before each turn starts | `{ type: :before_turn, session_id: "...", prompt: "..." (nil on a continue), messages: [...] (the history before this turn) }` |
54
- | `:after_turn` | After a turn completed or was stopped once the model was asked (not after one that failed, was cut short by an interrupt signal (SIGINT), or was stopped before its `:before_turn` hooks) | `{ type: :after_turn, status: "completed" \| "canceled", present: (see [Presenting the answer](#presenting-the-answer-display-only)), messages: [...] (the conversation the turn stored; a cancelled or empty turn ends it with a `kind: turn_note` system message, and a context line is `kind: context`, see [sessions.md](sessions.md#notes-a-turn-leaves-for-the-model)) }` |
53
+ | `:before_turn` | Before each turn starts | `{ type: :before_turn, session_id: "...", prompt: "..." (nil on a continue), resumed: true \| false (true on a step-limit Continue, which resumes the run before it; false on every other turn, a reminder's or a wake's too), messages: [...] (the history before this turn) }` |
54
+ | `:after_turn` | After a turn completed or was stopped once the model was asked (not after one that failed, was cut short by an interrupt signal (SIGINT), or was stopped before its `:before_turn` hooks) | `{ type: :after_turn, status: "completed" \| "canceled", present: (see [Presenting the answer](#presenting-the-answer-display-only)), refs: (see [Refs on the answer](#refs-on-the-answer-display-only)), messages: [...] (the conversation the turn stored; a cancelled or empty turn ends it with a `kind: turn_note` system message, and a context line is `kind: context`, see [sessions.md](sessions.md#notes-a-turn-leaves-for-the-model)) }` |
55
55
  | `:before_generation` | Before each LLM API call (both loops) | `{ type: :before_generation, iteration: N }` |
56
56
  | `:after_generation` | After LLM returns (both loops); not after a generation a hook cut (`stop_generation`) | `{ type: :after_generation, iteration: N, response: "...", messages: [...] (the conversation as sent) }` |
57
57
  | `:generation_progress` | While the response streams, in batches (see [Watching the stream](#watching-the-stream)) | `{ type: :generation_progress, iteration: N, thinking: "..." (new since the last fire), text: "..." (new visible text), thinking_chars: N, text_chars: N (this generation so far), elapsed_ms: N, restarted: true (only on the first fire after a dropped stream was asked again) }` |
@@ -117,7 +117,7 @@ known-names)` for a bundle hook, `audit.rb (config)` for a config hook,
117
117
  `turn hook` for one registered at runtime.
118
118
 
119
119
  `event[:notify]` also takes `fallback_for:`, what the line stands in for, so
120
- that a UI that already shows it can leave the line out. The one value is
120
+ that a UI that already shows it can leave the line out. The main value is
121
121
  `:display`: the line repeats what the hook's
122
122
  [`event[:present]`](#presenting-the-answer-display-only) display shows (its
123
123
  links), for a UI that doesn't render that display. The hook never names a UI;
@@ -125,7 +125,13 @@ each decides by what it renders. The web with markdown on (`web.markdown` and
125
125
  the commonmarker gem) leaves such a line out, live and after a reload; with
126
126
  markdown off it shows, as it does in the REPL, the attached TUI and
127
127
  `chi -p`, which print the answer as text. The log keeps the line with
128
- `fallback_for=display`. Another value, or none, shows the line everywhere.
128
+ `fallback_for=display`. `:editor` marks a line that names a file the viewer
129
+ could open from a ref in its editor: the web leaves it out for a viewer on
130
+ this machine with `web.editor` set, and every other UI and viewer (a phone,
131
+ the terminals) shows it. Name only a file the page shows as a ref (one a
132
+ tool row touched): a loopback viewer with `web.editor` set never sees the
133
+ line, so a file it can't open from a ref would be lost to it. Another value,
134
+ or none, shows the line everywhere.
129
135
  Mark a line only when the display really shows all of it (source-links marks
130
136
  its note only when the answer links every URL the note names). It needs chi
131
137
  0.35.0: an older chi's `event[:notify]` raises `ArgumentError` on the keyword,
@@ -269,6 +275,77 @@ end
269
275
 
270
276
  A plugin gets the same from `chi.on(:after_turn) { |event, ctx| event[:present].call { … } }`.
271
277
 
278
+ ## Refs on the answer (display only)
279
+
280
+ `:after_turn` also carries `refs:`, a callable that attaches a **ref** to the
281
+ turn's answer: a thing the answer mentions (an issue, a file, a commit) as
282
+ data, with links to open it. Like `present`, it is display only.
283
+
284
+ ```ruby
285
+ class JiraRefs
286
+ def call(event)
287
+ return unless event[:type] == :after_turn
288
+
289
+ answer = event[:messages].last
290
+ return unless answer && answer[:role] == "model"
291
+
292
+ answer[:content].to_s.scan(/\b[A-Z][A-Z0-9]+-\d+\b/).uniq.each do |key|
293
+ event[:refs].call(match: key, kind: "issue", target: { id: key }, label: key,
294
+ actions: [{ id: "open", label: "Open in Jira", url: "https://jira.example.com/browse/#{key}" }])
295
+ end
296
+ end
297
+ end
298
+ ```
299
+
300
+ - The call: `event[:refs].call(match:, kind:, target:, label: nil, actions: [])`,
301
+ keywords only (String or Symbol keys inside `target` and `actions`).
302
+ - `match`: the text in the answer the ref stands for (at most 500 characters).
303
+ - `kind`: a short lowercase word. `"file"` takes `target: {path:, line:, end_line:}`
304
+ (the lines optional); a relative path is resolved against the session's
305
+ folder, an absolute one is kept as given. Any other kind (`"issue"`,
306
+ `"commit"`, …) takes `target: {id:}`.
307
+ - `label`: what a UI shows for it (optional, at most 200 characters).
308
+ - `actions`: links a UI offers on top of the kind's own, each
309
+ `{id:, label:, url:}`; http(s) URLs only, without userinfo (`user:pass@`),
310
+ at most 8, opened in a new tab.
311
+ - It returns `true` when the ref is kept (or was there already: a duplicate
312
+ is kept once), `false` when it is refused, and `nil` when the turn has no
313
+ answer (as `present`: after a cancelled, failed or empty turn) or the call
314
+ comes after the hooks ran (a plugin that kept `event[:refs]`: the refs are
315
+ stored by then).
316
+ - Caps: 200 refs and 256 KiB (the list's JSON) per answer; an action's id
317
+ at most 64 characters and its label 100; a target id 200, a path 4,096.
318
+ - A refused ref is logged as `refs_rejected` with the hook's label and the
319
+ reason, each reason once per hook per answer (the same again is logged at
320
+ debug with its count). The call never raises into the hook.
321
+ - Refs are independent of `present`: a rejected `present` keeps them, and a
322
+ ref whose `match` isn't in the shown text is still stored (a UI drops it).
323
+ - They are kept as `display_refs` on the answer's model message in the
324
+ session file, next to `display`, and the `answer_display` event says how
325
+ many (`refs: N`). The model never sees them (as `display`), and a fork
326
+ (`chi.sessions.fork(messages: ctx.messages)`) starts without them.
327
+ - **The web** pairs each ref with the shown answer (its rendered markdown,
328
+ or the `present`ed display), live (with the turn-end read when the refs
329
+ are stored by then, else on the `answer_display` event) and on a reload. It looks, over all the answer's refs, for:
330
+ 1. a link whose href is one of the ref's action URLs;
331
+ 2. else a link whose text is `match`;
332
+ 3. else `match` as a whole word inside one run of plain text (not in a
333
+ code block or another link), or an inline code that is `match` alone.
334
+ The word's edges are checked within that run only: text in the next
335
+ element doesn't count (`JIRA-1<em>2</em>` pairs `JIRA-1`).
336
+
337
+ The ref binds every occurrence its first finding step finds (a ref paired
338
+ with its link leaves a plain mention of the same text alone); a ref
339
+ nothing pairs is dropped. Markdown changes text: `__init__.py` renders
340
+ bold, escapes and entities differ, a match across emphasis or code isn't
341
+ one run, so keep a markdown link to the ref's URL, or match the rendered
342
+ text. The ref's actions join its menu (a link's own target is labelled by
343
+ the action with its URL); a `file` ref keeps the editor, show-in-turn and
344
+ copy targets; `label` is its hover title when it differs from the text.
345
+ **Terminals** ignore them.
346
+
347
+ A plugin gets the same from `chi.on(:after_turn) { |event, ctx| event[:refs].call(…) }`.
348
+
272
349
  ## Settings
273
350
 
274
351
  A hook class whose `initialize` takes an argument gets its settings: **one
@@ -507,7 +584,7 @@ Notes:
507
584
  - Ordering: bundle hooks fire by `(priority, bundle_name, hook_name)` (lower priority first), then plain `config.yml` hooks in registration order.
508
585
  - Settings: a hook class with `initialize(settings = {})` gets the bundle's section of `config.yml` `bundles:` (see [Settings](#settings)).
509
586
  - A bundle can also ship a `plugin.rb` whose `chi.on(event)` blocks are bundle hooks too, next to commands and tools; see [Plugins](plugins.md). There `ctx.notify`, `ctx.steer` and the other `ctx` helpers act as this page's `event[:notify]`… do for the event the block runs for.
510
- - Shipped bundles (each in a profile: `core` or `dev`, see [Bundle profiles](memory.md#bundle-profiles-core-and-dev)): `chi bundle install guardrails` (rules, see [Guardrails](guardrails.md#the-guardrails-bundle)) and `chi bundle install known-names` (a hook, see [Guardrails](guardrails.md#the-known-names-bundle)), `chi bundle install source-links` (a hook: announces source refs, see [The source-links bundle](#the-source-links-bundle)), `chi bundle install btw` (a plugin: `/btw`, see [Plugins](plugins.md#the-btw-bundle)), `chi bundle install mcp` (a plugin: tools from MCP servers, see [Plugins](plugins.md#the-mcp-bundle)) `chi bundle install loop-guard` (a plugin: breaks tool-call loops, see [Plugins](plugins.md#the-loop-guard-bundle)), `chi bundle install check-in` (a plugin: checks on a long turn, see [Plugins](plugins.md#the-check-in-bundle)), `chi bundle install slow-commands` (a plugin: commands that ran past the execute limit are run as tasks next time, see [Plugins](plugins.md#the-slow-commands-bundle)) and `chi bundle install skills` (a plugin: `/skill`, versions of skills, see [Plugins](plugins.md#the-skills-bundle)).
587
+ - Shipped bundles (each in a profile: `core` or `dev`, see [Bundle profiles](memory.md#bundle-profiles-core-and-dev)): `chi bundle install guardrails` (rules, see [Guardrails](guardrails.md#the-guardrails-bundle)) and `chi bundle install known-names` (a hook, see [Guardrails](guardrails.md#the-known-names-bundle)), `chi bundle install source-links` (a hook: announces source refs, see [The source-links bundle](#the-source-links-bundle)), `chi bundle install btw` (a plugin: `/btw`, see [Plugins](plugins.md#the-btw-bundle)), `chi bundle install mcp` (a plugin: tools from MCP servers, see [Plugins](plugins.md#the-mcp-bundle)) `chi bundle install loop-guard` (a plugin: breaks tool-call loops, see [Plugins](plugins.md#the-loop-guard-bundle)), `chi bundle install check-in` (a plugin: checks on a long turn, see [Plugins](plugins.md#the-check-in-bundle)), `chi bundle install slow-commands` (a plugin: commands that ran past the execute limit are run as tasks next time, see [Plugins](plugins.md#the-slow-commands-bundle)), `chi bundle install agents-md` (a plugin: a nested AGENTS.md is steered into the conversation on the first touch under its directory, see [Plugins](plugins.md#the-agents-md-bundle)) and `chi bundle install skills` (a plugin: `/skill`, versions of skills, see [Plugins](plugins.md#the-skills-bundle)).
511
588
  - Installing a bundle executes its hook code at `Engine` startup. Only install bundles you trust, as you would a gem. Hooks are **not** executed at install time (copy-only); they are `module_eval`'d at `Engine.new` inside per-bundle `Samagotchi::Bundles::<name>` namespaces (no top-level `require` collisions). Keep hook files side-effect-free at load time; do work in `#call` — top-level side effects (require, IO, `at_exit`, global assignment) run once per `Engine.new` (class redefinition is idempotent).
512
589
 
513
590
  Lifecycle: