samagotchi 0.159.0 → 0.161.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 (143) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +125 -1
  3. data/docs/architecture.md +12 -9
  4. data/docs/cli.md +7 -5
  5. data/docs/context.md +10 -1
  6. data/docs/guardrails.md +33 -5
  7. data/docs/internals/attached-context.md +4 -2
  8. data/docs/internals/bridge.md +28 -6
  9. data/docs/internals/bundles.md +24 -5
  10. data/docs/internals/engine.md +7 -1
  11. data/docs/internals/objectives.md +8 -2
  12. data/docs/internals/remote-sessions.md +2 -1
  13. data/docs/memory.md +43 -13
  14. data/docs/plugins.md +13 -4
  15. data/docs/releasing.md +7 -4
  16. data/docs/scheduling.md +53 -8
  17. data/docs/sessions.md +17 -12
  18. data/docs/testing.md +9 -1
  19. data/lib/samagotchi/bridge/card_store.rb +31 -3
  20. data/lib/samagotchi/bridge/ring_buffer.rb +11 -1
  21. data/lib/samagotchi/bridge/sse_writer.rb +8 -5
  22. data/lib/samagotchi/bridge.rb +103 -44
  23. data/lib/samagotchi/bridge_client.rb +9 -6
  24. data/lib/samagotchi/broadcast/scope_card.rb +2 -1
  25. data/lib/samagotchi/broadcast_command.rb +24 -8
  26. data/lib/samagotchi/bundle_command.rb +57 -6
  27. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  28. data/lib/samagotchi/bundles/system/memory_guide.md +2 -2
  29. data/lib/samagotchi/child_reports.rb +29 -23
  30. data/lib/samagotchi/client_id.rb +6 -0
  31. data/lib/samagotchi/command_intake.rb +56 -0
  32. data/lib/samagotchi/compat.rb +32 -0
  33. data/lib/samagotchi/context_absorber.rb +6 -3
  34. data/lib/samagotchi/context_candidate_choice.rb +8 -5
  35. data/lib/samagotchi/context_candidates.rb +11 -8
  36. data/lib/samagotchi/context_candidates_log.rb +10 -1
  37. data/lib/samagotchi/context_command.rb +10 -4
  38. data/lib/samagotchi/context_move.rb +6 -1
  39. data/lib/samagotchi/context_poller.rb +5 -2
  40. data/lib/samagotchi/context_sources.rb +6 -4
  41. data/lib/samagotchi/engine/events.rb +25 -2
  42. data/lib/samagotchi/engine/guardrails.rb +3 -4
  43. data/lib/samagotchi/engine/plugins.rb +1 -1
  44. data/lib/samagotchi/engine/steering.rb +2 -2
  45. data/lib/samagotchi/engine.rb +1 -0
  46. data/lib/samagotchi/engine_peers.rb +1 -0
  47. data/lib/samagotchi/events.rb +71 -0
  48. data/lib/samagotchi/guardrails/approvals.rb +27 -3
  49. data/lib/samagotchi/guardrails/child_boundary.rb +37 -4
  50. data/lib/samagotchi/guardrails/protected_paths.rb +8 -5
  51. data/lib/samagotchi/guardrails/targets.rb +25 -6
  52. data/lib/samagotchi/hook_routes.rb +0 -1
  53. data/lib/samagotchi/live_versions.rb +7 -2
  54. data/lib/samagotchi/memory_bundle/asset_installer.rb +86 -18
  55. data/lib/samagotchi/memory_bundle/bundle_hook.rb +7 -27
  56. data/lib/samagotchi/memory_bundle/file_actions.rb +3 -4
  57. data/lib/samagotchi/memory_bundle/file_state.rb +11 -0
  58. data/lib/samagotchi/memory_bundle/index_updater.rb +7 -4
  59. data/lib/samagotchi/memory_bundle/installed_bundle.rb +1 -1
  60. data/lib/samagotchi/memory_bundle/installer.rb +93 -23
  61. data/lib/samagotchi/memory_bundle/manifest.rb +6 -0
  62. data/lib/samagotchi/memory_bundle/provenance.rb +158 -6
  63. data/lib/samagotchi/memory_bundle/shipped_update.rb +7 -4
  64. data/lib/samagotchi/memory_bundle/status.rb +3 -3
  65. data/lib/samagotchi/memory_bundle/system_bundle.rb +2 -0
  66. data/lib/samagotchi/memory_bundle/uninstaller.rb +29 -13
  67. data/lib/samagotchi/memory_orphans.rb +54 -13
  68. data/lib/samagotchi/memory_paths.rb +29 -6
  69. data/lib/samagotchi/memory_usage.rb +23 -12
  70. data/lib/samagotchi/note_command.rb +9 -1
  71. data/lib/samagotchi/note_delivery.rb +34 -24
  72. data/lib/samagotchi/note_move.rb +205 -0
  73. data/lib/samagotchi/objectives_command.rb +3 -1
  74. data/lib/samagotchi/owner_lock.rb +7 -1
  75. data/lib/samagotchi/plugin/attached_context.rb +3 -1
  76. data/lib/samagotchi/plugin/sessions.rb +4 -1
  77. data/lib/samagotchi/project_command.rb +58 -20
  78. data/lib/samagotchi/project_id.rb +102 -0
  79. data/lib/samagotchi/project_id_clash.rb +62 -0
  80. data/lib/samagotchi/project_init.rb +214 -10
  81. data/lib/samagotchi/project_scope.rb +11 -2
  82. data/lib/samagotchi/review_start.rb +5 -3
  83. data/lib/samagotchi/schedule_command.rb +12 -0
  84. data/lib/samagotchi/schedules.rb +374 -76
  85. data/lib/samagotchi/send_command.rb +2 -15
  86. data/lib/samagotchi/session.rb +59 -7
  87. data/lib/samagotchi/session_access/local.rb +53 -22
  88. data/lib/samagotchi/session_access/remote.rb +13 -5
  89. data/lib/samagotchi/session_access/web_bridge_client.rb +2 -2
  90. data/lib/samagotchi/session_access.rb +20 -4
  91. data/lib/samagotchi/session_commands.rb +7 -3
  92. data/lib/samagotchi/session_continue.rb +162 -24
  93. data/lib/samagotchi/session_delete_command.rb +3 -1
  94. data/lib/samagotchi/session_inbox.rb +41 -1
  95. data/lib/samagotchi/session_inbox_delivery.rb +254 -0
  96. data/lib/samagotchi/session_manager/delivery.rb +66 -12
  97. data/lib/samagotchi/session_manager/emptiness.rb +7 -2
  98. data/lib/samagotchi/session_manager/lifecycle.rb +2 -1
  99. data/lib/samagotchi/session_manager/listing.rb +4 -1
  100. data/lib/samagotchi/session_manager/removal.rb +130 -26
  101. data/lib/samagotchi/session_manager/spawn.rb +167 -100
  102. data/lib/samagotchi/session_manager/worker_command.rb +7 -19
  103. data/lib/samagotchi/session_manager.rb +1 -0
  104. data/lib/samagotchi/session_retention.rb +53 -23
  105. data/lib/samagotchi/session_setup.rb +27 -7
  106. data/lib/samagotchi/starting_marker.rb +93 -24
  107. data/lib/samagotchi/state_format.rb +129 -0
  108. data/lib/samagotchi/system_one/runs.rb +6 -1
  109. data/lib/samagotchi/system_one/runs_store.rb +18 -14
  110. data/lib/samagotchi/terminal_ui/attach_launcher.rb +7 -4
  111. data/lib/samagotchi/terminal_ui/attached_loop/composer.rb +5 -2
  112. data/lib/samagotchi/terminal_ui/attached_loop.rb +5 -0
  113. data/lib/samagotchi/terminal_ui/version_lines.rb +4 -2
  114. data/lib/samagotchi/terminal_ui.rb +2 -3
  115. data/lib/samagotchi/tools/context_read.rb +7 -2
  116. data/lib/samagotchi/tools/delegate.rb +19 -6
  117. data/lib/samagotchi/tools/schedule_task.rb +5 -3
  118. data/lib/samagotchi/tools/send_note.rb +10 -4
  119. data/lib/samagotchi/turn_stages/context_judge.rb +7 -7
  120. data/lib/samagotchi/turn_state.rb +108 -4
  121. data/lib/samagotchi/update_command.rb +9 -4
  122. data/lib/samagotchi/verify_fresh.rb +27 -9
  123. data/lib/samagotchi/version.rb +1 -1
  124. data/lib/samagotchi/web/app.rb +6 -5
  125. data/lib/samagotchi/web/public/app.js +2 -7
  126. data/lib/samagotchi/web/public/chains.js +4 -4
  127. data/lib/samagotchi/web/public/data.js +18 -2
  128. data/lib/samagotchi/web/public/info_bar_model.js +1 -1
  129. data/lib/samagotchi/web/public/session_fields.js +3 -2
  130. data/lib/samagotchi/web/public/session_state.js +2 -0
  131. data/lib/samagotchi/web/public/update_notice.js +2 -2
  132. data/lib/samagotchi/web/session_access_routes.rb +15 -11
  133. data/lib/samagotchi/web/session_control.rb +9 -3
  134. data/lib/samagotchi/web/session_create.rb +8 -4
  135. data/lib/samagotchi/web/session_hub.rb +50 -5
  136. data/lib/samagotchi/web/session_summary.rb +5 -2
  137. data/lib/samagotchi/worker.rb +61 -11
  138. data/lib/samagotchi/worker_boot.rb +22 -11
  139. data/lib/samagotchi/worker_inbound.rb +62 -10
  140. data/lib/samagotchi/worker_link.rb +4 -1
  141. data/lib/samagotchi/worker_presence.rb +106 -0
  142. data/lib/samagotchi/worker_sidecar.rb +3 -4
  143. metadata +9 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ab720ab347312f152fc99a03ca8cd423a640de2c9b4221ccc70803396a2d8c09
4
- data.tar.gz: 313bbe5c7cce008df50c73de233557824723566db8449a8e3a04ff01e85343f9
3
+ metadata.gz: d3a8c4d1d4d37965fda091bc5c48281cdf352d6f3e496d5e5073f72777fe5994
4
+ data.tar.gz: 74a26cfde1a9028c44763b99cdc60ce044028598ddd2955b244338076ef8fd86
5
5
  SHA512:
