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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +125 -1
- data/docs/architecture.md +12 -9
- data/docs/cli.md +7 -5
- data/docs/context.md +10 -1
- data/docs/guardrails.md +33 -5
- data/docs/internals/attached-context.md +4 -2
- data/docs/internals/bridge.md +28 -6
- data/docs/internals/bundles.md +24 -5
- data/docs/internals/engine.md +7 -1
- data/docs/internals/objectives.md +8 -2
- data/docs/internals/remote-sessions.md +2 -1
- data/docs/memory.md +43 -13
- data/docs/plugins.md +13 -4
- data/docs/releasing.md +7 -4
- data/docs/scheduling.md +53 -8
- data/docs/sessions.md +17 -12
- data/docs/testing.md +9 -1
- data/lib/samagotchi/bridge/card_store.rb +31 -3
- data/lib/samagotchi/bridge/ring_buffer.rb +11 -1
- data/lib/samagotchi/bridge/sse_writer.rb +8 -5
- data/lib/samagotchi/bridge.rb +103 -44
- data/lib/samagotchi/bridge_client.rb +9 -6
- data/lib/samagotchi/broadcast/scope_card.rb +2 -1
- data/lib/samagotchi/broadcast_command.rb +24 -8
- data/lib/samagotchi/bundle_command.rb +57 -6
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/memory_guide.md +2 -2
- data/lib/samagotchi/child_reports.rb +29 -23
- data/lib/samagotchi/client_id.rb +6 -0
- data/lib/samagotchi/command_intake.rb +56 -0
- data/lib/samagotchi/compat.rb +32 -0
- data/lib/samagotchi/context_absorber.rb +6 -3
- data/lib/samagotchi/context_candidate_choice.rb +8 -5
- data/lib/samagotchi/context_candidates.rb +11 -8
- data/lib/samagotchi/context_candidates_log.rb +10 -1
- data/lib/samagotchi/context_command.rb +10 -4
- data/lib/samagotchi/context_move.rb +6 -1
- data/lib/samagotchi/context_poller.rb +5 -2
- data/lib/samagotchi/context_sources.rb +6 -4
- data/lib/samagotchi/engine/events.rb +25 -2
- data/lib/samagotchi/engine/guardrails.rb +3 -4
- data/lib/samagotchi/engine/plugins.rb +1 -1
- data/lib/samagotchi/engine/steering.rb +2 -2
- data/lib/samagotchi/engine.rb +1 -0
- data/lib/samagotchi/engine_peers.rb +1 -0
- data/lib/samagotchi/events.rb +71 -0
- data/lib/samagotchi/guardrails/approvals.rb +27 -3
- data/lib/samagotchi/guardrails/child_boundary.rb +37 -4
- data/lib/samagotchi/guardrails/protected_paths.rb +8 -5
- data/lib/samagotchi/guardrails/targets.rb +25 -6
- data/lib/samagotchi/hook_routes.rb +0 -1
- data/lib/samagotchi/live_versions.rb +7 -2
- data/lib/samagotchi/memory_bundle/asset_installer.rb +86 -18
- data/lib/samagotchi/memory_bundle/bundle_hook.rb +7 -27
- data/lib/samagotchi/memory_bundle/file_actions.rb +3 -4
- data/lib/samagotchi/memory_bundle/file_state.rb +11 -0
- data/lib/samagotchi/memory_bundle/index_updater.rb +7 -4
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +1 -1
- data/lib/samagotchi/memory_bundle/installer.rb +93 -23
- data/lib/samagotchi/memory_bundle/manifest.rb +6 -0
- data/lib/samagotchi/memory_bundle/provenance.rb +158 -6
- data/lib/samagotchi/memory_bundle/shipped_update.rb +7 -4
- data/lib/samagotchi/memory_bundle/status.rb +3 -3
- data/lib/samagotchi/memory_bundle/system_bundle.rb +2 -0
- data/lib/samagotchi/memory_bundle/uninstaller.rb +29 -13
- data/lib/samagotchi/memory_orphans.rb +54 -13
- data/lib/samagotchi/memory_paths.rb +29 -6
- data/lib/samagotchi/memory_usage.rb +23 -12
- data/lib/samagotchi/note_command.rb +9 -1
- data/lib/samagotchi/note_delivery.rb +34 -24
- data/lib/samagotchi/note_move.rb +205 -0
- data/lib/samagotchi/objectives_command.rb +3 -1
- data/lib/samagotchi/owner_lock.rb +7 -1
- data/lib/samagotchi/plugin/attached_context.rb +3 -1
- data/lib/samagotchi/plugin/sessions.rb +4 -1
- data/lib/samagotchi/project_command.rb +58 -20
- data/lib/samagotchi/project_id.rb +102 -0
- data/lib/samagotchi/project_id_clash.rb +62 -0
- data/lib/samagotchi/project_init.rb +214 -10
- data/lib/samagotchi/project_scope.rb +11 -2
- data/lib/samagotchi/review_start.rb +5 -3
- data/lib/samagotchi/schedule_command.rb +12 -0
- data/lib/samagotchi/schedules.rb +374 -76
- data/lib/samagotchi/send_command.rb +2 -15
- data/lib/samagotchi/session.rb +59 -7
- data/lib/samagotchi/session_access/local.rb +53 -22
- data/lib/samagotchi/session_access/remote.rb +13 -5
- data/lib/samagotchi/session_access/web_bridge_client.rb +2 -2
- data/lib/samagotchi/session_access.rb +20 -4
- data/lib/samagotchi/session_commands.rb +7 -3
- data/lib/samagotchi/session_continue.rb +162 -24
- data/lib/samagotchi/session_delete_command.rb +3 -1
- data/lib/samagotchi/session_inbox.rb +41 -1
- data/lib/samagotchi/session_inbox_delivery.rb +254 -0
- data/lib/samagotchi/session_manager/delivery.rb +66 -12
- data/lib/samagotchi/session_manager/emptiness.rb +7 -2
- data/lib/samagotchi/session_manager/lifecycle.rb +2 -1
- data/lib/samagotchi/session_manager/listing.rb +4 -1
- data/lib/samagotchi/session_manager/removal.rb +130 -26
- data/lib/samagotchi/session_manager/spawn.rb +167 -100
- data/lib/samagotchi/session_manager/worker_command.rb +7 -19
- data/lib/samagotchi/session_manager.rb +1 -0
- data/lib/samagotchi/session_retention.rb +53 -23
- data/lib/samagotchi/session_setup.rb +27 -7
- data/lib/samagotchi/starting_marker.rb +93 -24
- data/lib/samagotchi/state_format.rb +129 -0
- data/lib/samagotchi/system_one/runs.rb +6 -1
- data/lib/samagotchi/system_one/runs_store.rb +18 -14
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +7 -4
- data/lib/samagotchi/terminal_ui/attached_loop/composer.rb +5 -2
- data/lib/samagotchi/terminal_ui/attached_loop.rb +5 -0
- data/lib/samagotchi/terminal_ui/version_lines.rb +4 -2
- data/lib/samagotchi/terminal_ui.rb +2 -3
- data/lib/samagotchi/tools/context_read.rb +7 -2
- data/lib/samagotchi/tools/delegate.rb +19 -6
- data/lib/samagotchi/tools/schedule_task.rb +5 -3
- data/lib/samagotchi/tools/send_note.rb +10 -4
- data/lib/samagotchi/turn_stages/context_judge.rb +7 -7
- data/lib/samagotchi/turn_state.rb +108 -4
- data/lib/samagotchi/update_command.rb +9 -4
- data/lib/samagotchi/verify_fresh.rb +27 -9
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +6 -5
- data/lib/samagotchi/web/public/app.js +2 -7
- data/lib/samagotchi/web/public/chains.js +4 -4
- data/lib/samagotchi/web/public/data.js +18 -2
- data/lib/samagotchi/web/public/info_bar_model.js +1 -1
- data/lib/samagotchi/web/public/session_fields.js +3 -2
- data/lib/samagotchi/web/public/session_state.js +2 -0
- data/lib/samagotchi/web/public/update_notice.js +2 -2
- data/lib/samagotchi/web/session_access_routes.rb +15 -11
- data/lib/samagotchi/web/session_control.rb +9 -3
- data/lib/samagotchi/web/session_create.rb +8 -4
- data/lib/samagotchi/web/session_hub.rb +50 -5
- data/lib/samagotchi/web/session_summary.rb +5 -2
- data/lib/samagotchi/worker.rb +61 -11
- data/lib/samagotchi/worker_boot.rb +22 -11
- data/lib/samagotchi/worker_inbound.rb +62 -10
- data/lib/samagotchi/worker_link.rb +4 -1
- data/lib/samagotchi/worker_presence.rb +106 -0
- data/lib/samagotchi/worker_sidecar.rb +3 -4
- metadata +9 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d3a8c4d1d4d37965fda091bc5c48281cdf352d6f3e496d5e5073f72777fe5994
|
|
4
|
+
data.tar.gz: 74a26cfde1a9028c44763b99cdc60ce044028598ddd2955b244338076ef8fd86
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
|
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
|
|
166
|
-
|
|
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
|
-
- **
|
|
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
|
|
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` (
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
-
|
|
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
|
|
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:)`
|
|
55
|
-
session sees: its own, then the project's it doesn't shadow.
|
|
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
|
data/docs/internals/bridge.md
CHANGED
|
@@ -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
|
|
199
|
-
the
|
|
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
|
-
(
|
|
290
|
-
|
|
291
|
-
|
|
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
|
data/docs/internals/bundles.md
CHANGED
|
@@ -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
|
|
135
|
-
|
|
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;
|
|
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
|
data/docs/internals/engine.md
CHANGED
|
@@ -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.
|