6
- metadata.gz: 4c02cc469245adaa130c74b1e10978c11b51e85f7ff8c2fe9c27d0de68682d0452f07d9dec7f9100c032dac9cf7357bff81fc70586c9c6cb1e99ab0327c68d56
7
- data.tar.gz: 7ffaacdca84dc1396e5f6edd1160b54e063daa38b71d9aa7c36d7619495d26271606efd277024f344e18043ed2112f3343ea127560438bea0f5480b98df617f3
6
+ metadata.gz: 1de962941edd2efc63a07e566954a8e6cd212891c4858db791667341e0eca601b0e920a4661198760317f5133ee26e3411d9cad570a63a87e861527ab8e21a7e
7
+ data.tar.gz: 0fb530ff96ccbfe79f2dc04b4cba6d59fc1b0ee6a921cf593907d9159b60eec1bed10c3360f1777417f30333c9afd36090d2b3abf7f58b9e4e4cf1b27e558946
data/CHANGELOG.md CHANGED
@@ -8,6 +8,128 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.161.0] - 2026-11-10
12
+
13
+ ### Added
14
+
15
+ - `chi bundle upgrade NAME` and `chi bundle uninstall NAME`, when the bundle's plugin changed (or an upgrade brought
16
+ one in) and a session running it can be restarted, end with one line naming those sessions and
17
+ `chi sessions restart ID` for each (nothing on a dry run, with no restartable worker, or when the plugin is
18
+ unchanged — only the plugin stays in a worker's memory; its rules reload by stamp and its scripts run from disk).
19
+
20
+ ### Changed
21
+
22
+ - Worker restart hints now follow one compatibility rule: the current and previous minor release are supported.
23
+ - `chi bundle install` and `chi bundle upgrade` refuse a memory, plugin or script whose bytes disagree with the
24
+ sha256 its manifest declares, before writing anything (`Checksum mismatch for FILE: ...; chi bundle install
25
+ --force installs it anyway`, exit 1). `--force` installs it anyway, as it already does for an edited memory (its
26
+ copy going to the trash first) and for a bundle whose `requires_chi` this chi doesn't meet; a dev install, a
27
+ non-strict run, `chi update` and a bundle profile keep the warning-only behaviour.
28
+ - Core guardrail checks judge a plugin tool by what its `targets:` say it acts on, not by its name: paths it
29
+ declares it writes (`writes: true`) are checked like `write` and `edit`. See docs/plugins.md (Guardrails).
30
+ - `chi context push` says what a wake push did: `· chi woken` (an idle-exited session started), `· chi is stopped:
31
+ it sees this at its next start` (a stopped session is never started) or `· chi is starting`; chi web's push
32
+ answer carries it as `wake`. A delegate's report, or a push, now wakes a session whose file still said stopped
33
+ without a `chi sessions stop` behind it: only the stop marker means stopped.
34
+
35
+ ### Fixed
36
+
37
+ - A session being started is no longer pulled from under its worker: a session resumed while the retention sweep runs
38
+ is not deleted, two starts at once (chi web and a `chi send`, a delegate's report) start one worker, and a second
39
+ continue of a chain can't archive the link the first one just started. A deleted session no longer comes back
40
+ when a worker that still had it saves.
41
+ - A `chi sessions stop` that lands while a session's worker is leaving (an idle exit, `/exit`, a signal) is no longer
42
+ undone by a new worker started for input or delegate reports that were waiting: the session stays stopped and
43
+ they wait for its next start, as a stop during a restart already did.
44
+ - Session deletion and retention now also end participant grants and purge System 1 runs and context-candidate records.
45
+ - An interrupted continue now repairs its archived previous link when no successor was saved, so the session stays listed.
46
+ - A note sent to a session that was continued (`chi note`, `send_note`, `chi broadcast`, chi web) reaches the new link
47
+ (`chi note` prints `OLD → NEW (continued)`), and notes waiting when it was continued move with it; a note that
48
+ still lands in the old link as it is continued is taken by the new link's worker, and a continue that fails gives
49
+ the notes back. An `/objectives` edit stays in its own session.
50
+ - `chi bundle uninstall NAME` of a bundle whose install was interrupted before its record (only the
51
+ `pending.json` journal left) retags the index line of a kept shared file in the journal's own target folder, not the
52
+ current folder's: run from another project, it no longer edits that project's `index.md` for a file living elsewhere.
53
+ - A schedule counts a firing only after its turn completes: failed, canceled or unanswered turns now count toward
54
+ `max_failures` with the reason visible in `chi schedule list` and `show`.
55
+ - A schedule firing left without a turn after a process interruption is recorded as a failure instead of staying
56
+ stuck; its uncertain occurrence is not replayed.
57
+ - `chi schedule run-now` reserves an entry under the tick lock before firing, preventing an overlapping tick from
58
+ starting the same work.
59
+ - Retention protects an enabled schedule's stored anchor and head as well as its resolved target; cancel the
60
+ schedule with `chi schedule cancel ID` before deleting any of them.
61
+ - Schedule add, list and `schedule_task` warn when the ticker is stale and auto-tick is off; acquired ticks record
62
+ their timestamp.
63
+ - chi web's schedule tick runs outside the session scan loop, and overlapping hub scans do not start a second
64
+ tick while one is in flight.
65
+ - The web chain chip waits until its session is in the loaded list before saying earlier links were pruned.
66
+ - A `chi send`, delegate or schedule delivery whose Bridge answered an error after it had queued the prompt no
67
+ longer runs it twice: the client makes the enqueued id, the input-file fallback reuses it, and the worker runs
68
+ each id once (also after a restart, a turn that already ran or was cut off by the worker's death).
69
+ - The continue's carry note of unabsorbed pushed context keeps the sources after one trimmed to the per-source 4 KiB
70
+ cap: a source that fits the room left is carried whole even when that room is under the cap, the note ends only at a
71
+ source it has no room for, and "[additional pushed text omitted]" is added whenever text is left out.
72
+ - The memory-orphan hint says how the other folder is keyed: a `chi project init` folder is named by its project id
73
+ instead of "another path's hash", and a project keyed by an id is named by its folder, not by that id (so a marked
74
+ folder whose memories stayed in its id folder after `git init` is described correctly).
75
+ - A browser tab or terminal that connects to a worker with no cursor, after its event ring has wrapped, is re-synced
76
+ from the session instead of replaying from the middle of a turn.
77
+ - Stop (the web button, Ctrl-C in the terminal) names the turn it was pressed for, so one pressed as that turn ends no
78
+ longer cancels the queued turn that starts next.
79
+ - `chi bundle install` and `upgrade` check that the plugin and scripts the manifest names are in the bundle before
80
+ they write anything, so a bundle that lacks one is refused with your memories, plugin and record as they were.
81
+ - A bundle install or upgrade that is killed or fails after it copied memories (a `pending.json` journal in
82
+ `.bundles/NAME/`) no longer leaves them as your own files or local edits: a retry owns them, `chi bundle status`
83
+ shows no edit, the next upgrade fast-forwards them and uninstall removes them.
84
+ - A `delegate` follow-up the child's worker refuses says why, in the same sentence `chi send` prints for a delivery
85
+ that did not go in (`DeliverResult#refusal_text`): "Error: session abc12345 refused: this session's worker predates
86
+ queueing ... and send again" rather than the bare error code.
87
+ - A fork and a continue keep the session's `--memory` and `--mute` lists; a non-delegate child does not inherit the
88
+ reserved `system/delegated` memory.
89
+ - A review fork carries its source's `--mute` list, but not its `--memory` preloads.
90
+
91
+ Update with `chi update`, then restart chi web and running sessions (`chi sessions restart`): workers from
92
+ 0.160.0 keep working (N-1), but the new starting marker, session inbox and stop-during-leave rule apply only to
93
+ workers started by 0.161.0.
94
+
95
+ ## [0.160.0] - 2026-11-09
96
+
97
+ ### Added
98
+
99
+ - A folder made a chi project with `chi project init` keeps its memories when you move or rename it (an `id:` in
100
+ .chi/project.yml); rerun `chi project init` in an older project to adopt one, `--new-id` to split a copied folder.
101
+
102
+ ### Changed
103
+
104
+ - `schedules.json` keeps what a newer chi wrote (its format version, unknown fields, entries this chi can't read) when
105
+ an older chi rewrites it.
106
+ - The `SessionManager.run_session_loop` forwarder for older boot strings is gone: a chi web or terminal still
107
+ running 0.158.0 or older must be restarted after updating, because a worker it spawns names that entry.
108
+
109
+ ### Fixed
110
+
111
+ - A message that starts with `!` runs as a shell command only when a person sends it (a web tab, a chi TUI, `chi send`):
112
+ from a delegating parent, a schedule or a plugin it is refused and nothing runs, and a `/command` line from them is
113
+ plain text for the model. `chi answer`'s lines (a parent agent's `/continue yes`, `/parent-answers`) still run as
114
+ commands, with the same trust as `chi send`.
115
+ - A delegate's task and a chain schedule's firing now start their session as their sender's turn, not yours: its
116
+ `last_turn` origin reads `delegate` or `schedule`, the turn counts as automatic in the session's metrics, and the
117
+ objectives audit skips it as it skips a delegate's follow-up; it no longer counts as your input either (it doesn't
118
+ unarchive the session or reset its wakes). Restart `chi web` (and any worker) after updating: one
119
+ still running the older chi starts chain links whose first message runs as yours.
120
+ - `delegate`, `schedule_task` and `chi schedule add` refuse a task or prompt that starts with `!`, saying why, so the
121
+ model can write it as an instruction. A schedule stored with one before this fails when it fires (counted toward
122
+ its `max_failures`; nothing is started or delivered).
123
+ - A command you allow "for this session" is allowed again only in the same worktree or folder, with the same `env:`
124
+ (kept as a digest); the same text run in another checkout asks again. Session approvals stored before this ask one
125
+ more time.
126
+ - `chi bundle upgrade` and `chi update` report a replaced guardrail rule as Updated, like other bundle assets.
127
+ - Bundle install, upgrade and dry runs now name the guardrail, plugin and script files a new version drops.
128
+ - `chi bundle upgrade --dry-run` names edited files it would copy to the trash, including marked memories.
129
+ - `chi bundle uninstall` names a plugin it removes even when the bundle has no memories or hooks.
130
+ - `chi bundle status` labels retired hooks as present or missing; they are not loaded.
131
+ - `chi update` plan rows name memory files a shipped bundle adds.
132
+
11
133
  ## [0.159.0] - 2026-11-09
12
134
 
13
135
  Update with `chi update`.
@@ -5884,7 +6006,9 @@ and long-lived sessions.
5884
6006
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
5885
6007
  hotkey panel that send selected text or the clipboard to your sessions.
5886
6008
 
5887
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.159.0...HEAD
6009
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.161.0...HEAD
6010
+ [0.161.0]: https://github.com/dm1try/samagotchi/compare/v0.160.0...v0.161.0
6011
+ [0.160.0]: https://github.com/dm1try/samagotchi/compare/v0.159.0...v0.160.0
5888
6012
  [0.159.0]: https://github.com/dm1try/samagotchi/compare/v0.158.0...v0.159.0
5889
6013
  [0.158.0]: https://github.com/dm1try/samagotchi/compare/v0.157.0...v0.158.0
5890
6014
  [0.157.0]: https://github.com/dm1try/samagotchi/compare/v0.156.0...v0.157.0
data/docs/architecture.md CHANGED
@@ -105,7 +105,7 @@ terminal renders the events it gets over the Bridge.
105
105
  | Adapters | `Samagotchi::Client`, `LLM::OpenAIChat`, `LLM::AnthropicMessages`, `LLM::HTTP` | Raw-prompt servers, the OpenAI chat API, Anthropic's Messages API, and the HTTP they share. |
106
106
  | 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, objectives, forget_outputs, find_tools/call_tool (the tool index), plus runtime/output-guardrails. |
107
107
  | Bundles | `lib/samagotchi/memory_bundle/*`, `BundleCommand` | `chi bundle`: install/upgrade (3-way, with a text merge of edited memories), build, uninstall; the install record under `.bundles/<name>/` that the guardrail and plugin loaders read ([internals/bundles.md](internals/bundles.md)). |
108
- | Background | `Samagotchi::Worker`, `Samagotchi::SessionManager` | `SessionManager` (`session_manager.rb`, its parts in `session_manager/`: spawn · worker_command · listing · delivery · lifecycle · removal · emptiness) forks/spawns one worker process per session. The worker process's main (owner lock, detach, signals, project env, what it does as it leaves) is `WorkerBoot` (`worker_boot.rb`), which the boot string loads directly; SessionManager's `run_session_loop` forwarder stays for older spawners' strings. 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. |
108
+ | Background | `Samagotchi::Worker`, `Samagotchi::SessionManager` | `SessionManager` (`session_manager.rb`, its parts in `session_manager/`: spawn · worker_command · listing · delivery · lifecycle · removal · emptiness) forks/spawns one worker process per session. The worker process's main (owner lock, detach, signals, project env, what it does as it leaves) is `WorkerBoot` (`worker_boot.rb`), which the boot string loads directly. 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. |
109
109
  | Web | `Samagotchi::Web::App`, `Samagotchi::Web::Server`, `Samagotchi::Web::SessionHub` | Rack+WEBrick on one port, `127.0.0.1:4567` (index.html + `/api/*` + SSE); with `web.host: lan` also on this machine's LAN address, behind an access token (`Web::Lan`, `Web::Token`). The hub is chi web's projection of the session list, pushed to every tab over `GET /api/events`. |
110
110
  | Sessions | `Samagotchi::Session`, `SessionManager` | File `sessions/<uuid>.json` + sidecar `input/`/`output/`; retention 14d/500, `updated_at desc`, lazy sweep. |
111
111
 
@@ -162,9 +162,8 @@ Unchanged: `ContextProviders`' `{ruby}` (`RbConfig.ruby`: the home's ruby inside
162
162
  project env below, so a `mise env` that sets GEM_HOME reaches it), `chi bundle
163
163
  try/test/check` (`-I LIB_DIR`), hooks (in-process). The worker's boot string, `run_session_loop(id, state_dir:)` and
164
164
  `SAMAGOTCHI_BOOT_FALLBACK` stay the cross-version contract (`spec/worker_contract_spec.rb`): the home only adds the
165
- prelude in front. The string names `WorkerBoot.run_session_loop`, the worker's main; the SessionManager forwarder
166
- remains one more release for boot strings written by older spawners. An older chi that wakes a session doesn't read
167
- home.json and starts its worker as before.
165
+ prelude in front. The string names `WorkerBoot.run_session_loop`, the worker's main. An older chi that wakes a
166
+ session doesn't read home.json and starts its worker as before.
168
167
 
169
168
  **The tools a worker runs** follow the project, not the boot, and only they do: a spawned worker
170
169
  (`WORKER_PROCESS_ENV`; never an in-process loop) calls `ProjectEnv.compute` in `WorkerBoot.run_session_loop`, after the owner
@@ -194,10 +193,14 @@ log's last 4 MB). Rows: [configuration.md](configuration.md#checking-it-chi-home
194
193
 
195
194
  ## Session retention & ordering
196
195
 
197
- - **Files:** `~/.local/state/samagotchi/sessions/<uuid>.json` + `<uuid>/input|output|owner.lock|bridge.json` (XDG-aware), and the `<uuid>/stopped` and `<uuid>/archived` marker files. Durable schedules belong to the machine, not a session: `~/.local/state/samagotchi/schedules.json` and its `schedules.lock`, next to `sessions/` ([Scheduling](scheduling.md)).
196
+ - **Files:** `~/.local/state/samagotchi/sessions/<uuid>.json` + `<uuid>/input|output|owner.lock|bridge.json` (XDG-aware), and the `<uuid>/stopped`, `<uuid>/starting` and `<uuid>/archived` marker files. Durable schedules belong to the machine, not a session: `~/.local/state/samagotchi/schedules.json` and its `schedules.lock`, next to `sessions/` ([Scheduling](scheduling.md)).
198
197
  - **Single owner:** the process running a session's Engine (its worker, or a `--non-interactive` one-shot) holds a flock on `owner.lock` (`OwnerLock`); a second owner backs off, and the web answers 409 for a session a one-shot owns.
199
- - **Status:** `status` is turn state (`idle`/`running`); liveness is the lock. A stop is the `<uuid>/stopped` marker (`Session.mark_stopped`; a resume removes it), which `Session.load` and `.summary_from_file` read as status `stopped`, so no other process rewrites a worker's session file.
200
- - **Retention:** 14 days / 500 cap (env `SAMAGOTCHI_SESSION_RETENTION_DAYS`/`MAX_COUNT`, optional `KEEP_STATUS`), live-owner guard, only when `*.json` present; lazy sweep at most once per `session.sweep_interval_hours` (default 24) from the session hub's full-probe tick (and on `GET /api/sessions`, which the page no longer calls), manual via `bin/chi sessions prune --dry-run`.
198
+ - **Status:** `status` is turn state (`idle`/`running`); liveness is the lock. A stop is the `<uuid>/stopped` marker (`Session.mark_stopped`; a resume removes it), which `Session.load` and `.summary_from_file` read as status `stopped`, so no other process rewrites a worker's session file. A resume writes nothing else: the worker, holding the lock, turns a status from before it idle (`Worker#settle_stale_status`: `stopped` with no marker, `error`, a `running` no turn is behind; not a new session's `running` whose first prompt waits), and saves at start whatever the status, which stamps `updated_at` as the resume's save did (a session woken with no turn stays young for the sweep). N-1: a newer spawner starting an older worker (a checkout's chi spawning the installed one) loses that reset; a restart covers it.
199
+ - **Starting:** every spawn marks the session starting (`<uuid>/starting`, `{pid, at}`, `StartingMarker`) until its worker holds the lock: `spawn_worker_for_session` writes it with its own pid before `Process.spawn` and the starter's after (the starter lives until the worker holds the lock, at most `WORKER_START_GRACE`; a spawn that raises or whose starter is gone leaves none), `spawn_session` before the new file is saved, and `Worker#start` removes it. It holds for 60 s at most, and only while the pid it names runs. A resume claims it (`StartingMarker.claim`, a link(2): of resumes racing from any processes one wins), then reads the owner again. A stop marker cancels the hold-off (`start_pending?` = starting and not stop-marked) for a start marked before the stop only: of two sends after a stop, the claim lets one spawn. The stop marker carries its own `at` (`{"at"}`, on the starting marker's clock; `Session.stopped_at`), an older chi's empty one its mtime. The sweep, archive (`:starting`) and delete (refused as an owned session) honour it. N-1: an older spawner writes none for a resume (the owner checks stay); an older worker never leaves one it was given (every worker clears it at start); a leftover one is stale by age or pid (a claim's temp files a kill left, `starting.*`, count as skeleton); a 0.160 spawner or chi web still holds off on a fresh marker under a stop (it reads `starting?` before the stop), until the start ends or 60 s. `SessionManager` keeps only its starters' pids, for chi web's restart to adopt (`live_spawn_pids`), and decides nothing from them.
200
+ - **Deleting:** `delete_session`, the retention sweep and a failed continue's cleanup use `remove_files` for the session JSON, sidecar dir, plugin state, attached context, participant grants, System 1 runs and context-candidate records. The JSON and dir go through `remove_session_files`, holding the owner lock (kind `deleting`, which `stop_session` never signals; not waited for: a worker that took it since the check keeps the session) and reading the starting marker again under it (a resume that claimed the start just before keeps the session; one after it sees the lock held). N-1: an older chi's stop reading the owner then would TERM the deleter (chi web, the CLI) in that moment; an older reader shows the kind as unknown. `Session#save` of a session read from its file (or saved once) skips a file that is gone (log `save_skipped_deleted`) rather than recreate it; only a new session's first save creates one.
201
+ - **Worker presence:** whether a worker is up, coming or held off is read once per decision as a `WorkerPresence` (`worker_presence.rb`, a `Data.define`), the deciders' one reader of those facts (display and list rows still read `session_owner` / `WorkerLink` themselves): the owner lock's holder, the stop marker (`stop_marked?`: the only "stopped" a decider reads, never the session file's status), the `starting` marker (`StartingMarker`, `starting?`, `start_pending?`: every spawn writes it, see Starting), the archive marker, the session file's existence, and the `WorkerLink` outcome when asked (never the file itself: a scratch session's flag is read by `WakePolicy`, last). The deciders read it: `resume_session` (tui, owned, `resume_held_off?`), the session inbox's `WakePolicy` (a ring, a push's wake, a note, a fresh verdict, `wake_for_report`, `deliver_turn`'s two liveness checks, `WorkerBoot`'s leave: below), `Local#wake`/`#worker`, `AttachLauncher`, `Schedules#busy_reason`, archive, delete and the retention sweep. Each keeps its own subset of the facts and their order.
202
+ - **Session inbox:** a delivery into a session (`SessionInbox.deliver(kind, id) { |at| write }`, `session_inbox_delivery.rb`) runs one path: resolve the link (`SessionInbox::Head.resolve`: send, push and a fresh verdict are pinned to the id named; a note goes to the chain's newest link; a ring's file stays with the parent `parent.json` names and its wake goes one hop, to the link whose `ChildReports` reads that file; only an archived id is walked, with one `Session.list`), refuse before any write (a gone session; a one-turn run's or a scratch session for send and a note), write with the kind's own writer (formats unchanged), read `WorkerPresence` once, and apply the kind's `WakePolicy` row. `decide` reads in one order for every kind: gone, one-turn run (`:owned`), owned (`:live`), the stop marker (`:stopped`: the only "stopped", the file's status never decides; send un-stops), archived (send alone starts one), starting (`:starting`, never a second spawn), the kind never starts (`:none`: a note, a fresh verdict) or wasn't asked to (`:off`), scratch (the session file is read only here), else `:start` through `SessionManager.start_for_inbox` (`resume_session`, its claim on the starting marker; it answers `:woken` only when it spawned the worker, `:starting` when another start won the claim, `:live` when a worker took the lock since). N-1 gap: a session file that says `stopped` with no stop marker (stopped before the marker existed, or a resume whose spawn failed after it cleared the marker) is woken, and an older worker, which has no `Worker#settle_stale_status`, exits on its first pass; the wake still reads `:woken`, and each later ring or push spawns another such worker (bounded by the rings and pushes). Restarting chi on the new version (`chi web`, the workers) closes it. `SessionInbox.wake` is the wake half alone (`wake_for_report`, a failed continue's rings). On it: `ChildRing.ring` (its start on a thread), `Local#context_push`'s wake (its outcome is `ContextPush#wake`, the push line's ending), `NoteDelivery.deliver` (every note writer a person or another session drives: `chi note`, `send_note`, `chi broadcast`, chi web's notes route; `NoteDelivery::Result#id` is the link that has it, `#asked` the one named; `/objectives` passes `pin: true`: its list is its own session's) and `VerifyFresh.forward` (`:fresh_verdict`: the child's parent, pinned, no worker). Send keeps its Bridge/file flow in `SessionManager.deliver_turn` (pinned to the id it names: an archived link gets the message and comes back); its two liveness checks (before the post, after a file's write) read the `:send` row and leave the refusals to `resume_session`'s wording. One row is not a delivery: `:leave`, `WorkerBoot.run_session_loop`'s successor as a worker leaves (input left after a signal, an idle exit's or `/exit`'s input or rings, a restart), read on a fresh presence once the lock is free: a stop that landed as it left starts none for any way out (`resume_session` would clear the stop; logged `respawn_dropped`, `restart_dropped` for a restart, whose reminders go with it), the input and rings stay for the next start. The notes a spawn writes into the session it starts stay direct `SessionInbox.write_note` calls, pinned by construction: spawn_session's chain note, `ContextMove`'s notes, `Objectives.carry`, `ChildMove`'s note to a moved child. A continue moves the previous link's waiting notes to the new link last in its `before_spawn` (`NoteMove.move`), a failed one gives them back before the link goes (`NoteMove.give_back`), and the new link's worker adopts what the archived links before it get after their continue (`NoteMove.adoption`, from `WorkerInbound#absorb_notes` and `Worker#start`: back along the chain through up to 8 archived links, from each only notes from the creation of the link that continued it, never an objectives list). `write_note` refuses a session with no file and no folder (`Session::NotFound`), so a late writer can't bring back a deleted link's folder.
203
+ - **Retention:** 14 days / 500 cap (env `SAMAGOTCHI_SESSION_RETENTION_DAYS`/`MAX_COUNT`, optional `KEEP_STATUS`), live-owner and starting guard (looked at again right before a session's files go), only when `*.json` present; lazy sweep at most once per `session.sweep_interval_hours` (default 24) from the session hub's full-probe tick (and on `GET /api/sessions`, which the page no longer calls), manual via `bin/chi sessions prune --dry-run`.
201
204
  - **Ordering:** `Session.list(sort:,order:,limit:,offset:)` and `GET /api/sessions?sort=&order=&limit=&offset=` default `updated_at desc`; Web UI sort/filter/pagination.
202
205
  - **Test hygiene:** `test_run` flag when `SAMAGOTCHI_ENV=test`/`RACK_ENV=test`/`CI`, targetable via `prune --test-only` / `clean`.
203
206
 
@@ -471,7 +474,7 @@ model: [internals/remote-sessions.md](internals/remote-sessions.md).
471
474
  | `DELETE /api/sessions/:id/context/:name?own=1` | owner | | 200 `{status: detached}`: the session's own only | 409 `project_source` (chi context rm's words); 404 |
472
475
  | `POST /api/sessions/:id/stop` | owner | `wait?` (seconds, capped at `SessionManager::STOP_WAIT`, 10: `chi --remote NAME sessions stop` sends it) | 200 `{status: stopped, session_id, released}`: `released`, whether the worker let go within the wait (without one `STOP_WAIT_SECONDS`, 2 s: the page's Stop) | 409 `owned_by_tui` |
473
476
  | `DELETE /api/sessions/:id?force=` | owner | `force=true` stops a live worker first (the page sends it) | as before | 409 `worker_running` without it |
474
- | `GET /api/sessions/:id?brief=1` | participant (theirs: title, status, participants) | | 200 `{brief, session: {id, status (the file's), title, display_title, project_root, working_directory, model_name, model_typed, parent_answers, owner}, pending_question}` (the file's; no messages): `Remote#show` | 404 |
477
+ | `GET /api/sessions/:id?brief=1` | participant (theirs: title, status, participants) | | 200 `{brief, session: {id, status (the file's), title, display_title, project_root, project_id, working_directory, model_name, model_typed, parent_answers, owner}, pending_question}` (the file's; no messages): `Remote#show` | 404 |
475
478
  | `POST /api/sessions/:id/turn` (its 202) | owner | | the ack with `via`: `bridge` or `file` (the input file: a remote `chi send --wait` follows that turn) | |
476
479
  | `GET /api/sessions/:id/context?full=1`, `…/context/:name?full=1` | owner | | each row with `source` (`Source#to_h`, its command too; the page's rows never carry one), `snapshot` (`Snapshot#to_h`, no text) and `shadowed`, shadowed project sources included | |
477
480
  | `POST /api/sessions/:id/wake` | owner | `wait` (seconds, default and most 10: `WAKE_MAX_WAIT`) | 200 `{session_id, woke, worker: {epoch, started_at, version, features}, model_name, default_model, parent_strategy, installed}`: the live worker as is (`woke: false`), else resumed and its Bridge waited for (`Local#wake`); `default_model` and `installed` are this machine's | 409 `owned_by_tui`, `scratch` (`Session::SCRATCH_REFUSED`); 504 `worker_timeout` (`LaunchMode::BRIDGE_LATE`); 400 `invalid_wait`; 404 |
@@ -655,7 +658,7 @@ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/
655
658
  | Dialects | `llm/dialect.rb` (`LLM::Dialect`) | What an `api: anthropic` provider's `/v1/messages` does differently (`dialect: fireworks`) ([internals/anthropic.md](internals/anthropic.md)). |
656
659
  | Logging | `log.rb` (`Log`), `log_subscriber.rb` (`LogSubscriber`), `log_path.rb` (`LogPath`), `log_line.rb` | The debug log: tagged records, a session's event trail, where the file goes ([configuration.md](configuration.md#debug-log-file)). |
657
660
  | Stores | `reminder_store.rb` (`ReminderStore`), `recap_store.rb` (`RecapStore`) | A session's reminders, and its idle recap (`recap.json`). |
658
- | Schedules | `schedules.rb` (`Schedules`, its `Entry`, `Tick`, `Index`), `schedule_command.rb` (`ScheduleCommand`) | Durable schedules that outlive every worker: `<state dir>/schedules.json` and its `schedules.lock`, the tick's busy check (a live worker asked over the Bridge's `GET /activity`, the files when there is none), the chain prune. `chi schedule` is this machine only. See [Scheduling](scheduling.md). |
661
+ | Schedules | `schedules.rb` (`Schedules`, its `Entry`, `Tick`, `Index`), `schedule_command.rb` (`ScheduleCommand`) | Durable schedules that outlive every worker: versioned `<state dir>/schedules.json` and its `schedules.lock`; rewrites preserve unknown keys and entries missing required members. The tick's busy check (a live worker asked over the Bridge's `GET /activity`, the files when there is none), the chain prune. `chi schedule` is this machine only. See [Scheduling](scheduling.md). |
659
662
  | Reply waits | `reply_wait.rb` (`ReplyWait`, `GONE_AFTER`: the one dead-worker rule), `reply_wait/poll.rb` (`ReplyWait::Poll`, one look per poll), `reply_wait/target.rb` (`ReplyWait::Target`: which turn a wait binds, cursor, baseline, `owner_grace`; `first_message`, `message`, `wait_only`, `answer`, `participant`, `from_cursor`), `reply_wait/liveness.rb` (`ReplyWait::Liveness`: one owner and link read per look; `#owner?` for gone, `#worker_owned?` for an open question) | `chi send --wait`, `chi answer`, a participant's wait, the delegate tools and delegate reports wait through `ReplyWait.call(id, **target.to_h, …)` ([sub-agent.md](sub-agent.md)). |
660
663
  | Session rows | `session_row.rb` (`SessionRow`) | A session as the lists see it: scalar facts only, built by `SessionManager.session_rows` (one owner, link, recap and analytics read each), `#to_summary_h` the summaries route's body and `.from_summary_h` a remote's rows read back, `#cli_line` the one `chi sessions list` line, `.running_or_starting?` the delegates' and Verify fresh's "running" (a 15 s grace for a worker on its way). A leaf: never session_manager or web. |
661
664
  | Setup and update | `bootstrap_command.rb`, `bootstrap/`, `update_command.rb`, `gem_update.rb` | `chi bootstrap` (first config.yml, system bundle, core profile) and `chi update` (the gem, shipped bundles, the macOS helper). |
data/docs/cli.md CHANGED
@@ -36,8 +36,8 @@
36
36
  - `chi self` — print version, source dir (checkout or installed gem), config/memory/session paths, model/host and bundles. Run by a session's `execute`, the `model` row (and host, loop, profile, thinking, served model) is **that session's** model, labelled `(this session <id8>; default …)`; elsewhere it is the default, `(default)`. `model key` is the memory overlay key (`<name>.<key>.md`), and `big memories` the memories over `memory.entry_warn_tokens` ([Memory](memory.md#a-memorys-size)), and `model notes` the model notes a prompt of that model loads (`model_notes_deepseek (system, 612 chars)`, `none`; inside a session its `--mute` list is left out). `chi self --model` prints only the model a new session starts on, an alias resolved to its target (the desktop helper's hint)
37
37
  - `chi update [--dry-run] [--to VERSION] [--json] [--no-gem] [--no-bundles] [--no-desktop]` — update an installed chi: the gem, the system bundle, the shipped bundles you installed and the desktop helper, and check the bundles from elsewhere, in one table (see [Updating](#updating))
38
38
  - `chi home`, `chi home set [--ruby PATH]`, `chi home doctor` — the chi home: the one Ruby (and its gem path) every chi launch boots, whoever launches it. `chi home` shows it (its Ruby, the newest chi in it, gem home and path, `mise`, when and by which chi it was set, and the chi you ran when that is another one; exit 1 with the problem when it can't boot chi), `set` records this chi's Ruby and gem path (or `--ruby PATH`'s own; it must have samagotchi), `doctor` checks it and what should follow it, one row each with `ok`, `warn` or `fail` and what to run (exit 1 on a `fail`; see [Checking it](configuration.md#checking-it-chi-home-doctor)). At a terminal, a chi on another Ruby than the home's says so in one dim line a day. This machine only (see [The chi home](configuration.md#the-chi-home))
39
- - `chi project`, `chi project init [DIR]` — this folder's project, and making a folder in no git repository one. `chi project` prints how its project was found (`kind` `git`, `marker` or `none`), its `root` (what memories are keyed on), its `top` (where the AGENTS.md chain starts) and its project `memories` folder, with a `note` for a marker a repository makes ignored, or a `$HOME` that is a git repository. `init` writes `DIR/.chi/project.yml` (DIR defaults to this folder and is created when missing), so DIR and every subfolder share one memories folder, group their sessions and take `chi context --project`; it writes nothing in a git repository (the repository already is the project, exit 0), says so for a folder already marked, refuses `$HOME`, a folder above it and `/` (exit 1), and marks a folder inside another marked project as its own, nested one. It prints the root and the memories folder (with the memories already there: a folder chi ran in at its top keeps them), the running sessions in DIR to restart (`chi sessions restart ID`: their prompt keeps the old project) and, when DIR has saved sessions, that a running chi web groups them after its restart. This machine only. See [Projects without git](memory.md#projects-without-git)
40
- - `chi bundle install|upgrade|uninstall|status|diff|list|build|trash|new|test|check` — manage memory bundles (see [Plugins](plugins.md) and [Bundle hooks](hooks.md#bundle-hooks)); `list` shows the installed ones and the ones shipped with chi, which `install <name>` installs; `upgrade <name>` upgrades an installed bundle by name from the source it came from (its folder, archive or git URL), `upgrade <source>` from a new one; `core` and `dev` are profiles that install a set of them (see [Bundle profiles](memory.md#bundle-profiles-core-and-dev), [Guardrails](guardrails.md), [Bundles](bundles.md#the-btw-bundle), [the mcp bundle](bundles.md#the-mcp-bundle) [the loop-guard bundle](bundles.md#the-loop-guard-bundle), [the check-in bundle](bundles.md#the-check-in-bundle) and [the skills bundle](bundles.md#the-skills-bundle)). `trash` lists and empties the bundle trash (moved files from uninstalls/upgrades). `install`/`upgrade` `--force` overwrites existing entries and also installs a bundle whose `requires_chi` this chi doesn't meet; without it such a bundle is refused (exit 1), nothing written. A plain `install` of a bundle that is already installed from the same source rewrites its rules, plugin, scripts and record: the summary lists a file that already holds the bundle's bytes as `Up to date: …`, one you wrote as `Skipped: …`, and ends with `NAME was already installed from SOURCE: its rules, plugin and scripts now match it and its record was rewritten` (plus, when a memory was kept, `your own copies of its memories were kept (chi bundle upgrade NAME merges the bundle's changes into them)`). A source with class-file hooks (`hooks/*.rb` or a manifest `hooks:` entry) is refused, `--dry-run` too; an older install's hooks are removed by the next install or upgrade (`Removed (chi no longer loads class-file bundle hooks): …`, an edited one copied to the trash first; see [Class-file hooks are retired](hooks.md#class-file-hooks-are-retired)). `status NAME` lists the bundle's guardrail rules and scripts beside its plugin, each `[ok]`, `[missing]` or `[modified]` (edited: the next install replaces it, its copy kept in the trash; an edited rule doesn't load, so it counts in `issues=`), and `diff NAME` names an edited rule or script with its recorded and current sha256 (chi keeps no base text for them). A usage error (an unknown subcommand or flag, a missing argument, a bad `build --scope`) exits 2, as for the other subcommands. Plain `chi` and `chi web` exit 1 on an unknown flag, a flag missing its value or a stray argument. `new`, `test` and `check` are for writing one: see [Writing a bundle](#writing-a-bundle-new-test-check)
39
+ - `chi project`, `chi project init [--new-id] [DIR]` — this folder's project, and making a folder in no git repository one. `chi project` prints how its project was found (`kind` `git`, `marker` or `none`), its `root`, its `top` (where the AGENTS.md chain starts), a marked project's `id` (what its memories and project context are keyed on; else the root's path is) and its project `memories` folder, with a `note` for a marker a repository makes ignored, or a `$HOME` that is a git repository. `init` writes `DIR/.chi/project.yml` with a new `id:` (DIR defaults to this folder and is created when missing), so DIR and every subfolder share one memories folder (kept when DIR moves or is renamed), group their sessions and take `chi context --project`; it writes nothing in a git repository (the repository already is the project, exit 0), says so (with the id) for a folder already marked, refuses `$HOME`, a folder above it and `/` (exit 1), and marks a folder inside another marked project as its own, nested one. On a folder marked without an id (an older chi's marker) it adopts one: the folders keyed by its path move to the id, each leaving a link by its old name, and it says what moved and names memories an earlier path left (refused, exit 1, while a session of the project runs). `--new-id` gives a marked folder a fresh id with a copy of its memories and project context (a copied folder going its own way; refused elsewhere, exit 1). It prints the root, the id and the memories folder (with the memories already there: a folder chi ran in at its top keeps them, moved to the id), the running sessions in DIR to restart (`chi sessions restart ID`: their prompt keeps the old project) and, when DIR has saved sessions, that a running chi web groups them after its restart. This machine only. See [Projects without git](memory.md#projects-without-git)
40
+ - `chi bundle install|upgrade|uninstall|status|diff|list|build|trash|new|test|check` — manage memory bundles (see [Plugins](plugins.md) and [Bundle hooks](hooks.md#bundle-hooks)); `list` shows the installed ones and the ones shipped with chi, which `install <name>` installs; `upgrade <name>` upgrades an installed bundle by name from the source it came from (its folder, archive or git URL), `upgrade <source>` from a new one; `core` and `dev` are profiles that install a set of them (see [Bundle profiles](memory.md#bundle-profiles-core-and-dev), [Guardrails](guardrails.md), [Bundles](bundles.md#the-btw-bundle), [the mcp bundle](bundles.md#the-mcp-bundle) [the loop-guard bundle](bundles.md#the-loop-guard-bundle), [the check-in bundle](bundles.md#the-check-in-bundle) and [the skills bundle](bundles.md#the-skills-bundle)). `trash` lists and empties the bundle trash (moved files from uninstalls/upgrades). `install`/`upgrade` `--force` overwrites existing entries and also installs a bundle whose `requires_chi` this chi doesn't meet and a memory, plugin or script whose bytes disagree with its manifest `sha256`; without it such a bundle or file is refused (exit 1), nothing written. A plain `install` of a bundle that is already installed from the same source rewrites its rules, plugin, scripts and record: the summary lists a file that already holds the bundle's bytes as `Up to date: …`, one you wrote as `Skipped: …`, and ends with `NAME was already installed from SOURCE: its rules, plugin and scripts now match it and its record was rewritten` (plus, when a memory was kept, `your own copies of its memories were kept (chi bundle upgrade NAME merges the bundle's changes into them)`). A source with class-file hooks (`hooks/*.rb` or a manifest `hooks:` entry) is refused, `--dry-run` too; an older install's hooks are removed by the next install or upgrade (`Removed (chi no longer loads class-file bundle hooks): …`, an edited one copied to the trash first; see [Class-file hooks are retired](hooks.md#class-file-hooks-are-retired)). `status NAME` lists the bundle's guardrail rules and scripts beside its plugin, each `[ok]`, `[missing]` or `[modified]` (edited: the next install replaces it, its copy kept in the trash; an edited rule doesn't load, so it counts in `issues=`), and `diff NAME` names an edited rule or script with its recorded and current sha256 (chi keeps no base text for them). A usage error (an unknown subcommand or flag, a missing argument, a bad `build --scope`) exits 2, as for the other subcommands. Plain `chi` and `chi web` exit 1 on an unknown flag, a flag missing its value or a stray argument. `new`, `test` and `check` are for writing one: see [Writing a bundle](#writing-a-bundle-new-test-check)
41
41
 
42
42
  - `chi guardrails replay [--dry-run] [--threshold T] [--model M]` — seed System 1's record from the approvals this machine's sessions already answered: each ask is judged by the configured model and written to `guardrails/judgments.jsonl` with the answer the user gave, then a per-rule table prints (asks, allowed, denied, would-be auto-allows, false allows, asks with no judgment). A parent agent's answer is not the user's: only its deny is replayed, and only where the parent could have allowed. `--dry-run` judges without writing, `--threshold` overrides `guardrails.system_one.threshold` for the table, `--model` the configured model for this run. Exit 2 when `guardrails.system_one.mode` is off or no `host` is set; a usage error exits 2 (see [System 1](guardrails.md#system-1-experimental))
43
43
  - `chi guardrails rule <id>` — print exactly `asks` when that rule is loaded, not disabled and votes `ask` in this `guardrails.mode` for every model; any other line says why not (no such rule, disabled, another mode, it denies, for some models only, rules that failed to load, `guardrails.enabled: false`). Exit 0 either way, 2 for a usage error. It reads config.yml and the installed bundles as a worker does (`GuardrailWiring#rules`) and takes the rule's id, never a command; the [coordinator skill](bundles.md#the-coordinator-bundle) reads it before it merges without asking
@@ -512,9 +512,9 @@ invalid option (exit 1).
512
512
  | `--parent-answers LEVEL` | A new session's own `guardrails.parent_answers` (`hard`, `standard` or `soft`): what a parent agent may answer on its questions. It may only tighten config.yml's: the stricter of the two applies, and a looser one is saved as given with a warning (`--parent-answers soft is looser than config.yml's guardrails.parent_answers: standard; standard applies`). Saved in the session; a respawned worker, a fork and a continue keep it, a `delegate` child takes its parent's. Ignored with `--attach`/`--resume` (one line, as `--toolkit`); refused with `chi web`. Only you loosen it: `/parent-answers` in the terminal or the web. See [Guardrails](guardrails.md#answers-from-a-parent-agent). |
513
513
  | `--set KEY=VALUE` | A setting for a new session (repeatable; a later one of the same key wins). A core key a session may set (`recap.enabled`, `recap.sentences`) is typed by the key and goes first, over the CLI, env and config.yml: `--set recap.enabled=false` turns the idle recap off for this session; a value it doesn't take (`recap.enabled=maybe`) or a key no session sets (`guardrails.mode`) is an error, exit 2 (see [Per-session settings](configuration.md#per-session-settings)). Any other name is a bundle's `BUNDLE.KEY`, laid over config.yml's `bundles: <bundle>:` in what the bundle's plugin reads (`ctx.settings`): `--set cost-cap.cap=3`. The value is typed simply: an integer (`3`, `-1`) or a decimal (`2.5`, `.5`, `1e1`) is a number, `true`, `yes` and `on` are true, `false`, `no` and `off` are false (any case, as in config.yml), anything else stays the text as given (`010`, `0x10`, `3:30`, `y`, `null`), and so does every value of a key the manifest types `string`. For cost-cap, `0`, a negative number or `off` is no cap. Only a key the installed bundle's manifest lists in `session_settings:` is taken, with a value of the type it gives the key: a bundle that isn't installed (or whose plugin doesn't load), a key it doesn't list (a record installed before the bundle listed it says to run `chi update` or `chi bundle upgrade NAME`), a value of another type (`--set cost-cap.cap=five`), or another shape (`--set foo`, `--set a.b.c=1`) is an error naming what it takes, exit 2, nothing started. Saved in the session; a respawned worker, a fork and a continue keep it, a delegate child doesn't. Ignored with `--attach`/`--resume` (one line, as `--toolkit`); refused with `chi web`. See [Per-session settings](plugins.md#per-session-settings). |
514
514
  | `--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. |
515
- | `--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. |
515
+ | `--memory NAME` | Preload a memory entry into the system prompt (repeatable; a comma list too). Merged under the config.yml `memories:` baseline. Stored on the session; a fork and a continue keep the list too. A fork leaves out reserved `system/delegated` so it does not become a delegate. |
516
516
  | `--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). |
517
- | `--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). |
517
+ | `--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`). A fork and a continue keep the list. Nothing on disk changes. See [Muting a memory](#muting-a-memory). |
518
518
  | `--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. |
519
519
  | `--llm-context-apply RULE` | This session's own apply rule: `payoff`, `next_request` or `turn_end`. |
520
520
  | `--llm-context-budget TOKENS` | This session's own context budget: `64000`, `64k` (4k to 10M), or `off` (wins over the model's budget). |
@@ -804,7 +804,9 @@ file and index line stay as they are; only this session doesn't see it.
804
804
  `muted_memory_names` in the session's JSON), written before the worker
805
805
  starts. A worker respawned by `--resume`, `--attach` or `chi send` builds
806
806
  the same prompt, and a `--resume ID --non-interactive` one-shot keeps the
807
- lists too (merged with the flags it is given).
807
+ lists too (merged with the flags it is given). A fork and a continue keep
808
+ the lists too; a fork drops the reserved `system/delegated` memory, and a
809
+ continuation of a delegate child drops it because the new link is not a delegate.
808
810
  - A mute wins: `--mute user_preferences` drops that config baseline entry for
809
811
  one session, and `--memory x --mute x` is a mute (one warning line).
810
812
  - Names are checked before the launch, warnings only: `Warning: --memory 'x'
data/docs/context.md CHANGED
@@ -252,7 +252,16 @@ you're away from the terminal. All of these must hold:
252
252
  - the session's worker is up and idle: no turn running, nothing queued, no
253
253
  question or continue offer waiting, not just started. A changed push from a
254
254
  session-scoped source can start an idle-exited worker; it never restarts an
255
- explicitly stopped session;
255
+ explicitly stopped session (the push waits for its next start). Only the
256
+ stop marker (`chi sessions stop`) counts as stopped. `chi context push`
257
+ ends its line with what that wake did: `· chi woken`, `· chi is stopped: it
258
+ sees this at its next start` or `· chi is starting` (nothing when the
259
+ worker is up, or the push asked no wake; chi web's push answer carries it
260
+ as `wake`). `· chi woken` means this push started the worker; one another
261
+ start had begun says `· chi is starting`. A worker from an older chi woken
262
+ for a session whose file still says stopped with no `chi sessions stop`
263
+ behind it exits at once: the line still says woken; restarting chi on the
264
+ new version covers it;
256
265
  - under `session.max_wakes` turns in a row with no message from you (the
257
266
  budget delegate reports and finished background tasks use too; a turn that
258
267
  failed, or was stopped before the model call, pauses wakes until you write);
data/docs/guardrails.md CHANGED
@@ -92,10 +92,13 @@ Allowing beyond "once" is stored in `$XDG_STATE_HOME/samagotchi/guardrails/appro
92
92
 
93
93
  | Scope | Allows |
94
94
  |---|---|
95
- | session | this exact call in this session: the tool + command, or for a file tool its paths, so its label names them (`Allow edits to config.yml for the session`) and a later edit of that file needs no ask |
95
+ | session | this exact call in this session: the tool + command, or for a file tool its paths, so its label names them (`Allow edits to config.yml for the session`) and a later edit of that file needs no ask. A command's holds only where it was given: the same worktree or folder (`repo_root`, never the repository's other checkouts) and the same `env:` (a sha256 of it is stored as `env`, never the values) |
96
96
  | repo | this exact call in this repo (the cwd outside a repo), any session |
97
97
  | rule | anything this rule asks about in this repo |
98
98
 
99
+ A session entry of a command from before chi kept its place (no `repo_root`) no longer matches: the call asks once
100
+ more. `/guardrails` shows a session entry with its folder (`session 3fa2c1d0 in /work/app`, `with its env:`).
101
+
99
102
  "This repo" is the git repository, not the folder: an approval given in one
100
103
  worktree holds in the main checkout and every other worktree of it (it is
101
104
  stored with the repository's common git dir as `repo`, and the worktree as
@@ -294,9 +297,26 @@ by editing the session file, or by running `chi --attach` on a PTY with the
294
297
  markers cleared. chi's instructions for parents ([chi as a
295
298
  sub-agent](sub-agent.md)) tell them to use `chi answer` only.
296
299
 
300
+ ## Who may run a session command
301
+
302
+ A message that is a session command (`/model x`, `!cmd`, `!rollback`) runs as the command only when a person sends
303
+ it: a web tab, an attached chi TUI, `chi send` (yours, or one a model runs from `execute`: the trust model above), a
304
+ message with no sender named, and `chi answer`'s lines (`cli:answer`: a parent agent's `/continue yes` and
305
+ `/parent-answers`), which may send any session command, with the same trust as `chi send`.
306
+ `!cmd` runs its shell line with no rule or ask (it is yours), so from anyone else it is refused: a delegating parent's
307
+ task or follow-up, a schedule's firing (both modes), a plugin's message, a context or task wake. Chi web's Bridge answers
308
+ `403 command_not_permitted` with one message, an input file's line shows as the command's error, and nothing runs.
309
+ Their `/word` line is plain text: the model reads it. `delegate`, `schedule_task` and `chi schedule add` refuse a task
310
+ or prompt that starts with `!` with the same message, so the model can rewrite it. A participant's command is refused
311
+ as before ([their review boundary](#a-participants-review-boundary)). The rule is `CommandIntake`
312
+ (`lib/samagotchi/command_intake.rb`), asked by `Bridge#handle_post_turn` and `WorkerInbound#queue_as_command`;
313
+ `POST /command` (a UI's or a card's explicit command line) is not a message and is not covered.
314
+
297
315
  ## Protected paths
298
316
 
299
- Built in, for `write`, `edit` and `memory_write` (symlinks resolved; see
317
+ Built in, for a call whose [targets](plugins.md#guardrails) say it writes
318
+ its paths: `write`, `edit`, `memory_write`, and a plugin tool's `targets:`
319
+ with `writes: true` (symlinks resolved; see
300
320
  [How paths resolve](#how-paths-resolve)):
301
321
 
302
322
  - deny: the approval store's dir, installed bundles (`memories/.bundles/`),
@@ -316,7 +336,9 @@ bundle: a child asks before it changes anything outside its folder, the work
316
336
  tree top of the folder it was started in (a worktree from `delegate cwd:`,
317
337
  else the parent's checkout). Rule `child-boundary`, source `core`; it asks when
318
338
 
319
- - `write` or `edit` names a path outside it (tmp dirs and memory files don't count);
339
+ - a call whose [targets](plugins.md#guardrails) say it writes its paths —
340
+ `write`, `edit`, and a plugin tool's `targets:` with `writes: true` —
341
+ names a path outside it (tmp dirs and memory files don't count);
320
342
  - `execute` or `task_create` runs mutating git in another dir (`git -C`, `cd`,
321
343
  `GIT_DIR`: what `git: outside_repo` matches), or a command that isn't
322
344
  read-only starts outside it (its `cwd:`) or names a path in the repository's
@@ -326,7 +348,12 @@ else the parent's checkout). Rule `child-boundary`, source `core`; it asks when
326
348
  even from its own worktree: a `git config` write (not `--get`, `--list`,
327
349
  `--worktree`), `update-ref`, `worktree add|remove|move|prune`, `stash
328
350
  drop|clear|pop`, creating or deleting a tag, and deleting, forcing or
329
- overwriting a branch other than its own.
351
+ overwriting a branch other than its own;
352
+ - any call whose `targets:` carry a `command:` (a plugin tool's) is checked
353
+ for git that changes what every checkout shares, and for a `cwd:` outside
354
+ the child's folder. Its command's text may not be shell, so the reads a
355
+ shell call gets (its `git -C`/`cd` dirs, its redirects, its read-only test)
356
+ are left out.
330
357
 
331
358
  Read-only commands (`git -C ../app log`, `cat ../app/x`) and paths elsewhere
332
359
  (`/usr/bin`, `~/.gem`) never ask, nor does a read-only command that writes only
@@ -1508,6 +1535,7 @@ the text a rule sees, and a function or alias shadowing a reader in the same
1508
1535
  command isn't seen at all. `outside_repo` covers the file tools and git
1509
1536
  only: `sed -i`, `cp` or `cat > file` into another folder from the shell aren't
1510
1537
  asked about, and neither is git run through `sh -c`, a script, an alias, `make`
1511
- or a folder in a variable. `!cmd` lines typed by you are not checked. The only
1538
+ or a folder in a variable. `!cmd` lines typed by you are not checked (see
1539
+ [Who may run a session command](#who-may-run-a-session-command)). The only
1512
1540
  real defence against an adversarial model is isolation (a sandbox, a git
1513
1541
  identity without push rights).
@@ -51,8 +51,10 @@ comment is the file map and [../context.md](../context.md#storage) lists it for
51
51
  `Candidate`, `Left`, `Moved`/`Stayed`.
52
52
  - **Locations.** A `Location` is a folder of sources: a project's (`projects/<project_key>/`, shared by the
53
53
  project's workers) or a session's (`sessions/<id>/`, which also holds `subscriptions.json`, the `muted/` and
54
- `declined/` markers, `candidates/`, `left.json`). `ContextSources.attached(session_id, project_root:)` is what a
55
- session sees: its own, then the project's it doesn't shadow. Names are `NAME_RE`; `subscriptions` and `left` are
54
+ `declined/` markers, `candidates/`, `left.json`). `ContextSources.attached(session_id, project_root:, project_id:)`
55
+ is what a session sees: its own, then the project's it doesn't shadow. The project key is
56
+ `MemoryPaths.key_for(root:, id:)`, the memories folder's key: a marked project's id (`Session#project_id`, saved at
57
+ creation), else the root's path key; every caller holding a session passes both. Names are `NAME_RE`; `subscriptions` and `left` are
56
58
  reserved (`RESERVED_NAMES`) because their files share the folder.
57
59
  - **Writes are file replaces** (`AtomicFile`). `Location#locked(name)` takes `<name>.lock`, the lock a fetch holds
58
60
  for its whole command; `record_text` and `record_error` run under it, and `remove` leaves the lock file so a fetch
@@ -195,8 +195,11 @@ ring holds the event. `serve!` orders a join so that nothing is missed or double
195
195
 
196
196
  A **`reset`** frame (the same content, numbered with its own seq) is written when a cursor cannot be replayed: it is
197
197
  from another epoch, ahead of the engine (a restarted worker), behind the ring's window (a cursor at `oldest - 1` has
198
- seen everything before the window and replays), or the queue overflowed since the last drain. A cold connect replays
199
- the whole window. Replay state is in memory per worker; there is no durable resume.
198
+ seen everything before the window and replays), or the queue overflowed since the last drain. A cold connect (no
199
+ cursor) is behind the window too once the ring has evicted an event (`RingBuffer#dropped?`: a replay would start mid-turn), so
200
+ it gets the `reset`; on a ring that never evicted (its first seq can be above 1: events before the Bridge started) it
201
+ replays the whole window. The clients re-sync on a `reset` and
202
+ reconnect with the snapshot's own id, which is inside the window (the web `resync`, the TUI's `follow`). Replay state is in memory per worker; there is no durable resume.
200
203
 
201
204
  Each frame is `id: <seq>-<epoch>` (`EventId`: the seq first so a reader that takes it as a number still gets the
202
205
  seq), `event: <type>`, `data: <json>`. A `: ping` goes out after `heartbeat_interval` (15 s) of silence. A watcher
@@ -235,6 +238,13 @@ The Bridge exposes:
235
238
  sends its requests there (`worker_changed`); it gives up after ~12 s, or at once when the session
236
239
  was stopped.
237
240
 
241
+ **Adding an event type.** Assign every emitted type a lifetime in `Events::CLASSIFICATION`
242
+ (`row`, `state`, `transient` or `internal`); keep stream frames and synthetic replay types in
243
+ `Events::STREAM_FRAMES` and `Events::SYNTHETIC`. `spec/events_drift_spec.rb` checks event coverage
244
+ and names the live web/TUI handlers; for rows it also checks the reload owner (`TurnAccumulator`
245
+ or `CardStore`) and TUI join replay. Follow any documented exception there when a row reloads
246
+ through session messages or a fallback path instead.
247
+
238
248
  The wire chunk (the contract; the engine's own event keeps the provider's `payload` and the raw `content`):
239
249
 
240
250
  ```
@@ -286,9 +296,18 @@ The Bridge exposes:
286
296
  doesn't: the client uses `generation_started`'s). Unknown provider fields never cross; a field is added here
287
297
  when a client needs it, never as a catch-all bag. Details: [internals/context-telemetry.md](context-telemetry.md).
288
298
  - `POST /session/:id/turn` — fire-and-forget turn creation; returns `202` with an `enqueued_id`
289
- (delivery is at-least-once via the worker's file-IPC input path — it never calls `run_turn`
290
- across the HTTP boundary). Only the bridge's own session: another id, in the path or the body's
291
- `session_id`, is `404 unknown_session`. Inspect results through the read surface, not the turn response.
299
+ (body: `{session_id, prompt, enqueued_id?}`; the optional id is a client-made UUID, generated by
300
+ the Bridge when absent or invalid). `BridgeClient#post_turn` supplies one when its caller does not,
301
+ including the attached TUI. The turn still goes through the worker's file-IPC input path — the
302
+ Bridge never calls `run_turn` across the HTTP boundary. Repeating an id already accepted by this
303
+ worker with the same prompt returns the original `202` acknowledgement, including its delivery and
304
+ cut outcome, and does not enqueue another turn; a different prompt with that id is refused with
305
+ `409 enqueued_id_conflict`. An input file with an already accepted id is consumed without running.
306
+ The worker keeps a bounded recent history of accepted ids.
307
+ For N-1 compatibility, an older worker ignores the optional field and generates its own id; an older
308
+ client sends none, so this Bridge generates one. Only the bridge's own session: another id, in the
309
+ path or the body's `session_id`, is `404 unknown_session`. Inspect results through the read surface,
310
+ not the turn response.
292
311
  An optional `deadline` (epoch seconds; `BridgeClient` sends 5/6 of its read timeout ahead) makes
293
312
  a request read after it (a worker frozen by sleep or SIGSTOP) answer `408 deadline_passed` and
294
313
  not run: a client that timed out has said the message was not sent. `/answer`,
@@ -300,7 +319,10 @@ The Bridge exposes:
300
319
  timeout is `504 worker_timeout`; a Bridge 404 for a route an older worker lacks is
301
320
  `501 not_supported` with the restart hint; a Bridge `404 unknown_session`, no bridge or a
302
321
  refused connection is `503 not_live`.
303
- - `POST /session/:id/cancel` — cancel the running turn; `202`, or `409` when none runs.
322
+ - `POST /session/:id/cancel` — cancel the running turn; `202`, or `409 not_running` when none runs. The optional
323
+ `turn_id` names the turn the Stop means (the web page and the TUI send the one they show): `TurnState#cancel!` compares
324
+ it to the running turn's id under its lock, and another turn running is `409 turn_mismatch`, nothing cancelled, so a
325
+ Stop pressed as a turn ends can't cancel the queued next one. No `turn_id` (an older client) cancels whichever runs.
304
326
  - `POST /session/:id/recap` — `/recap` in an attached TUI: `200 {enabled, saved, request,
305
327
  min_user_turns}` (`{enabled: false, session_off}` without recaps; `session_off` when the session's own
306
328
  `/recap off` turned it off); a new recap is asked for at once and
@@ -31,7 +31,7 @@ user-facing side (what a bundle owns, profiles, needs) is in [memory.md](../memo
31
31
 
32
32
  | Piece | Role |
33
33
  |---|---|
34
- | `BundleCommand` (`lib/samagotchi/bundle_command.rb`) | `chi bundle install/upgrade/uninstall/status/diff/list/build/trash/new/test/check/try` (`install --dev`: the Installer's `dev:`, links and `Provenance::DEV_SHA`); runs the Installer, prints its summary, runs the conflict agent step; refuses a source already installed under another name (`installed_under_another_name?`: a git URL bundle recorded under its repo's name before the manifest named it). `upgrade` resolves its argument by `upgrade_source`: a git URL or an existing path, then a shipped name, then the bare name of an installed bundle — whose record's source it then uses (a git URL with its `#ref`, a dir or an archive that is still there), printed as `Upgrading NAME from SOURCE`; a dev record is refused with `Installer.dev_install_line`, a recorded source that is relative, empty or gone says so and asks for the path or URL of a copy (`Installer.source_gone_line`, the words `ManifestStanding` uses too) |
34
+ | `BundleCommand` (`lib/samagotchi/bundle_command.rb`) | `chi bundle install/upgrade/uninstall/status/diff/list/build/trash/new/test/check/try` (`install --dev`: the Installer's `dev:`, links and `Provenance::DEV_SHA`); runs the Installer, prints its summary, runs the conflict agent step; refuses a source already installed under another name (`installed_under_another_name?`: a git URL bundle recorded under its repo's name before the manifest named it). `upgrade` resolves its argument by `upgrade_source`: a git URL or an existing path, then a shipped name, then the bare name of an installed bundle — whose record's source it then uses (a git URL with its `#ref`, a dir or an archive that is still there), printed as `Upgrading NAME from SOURCE`; a dev record is refused with `Installer.dev_install_line`, a recorded source that is relative, empty or gone says so and asks for the path or URL of a copy (`Installer.source_gone_line`, the words `ManifestStanding` uses too). After an upgrade or uninstall that changed the bundle's plugin (or brought one in), one line names the sessions still running its old code (`LiveVersions.workers`, the ones whose worker takes a restart) and `chi sessions restart ID` for each: only the plugin stays in memory, since a bundle's rules reload by their stamp and its scripts run from the replaced files. |
35
35
  | `BundleCommand::Check` (bundle_command/check.rb) | `chi bundle check [PATH] [--fix] [--set K=V]`: a bundle being written, checked as an install takes it (docs/cli.md lists the codes); parses the manifest from `Shas.parseable` (a placeholder sha is a `sha` finding, not a YAML error), reads the user's installed bundles only for `clash`, and installs and loads through `Testing::Install` in a `Testing::Sandbox` with `force: true`, the log's echoes swallowed |
36
36
  | `BundleCommand::Try` (`bundle_command/try/`) | `chi bundle try PATH -p TEXT`: `Command` (its flags, help and checks) drives the rest and `Render` prints the trace; a filtered-config `Sandbox` (its folders a `SandboxLayout`, the layout `Testing::Sandbox` uses too) installs the tried bundle and shipped `guardrails` / `cost-cap` in a separate child, then `Run` starts one non-interactive chi process from its realpath work dir; `TraceReader` reads that session's analytics, cards (`Bridge::CardStore.saved`, as the web does) and session JSON by id (never `Session.list`), joining timing and tool args for text or experimental JSON output (a turn a `--timeout` interrupt cut keeps its calls like a Ctrl-C, the one cut short answered as interrupted, so its rows come from the saved calls too; `Run::Result#timed_out` becomes `timed_out`). `Sandbox#child_env` is a Hash, never an ENV mutation; it keeps ordinary user model settings and Keychain access while dropping the spawner's session/guardrail/worker markers (`ChildEnv`, the scrub a session's worker spawn uses too). Every dropped key is sent as nil, so the child really lacks it: a spawn env is merged onto the spawner's, so a key only deleted from the Hash is still inherited (`ChildEnv.spawn_env`). The child runs in its own process group: a `--timeout` sends it INT, then `ProcessGroup.stop` (the groups its processes started too); after any exit the group is signalled only while a straggler is still in it, never a reaped group. `Sandbox#stop_leftovers` (before the trace, and in `cleanup`) stops what left the child's tree, found by the sandbox's own records: a delegate's worker by its session's `owner.lock`, a task by its running `task.json` whose pid still leads its group. |
37
37
  | `BundleCommand::ApiCalls` (bundle_command/api_calls.rb) | what a plugin's source calls of the API, by Prism: `#findings` (a call or keyword the API's classes lack, `check`'s `api`), `#keywords` (the floor check's `call(keyword:)`). Its only list is `MEMBERS`, which object a member gives (`ctx.sessions` → `Plugin::Sessions`); a new such object joins it |
@@ -131,8 +131,12 @@ has the bundle's own manifest at the recorded version (`Provenance#adopt_manifes
131
131
 
132
132
  ## Installer#run, in order
133
133
 
134
- The order matters: warnings print in it, and a plugin/script the manifest names but the bundle lacks raises
135
- `InstallError` after the memories are already copied.
134
+ The order matters: warnings print in it. Before the first write, `AssetInstaller#check_named_files` raises `InstallError`
135
+ for a plugin or script the manifest names but the bundle lacks (or ships as a link): nothing is written then. With
136
+ `refuse_mismatch: true` (only `BundleCommand#installer_for` passes it), `Installer#refuse_memory_mismatches` and
137
+ `AssetInstaller#check_named_checksums` raise the same way for a memory, plugin or script whose bytes disagree with the
138
+ sha its manifest declares -- `--force` skips the check, a dev install follows its own tree, and a non-strict run has no
139
+ shas to trust; `chi update`, `SystemBundle` and a profile pass nothing and keep the post-write warning.
136
140
 
137
141
  1. `load_source`: normalize (or take the `PreparedSource` `BundleCommand` passed as `prepared:`), read `manifest.yml` (strict: a missing/invalid one raises; non-strict: a warning). Then `refuse_too_new`: a bundle whose `requires_chi` this chi doesn't meet raises `InstallError` unless `force` — nothing is written, dry run included (docs/plugins.md, "Loading, and when it fails").
138
142
  2. `resolve_target`: scope = CLI > manifest > `system`; then `check_source` against the existing record
@@ -155,7 +159,8 @@ The order matters: warnings print in it, and a plugin/script the manifest names
155
159
  rules. A file that is a symlink or resolves outside the bundle dir (`AssetInstaller#linked?`) is skipped with a
156
160
  warning (a rule) or raises `InstallError` (the plugin, a script).
157
161
  5. An upgrade's `prune_dropped_files`: each dropped file's `FileState` (`shipped: false`) and its `UPGRADE` / `FORCE` row: unchanged (`#edited?` false) → trash; edited → kept (`kept_pruned`); owned by another bundle → kept.
158
- 6. `verify_memory_checksums` (strict except a dev install, whose own tree may have stale shas; `chi bundle check` flags them: a memory whose action takes the bundle's text, `Action#takes_source?`: installed, forced, an upgrade's or a re-install's fast-forward, or merged into a local edit; it checks the source file the write or merge takes its bytes from, so a dry run checks what it would bring in; a `Remerge` merge checks its incoming file too), `warn_missing_needs`.
162
+ 6. `verify_memory_checksums` (strict except a dev install, whose own tree may have stale shas; the warning only, since a
163
+ run that refuses a mismatch (`refuse_mismatch: true`) never reaches it with one unless `--force`; `chi bundle check` flags them: a memory whose action takes the bundle's text, `Action#takes_source?`: installed, forced, an upgrade's or a re-install's fast-forward, or merged into a local edit; it checks the source file the write or merge takes its bytes from, so a dry run checks what it would bring in; a `Remerge` merge checks its incoming file too), `warn_missing_needs`.
159
164
  7. `write_provenance` (manifest, not a dry run): each owned file's base comes from its action's `next_base`
160
165
  (`Installer#next_base`): the file on disk, which has the bundle's bytes; the incoming file for a merge, since the merged
161
166
  file holds the user's edit and taking it would make current == base, so the next upgrade would fast-forward over the
@@ -171,7 +176,21 @@ The order matters: warnings print in it, and a plugin/script the manifest names
171
176
  A file system error in steps 3 to 5 (`Installer#replace_files`; a `.trash` it can't write, an edited file it can't read) is an
172
177
  `InstallError` in `Trash.failure_message`'s words, as it is an `UninstallError` in `Uninstaller#run`. Each save comes before
173
178
  its removal, so no edit is lost, and the record isn't written, so a retry is the same upgrade. Not atomic: files the
174
- failed run already placed stay.
179
+ failed run already placed stay, and the intent journal says they are the bundle's.
180
+
181
+ **The intent journal.** After `check_named_files` and before the first write, a real run with a manifest writes
182
+ `.bundles/<name>/pending.json` (`Provenance#write_pending`, atomically like the record): `{"version": "2.0.0",
183
+ "scope": "project", "target_dir": "/path/to/project-memories", "files": {"a.md": ["<sha256>"]}}`: the target
184
+ `version` is the latest run's target version, while `files` holds the union of incoming shas recorded by interrupted
185
+ runs for that scope/target directory. A retry of another version in the same target adds its incoming shas; a journal
186
+ for another scope directory is ignored. `Provenance#write` removes it with the record write; an uninstall's `rm_rf`
187
+ sweeps it. `FileState.of` reads it:
188
+ a file whose disk sha is one of its shas is that bundle's text at the pending version, owned and its own base (an entry
189
+ made up when the record has none), so a killed fresh install's copy isn't `:user_same` (kept as the user's for good) and
190
+ a killed upgrade's fast-forward isn't `:edited` (a conflict at the next version). Install, upgrade, uninstall and
191
+ `chi bundle status` all go through it. A file matching none of its shas follows the usual rules; a merged file isn't
192
+ journaled (its bytes are no bundle text). Not covered: a journal left by a run that failed before writing a file whose
193
+ text the user already had is read the same way.
175
194
 
176
195
  Which files a plan asks `Merger::Upgrade` about is the caller's, and the lists differ, each less the files the bundle
177
196
  doesn't own: the Installer's `MemoryFiles.select`; `ShippedUpdate.forecast`' every shipped `*.md`, so `chi update`'s plan
@@ -538,7 +538,13 @@ Where a participant's author (`"p:<id>"`; `Steer.participant_author?`) goes (the
538
538
  - **`Session#last_turn.author`** and the analytics record's `author` (`SessionMetrics`); a Continue keeps the offer
539
539
  turn's author (`ContinueOffer#run_continue_turn`).
540
540
  - **Never a command**: a participant's `/word` or `!line` is refused (`Bridge#handle_post_turn` 403,
541
- `WorkerInbound#queue_as_command` drops it).
541
+ `WorkerInbound#queue_as_command` drops it). The same two intakes ask `CommandIntake.allowed?` for every other
542
+ sender: only a person's (`ClientId.human?`, not `review:`) and `cli:answer`'s line is a command; from a delegate, a
543
+ schedule, a plugin or a wake a `/word` line is a prompt and a `!` line is refused (the Bridge's 403
544
+ `command_not_permitted`; an input file's or a first prompt's is queued and `Worker#run_command_line` answers it with
545
+ the error `command_ran`). A session's first prompt carries its sender too: `WorkerInbound#initial_origin` reads
546
+ `Session#first_turn_client_id` (a delegate child without it: `ClientId.delegate` of its parent) and `first_turn_id`,
547
+ for `#initial_command?` and `Worker#run_initial_prompt`.
542
548
  - **Delivery**: a participant's input always waits for a turn of its own (`SessionInbox.waits_for_turn_end?`), so
543
549
  the drain never merges it; their turns count against chi web's `pending_limit` (429 `too_many_turns`).
544
550
  `Worker#run_prompt` checks their link and cap (`participant_refusal`) before the turn and after it.
@@ -61,7 +61,10 @@ the payload, so an edit and a continue carry none: a new link starts with no las
61
61
  ## Editing
62
62
 
63
63
  `/objectives` (`ObjectivesCommand`, registered `anytime`) reads `Objectives.current`. An edit goes out through
64
- `NoteDelivery.deliver(objectives:)`, which means:
64
+ `NoteDelivery.deliver(objectives:, pin: true)`, which means:
65
+
66
+ - It stays in its own session, even one that was continued: the list is built from this conversation, so it never
67
+ follows the chain to the newest link (as other notes do) and overwrites that link's list.
65
68
 
66
69
  - One edit's read and write are one step (`Objectives.with_lock`, a file lock in the session's folder, across threads
67
70
  and processes), and the worker's absorb holds it from its claim to the files' delete, so no edit reads between them.
@@ -176,7 +179,10 @@ command), which absorbs the file. It no longer waits for the fallback tick.
176
179
  `SessionChain.note_text` (capped; the last answer gets what the recap leaves). It reads the previous link fresh,
177
180
  with its unabsorbed newest file. A failure is only logged (`continue_objectives_failed`), because it runs inside the
178
181
  continue's rollback. A review link's list stays behind, as its last answer does: the next link's participants could
179
- differ.
182
+ differ. The previous link's other waiting notes move after it (`NoteMove.move`, the step after: moved first, the
183
+ carry would miss a pending edit, and the stale list would be the batch's newest); its objectives notes never move,
184
+ and the new link's worker never adopts one from it (`NoteMove.adoption`), so a list from the archived link can't
185
+ overwrite the carried one.
180
186
  - **Rollback**: a note survives `TurnFlow#restore`, and an unabsorbed edit is on disk, untouched.
181
187
  - **Fork, delegate**: a plugin fork copies what it is passed. A delegate starts with an empty fold, so the tool answers
182
188
  that there are none.