samagotchi 0.158.0 → 0.160.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 +60 -1
- data/bin/chi +3 -1
- data/docs/architecture.md +7 -7
- data/docs/cli.md +57 -2
- data/docs/configuration.md +9 -0
- data/docs/context.md +7 -1
- data/docs/guardrails.md +24 -3
- data/docs/hooks.md +3 -0
- data/docs/internals/attached-context.md +4 -2
- data/docs/internals/bridge.md +8 -0
- data/docs/internals/bundles.md +9 -7
- data/docs/internals/engine.md +7 -1
- data/docs/memory.md +46 -14
- data/docs/scheduling.md +241 -0
- data/docs/sessions.md +28 -2
- data/lib/samagotchi/bridge.rb +37 -4
- data/lib/samagotchi/broadcast/scope_card.rb +2 -1
- data/lib/samagotchi/bundle_command.rb +39 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/memory_guide.md +2 -2
- data/lib/samagotchi/client_id.rb +17 -3
- data/lib/samagotchi/command_intake.rb +56 -0
- data/lib/samagotchi/config.rb +12 -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_command.rb +9 -3
- data/lib/samagotchi/context_move.rb +74 -0
- data/lib/samagotchi/context_poller.rb +5 -2
- data/lib/samagotchi/context_sources.rb +6 -4
- data/lib/samagotchi/engine/events.rb +13 -2
- data/lib/samagotchi/engine/guardrails.rb +3 -4
- 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/pair_boundary.rb +1 -1
- data/lib/samagotchi/hook_routes.rb +0 -1
- data/lib/samagotchi/memory_bundle/asset_installer.rb +106 -104
- data/lib/samagotchi/memory_bundle/asset_state.rb +81 -0
- data/lib/samagotchi/memory_bundle/bundle_hook.rb +7 -27
- data/lib/samagotchi/memory_bundle/file_actions.rb +50 -0
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +1 -1
- data/lib/samagotchi/memory_bundle/installer.rb +14 -11
- data/lib/samagotchi/memory_bundle/manifest.rb +6 -0
- data/lib/samagotchi/memory_bundle/shipped_update.rb +22 -15
- data/lib/samagotchi/memory_bundle/status.rb +22 -3
- data/lib/samagotchi/memory_bundle/uninstaller.rb +12 -4
- data/lib/samagotchi/memory_orphans.rb +14 -4
- data/lib/samagotchi/memory_paths.rb +29 -6
- data/lib/samagotchi/memory_usage.rb +23 -12
- 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/run_options.rb +1 -0
- data/lib/samagotchi/schedule_command.rb +252 -0
- data/lib/samagotchi/schedules.rb +1008 -0
- data/lib/samagotchi/session.rb +17 -2
- data/lib/samagotchi/session_access/contract.rb +1 -1
- data/lib/samagotchi/session_access/local.rb +28 -5
- data/lib/samagotchi/session_access/remote.rb +2 -1
- data/lib/samagotchi/session_access.rb +4 -1
- data/lib/samagotchi/session_commands.rb +7 -3
- data/lib/samagotchi/session_continue.rb +10 -6
- data/lib/samagotchi/session_manager/spawn.rb +34 -13
- data/lib/samagotchi/session_manager/worker_command.rb +11 -23
- data/lib/samagotchi/state_format.rb +118 -0
- data/lib/samagotchi/terminal_ui/formatting.rb +2 -0
- data/lib/samagotchi/tool_declarations.rb +43 -0
- data/lib/samagotchi/tools/builtin_calls.rb +2 -0
- data/lib/samagotchi/tools/builtins.rb +17 -0
- data/lib/samagotchi/tools/cancel_schedule.rb +43 -0
- data/lib/samagotchi/tools/context_read.rb +7 -2
- data/lib/samagotchi/tools/delegate.rb +10 -2
- data/lib/samagotchi/tools/list_schedules.rb +43 -0
- data/lib/samagotchi/tools/schedule_task.rb +39 -0
- data/lib/samagotchi/turn_stages/context_judge.rb +7 -7
- data/lib/samagotchi/update_command.rb +2 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +5 -4
- data/lib/samagotchi/web/public/chains.js +32 -8
- data/lib/samagotchi/web/public/index.html +4 -1
- data/lib/samagotchi/web/public/info_bar.js +4 -3
- data/lib/samagotchi/web/public/turn_events.js +4 -0
- data/lib/samagotchi/web/session_access_routes.rb +3 -3
- data/lib/samagotchi/web/session_hub.rb +20 -0
- data/lib/samagotchi/worker.rb +42 -12
- data/lib/samagotchi/worker_boot.rb +3 -4
- data/lib/samagotchi/worker_inbound.rb +44 -2
- data/lib/samagotchi/worker_link.rb +5 -0
- data/lib/samagotchi/worker_wakes.rb +9 -1
- metadata +12 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9f4ca470372c140d0bcdc30583312ee1139b05b878a8e0f4688f9401a7af3700
|
|
4
|
+
data.tar.gz: dfd3b0bf49a29758ea645bdf4ac6614c9a93610f013e7517d75d8eefe4b3e3ab
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 18bd973f0dfffd58845df4341e41a2f3d32292a4d158e866cf76d907b64d9aac4539bacfc0a9cbc4a81a05b5a3bcc81cf8fec8413f52deb0a95c7141a6235082
|
|
7
|
+
data.tar.gz: 2c2aaf26b58566064737487ae20dfc7037a752f20e426c80375ca0068e14f31e85ac2bc5927522c500dd4622c17f7696e0e93aadf3697cb79cf6b7d08e00669d
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,63 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.160.0] - 2026-11-09
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- A folder made a chi project with `chi project init` keeps its memories when you move or rename it (an `id:` in
|
|
16
|
+
.chi/project.yml); rerun `chi project init` in an older project to adopt one, `--new-id` to split a copied folder.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- `schedules.json` keeps what a newer chi wrote (its format version, unknown fields, entries this chi can't read) when
|
|
21
|
+
an older chi rewrites it.
|
|
22
|
+
- The `SessionManager.run_session_loop` forwarder for older boot strings is gone: a chi web or terminal still
|
|
23
|
+
running 0.158.0 or older must be restarted after updating, because a worker it spawns names that entry.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- A message that starts with `!` runs as a shell command only when a person sends it (a web tab, a chi TUI, `chi send`):
|
|
28
|
+
from a delegating parent, a schedule or a plugin it is refused and nothing runs, and a `/command` line from them is
|
|
29
|
+
plain text for the model. `chi answer`'s lines (a parent agent's `/continue yes`, `/parent-answers`) still run as
|
|
30
|
+
commands, with the same trust as `chi send`.
|
|
31
|
+
- A delegate's task and a chain schedule's firing now start their session as their sender's turn, not yours: its
|
|
32
|
+
`last_turn` origin reads `delegate` or `schedule`, the turn counts as automatic in the session's metrics, and the
|
|
33
|
+
objectives audit skips it as it skips a delegate's follow-up; it no longer counts as your input either (it doesn't
|
|
34
|
+
unarchive the session or reset its wakes). Restart `chi web` (and any worker) after updating: one
|
|
35
|
+
still running the older chi starts chain links whose first message runs as yours.
|
|
36
|
+
- `delegate`, `schedule_task` and `chi schedule add` refuse a task or prompt that starts with `!`, saying why, so the
|
|
37
|
+
model can write it as an instruction. A schedule stored with one before this fails when it fires (counted toward
|
|
38
|
+
its `max_failures`; nothing is started or delivered).
|
|
39
|
+
- A command you allow "for this session" is allowed again only in the same worktree or folder, with the same `env:`
|
|
40
|
+
(kept as a digest); the same text run in another checkout asks again. Session approvals stored before this ask one
|
|
41
|
+
more time.
|
|
42
|
+
- `chi bundle upgrade` and `chi update` report a replaced guardrail rule as Updated, like other bundle assets.
|
|
43
|
+
- Bundle install, upgrade and dry runs now name the guardrail, plugin and script files a new version drops.
|
|
44
|
+
- `chi bundle upgrade --dry-run` names edited files it would copy to the trash, including marked memories.
|
|
45
|
+
- `chi bundle uninstall` names a plugin it removes even when the bundle has no memories or hooks.
|
|
46
|
+
- `chi bundle status` labels retired hooks as present or missing; they are not loaded.
|
|
47
|
+
- `chi update` plan rows name memory files a shipped bundle adds.
|
|
48
|
+
|
|
49
|
+
## [0.159.0] - 2026-11-09
|
|
50
|
+
|
|
51
|
+
Update with `chi update`.
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
|
|
55
|
+
- `chi schedule <add|list|show|tick|run-now|enable|disable|cancel>`: durable schedules that outlive every worker. Each entry is a prompt with a clock in `~/.local/state/samagotchi/schedules.json`, fired by a tick run by cron, systemd or (with `schedule.auto_tick: true`) chi web's hub, and each firing is either the next link of the session's chain (the default, with `--keep-links` pruning the older links and their delegates) or a turn queued in the session itself (`--mode same`). Recurrence is `--every 45m`, `--weekday 1 --time 09:00` or `--date 2026-11-20T09:00`; a busy target is skipped and retried at the next tick, and 12 consecutive skips or 3 consecutive failures disable it (any success resets both). The model registers its own with `schedule_task`, `list_schedules` and `cancel_schedule`. `register_reminder` is unchanged: a reminder lives in a live worker and holds it up; a schedule wakes one. See docs/scheduling.md.
|
|
56
|
+
|
|
57
|
+
### Changed
|
|
58
|
+
|
|
59
|
+
- `chi bundle status NAME` lists the bundle's guardrail rules and scripts, and says which you edited (an edited one is
|
|
60
|
+
replaced by the next install, its copy kept in the trash).
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
|
|
64
|
+
- A peer's wake push now starts a session that went idle (never one you stopped).
|
|
65
|
+
- Continuing a session carries a peer's pushed text the old session hadn't absorbed or read yet, as a separate
|
|
66
|
+
third-party context note.
|
|
67
|
+
|
|
11
68
|
## [0.158.0] - 2026-11-08
|
|
12
69
|
|
|
13
70
|
### Added
|
|
@@ -5865,7 +5922,9 @@ and long-lived sessions.
|
|
|
5865
5922
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
5866
5923
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
5867
5924
|
|
|
5868
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
5925
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.160.0...HEAD
|
|
5926
|
+
[0.160.0]: https://github.com/dm1try/samagotchi/compare/v0.159.0...v0.160.0
|
|
5927
|
+
[0.159.0]: https://github.com/dm1try/samagotchi/compare/v0.158.0...v0.159.0
|
|
5869
5928
|
[0.158.0]: https://github.com/dm1try/samagotchi/compare/v0.157.0...v0.158.0
|
|
5870
5929
|
[0.157.0]: https://github.com/dm1try/samagotchi/compare/v0.156.0...v0.157.0
|
|
5871
5930
|
[0.156.0]: https://github.com/dm1try/samagotchi/compare/v0.155.0...v0.156.0
|
data/bin/chi
CHANGED
|
@@ -104,7 +104,9 @@ SUBCOMMANDS = {
|
|
|
104
104
|
# the chi home: the Ruby every chi launch boots (set, doctor)
|
|
105
105
|
"home" => ["samagotchi/home_command", "HomeCommand"],
|
|
106
106
|
# this folder's project; init marks a folder in no git repository (.chi/project.yml)
|
|
107
|
-
"project" => ["samagotchi/project_command", "ProjectCommand"]
|
|
107
|
+
"project" => ["samagotchi/project_command", "ProjectCommand"],
|
|
108
|
+
# durable schedules that outlive every worker (add, list, show, tick, run-now, …)
|
|
109
|
+
"schedule" => ["samagotchi/schedule_command", "ScheduleCommand"]
|
|
108
110
|
}.freeze
|
|
109
111
|
# chi --remote NAME <command> (or CHI_REMOTE=NAME): the session commands
|
|
110
112
|
# against another machine's chi web; what runs here only is refused (exit 2).
|
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
|
|
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 `
|
|
166
|
-
|
|
167
|
-
forwarder goes the release after that. An older chi that wakes a session doesn't read 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,7 +193,7 @@ 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.
|
|
196
|
+
- **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)).
|
|
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
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.
|
|
200
199
|
- **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`.
|
|
@@ -432,7 +431,7 @@ shared with `/api/sessions` and the session view; built on the session's `Sessio
|
|
|
432
431
|
reads, so the page, `chi sessions list` and `/children` agree on status, liveness and a waiting question) carries `owner`, `project_root` and
|
|
433
432
|
`bridge_up` (`WorkerLink` answers Live: a worker holds the lock and its sidecar names its pid; the page attaches its stream on it), and
|
|
434
433
|
from that sidecar `worker_version` (the chi the worker runs) and `worker_features`
|
|
435
|
-
(`Bridge::FEATURES`, what a client may ask of it beyond the routes: `restart`, `task_stop`, `reparent`, `unqueue`,
|
|
434
|
+
(`Bridge::FEATURES`, what a client may ask of it beyond the routes: `restart`, `task_stop`, `reparent`, `unqueue`, `activity` (`GET /activity`, the busy check a schedule tick asks a live worker),
|
|
436
435
|
`delivery`, a worker that queues a message for after the turn; `SessionManager.deliver_turn` and
|
|
437
436
|
the attached TUI refuse a queue to a worker without it, which would steer; `turn_ids`, a worker whose running
|
|
438
437
|
turn's mark, continue turn and Stop carry the turn's `enqueued_id`, which `chi answer`, `chi send --wait` with no
|
|
@@ -471,7 +470,7 @@ model: [internals/remote-sessions.md](internals/remote-sessions.md).
|
|
|
471
470
|
| `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
471
|
| `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
472
|
| `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 |
|
|
473
|
+
| `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
474
|
| `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
475
|
| `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
476
|
| `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,6 +654,7 @@ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/
|
|
|
655
654
|
| 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
655
|
| 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
656
|
| Stores | `reminder_store.rb` (`ReminderStore`), `recap_store.rb` (`RecapStore`) | A session's reminders, and its idle recap (`recap.json`). |
|
|
657
|
+
| 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). |
|
|
658
658
|
| 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)). |
|
|
659
659
|
| 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. |
|
|
660
660
|
| 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
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
- `chi context report [--days N]` — the candidate judge's counts from `context_candidates.jsonl`: candidates proposed, mentioned, attached or declined by you; the judge's asks; what it would attach per threshold, and how that agrees with what you did (this machine only; see [The log and the report](context.md#the-log-and-the-report))
|
|
25
25
|
- `chi send [-m TEXT] [--image PATH]... [--cut | --queue] (ID|PREFIX)...` — send a message to sessions as if typed there: a turn starts (or a running one takes it at its next step; `--cut` asks it to cut long thinking for it; `--queue` runs it as a turn of its own after the running one, and `--queue --wait` waits for that turn's own answer); piped stdin goes above `-m` as quoted context, and `--image` attaches images (see [Sessions: Sending a message](sessions.md#sending-a-message)); `--new` starts a session with it instead (`--new --continues ID|last:ID` the next link of that session's chain, see [Session chains](sessions.md#session-chains)), and `--wait` prints the answer (`--wait ID` with no message waits for the next reply without sending — to an idle session whose last turn already ended, that ended turn's reply or end comes back; `--wait --turn T ID` with no message waits for exactly turn T, whether running, queued or last ended; `--format json` includes its `turn_id` when known, for waiting again after exit 3 or 4; exit 3 a question waits, its options on stderr; exit 4 `--timeout` passed with the turn still running; see [Starting a session](sessions.md#starting-a-session))
|
|
26
26
|
- `chi answer ID --question QID (--option N|LABEL)... [--text T] [--timeout S] [--format json]` (or `--dismiss`) — answer the question a session waits on (the one `chi send --wait` exited 3 with), then wait and print what comes next as `chi send --wait` does: the reply (0), the next question (3), still running after `--timeout` (4). `--option` is 1-based or the label, repeated on a multi-select question; `--text` is free text, or a Deny's reason; `--dismiss` leaves it unanswered and the model finishes its reply. A question no longer open (the web answered first) isn't answered again: it waits for that turn's reply. An option the question doesn't offer exits 2; a worker that is gone exits 1 (`send the task again: chi send --wait -m "…" ID`). What a parent may pick follows `guardrails.parent_answers`, or the session's own `parent_answers` when stricter (`soft`, the default: a deny, a plain answer, Continue and "Allow once"; `standard`: no allow; `hard`: no Continue either), never a wider scope or an allow on chi's own config; a refused pick exits 1 with what to answer instead (a convention, not a security boundary; see [Guardrails](guardrails.md#answers-from-a-parent-agent))
|
|
27
|
+
- `chi schedule <add|list|show|tick|run-now|enable|disable|cancel>` — durable schedules on this machine: a prompt with a clock that fires out of no worker (cron, systemd, or chi web's hub with `schedule.auto_tick`), as the next link of a session's chain or as a turn in the session itself (see [Scheduling](scheduling.md) and [Durable schedules](#durable-schedules-chi-schedule)). A usage error exits 2, an unknown session or schedule 1; `chi --remote X schedule list` exits 2
|
|
27
28
|
- `chi remote add NAME URL [--token-file F | --token-stdin]`, `chi remote ls`, `chi remote rm NAME` — the other machines whose chi web the session commands reach with `--remote NAME` (see [Remote sessions](#remote-sessions))
|
|
28
29
|
- `chi review FILE… [--dir DIR] [--model M]`, `chi review --from ID [FILE…]` — start a review session to invite participants to: for files of a project, or as a review of a working session that starts from its recap, not its transcript (see [Review sessions](#review-sessions))
|
|
29
30
|
- `chi share ID --name NAME [--identity EMAIL] [--agent] [--expires 3d] [--cap 2]`, `chi share ls [ID]`, `chi share cap ID P DOLLARS`, `chi share revoke ID [P] | --all` — invite a participant to a session through chi web on the LAN (read-only review turns in a review session, pair turns that act under your guardrails in a working session) (a link of their own, 7 days, a $2 spending cap), list them, change one's cap, revoke them (see [Participants](#participants))
|
|
@@ -35,8 +36,8 @@
|
|
|
35
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)
|
|
36
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))
|
|
37
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))
|
|
38
|
-
- `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` (
|
|
39
|
-
- `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)). 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; 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)
|
|
40
41
|
|
|
41
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))
|
|
42
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
|
|
@@ -436,6 +437,60 @@ bundle). After a gem update, the first interactive start of the new version
|
|
|
436
437
|
(`chi`, `chi web`; not `-p` or `--non-interactive`) says in one line when
|
|
437
438
|
bundles or the helper can be updated.
|
|
438
439
|
|
|
440
|
+
### Durable schedules (`chi schedule`)
|
|
441
|
+
|
|
442
|
+
A schedule is a prompt with a clock, kept in
|
|
443
|
+
`~/.local/state/samagotchi/schedules.json` and fired by a **tick** that needs no
|
|
444
|
+
worker: cron, systemd, `chi schedule tick`, or chi web's hub when
|
|
445
|
+
`schedule.auto_tick: true`. How it works, what "busy" means and the DST caveat:
|
|
446
|
+
[Scheduling](scheduling.md). This machine only — `chi --remote studio schedule
|
|
447
|
+
list` exits 2.
|
|
448
|
+
|
|
449
|
+
```
|
|
450
|
+
# the daily coordinator, every Monday 09:00 local, each run a fresh link of the chain
|
|
451
|
+
chi schedule add --session 3f9a1c2e --weekday 1 --time 09:00 -m "Audit the open PRs" --title "Monday audit"
|
|
452
|
+
|
|
453
|
+
# every 30 minutes, as a turn in the session itself
|
|
454
|
+
chi schedule add --continues 3f9a1c2e --every 30m --mode same -m "Check the build"
|
|
455
|
+
|
|
456
|
+
chi schedule list # id mode recurrence next due target [skips 3 of 12 (≈1 h at a 5 min tick)]
|
|
457
|
+
chi schedule list --all --format json # the disabled ones and the fired one-offs too
|
|
458
|
+
chi schedule show 4b7e9a # the entry in full: anchor, head, prompt, counters
|
|
459
|
+
chi schedule tick # fire everything due; quiet when nothing is
|
|
460
|
+
chi schedule tick --dry-run # print what would fire, change nothing
|
|
461
|
+
chi schedule run-now 4b7e9a # fire it now, whatever its next_due says
|
|
462
|
+
chi schedule disable 4b7e9a # stop it, keep it in the file
|
|
463
|
+
chi schedule enable 4b7e9a
|
|
464
|
+
chi schedule cancel 4b7e9a # delete it (by id, a unique prefix, or its title)
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
`add` needs a target (`--session ID`, or `--continues ID` for the same session)
|
|
468
|
+
and exactly one recurrence kind — `--every 45m|2h|1d`, `--weekday 1..7` with
|
|
469
|
+
`--time HH:MM` (local wall clock), or `--date 2026-11-20T09:00` — plus `-m|--message|--prompt TEXT`.
|
|
470
|
+
`--title` names it, `--mode chain|same` picks the mode (chain by default),
|
|
471
|
+
`--catch-up once|none` what an overdue entry does, `--keep-links N` how many chain
|
|
472
|
+
links stay behind the head (`0` keeps all), and `--max-failures` / `--max-skips`
|
|
473
|
+
how many consecutive failures or busy targets disable it. A blank or over-16-KiB
|
|
474
|
+
prompt, two recurrence kinds, a `scratch` target and a `--weekday` without
|
|
475
|
+
`--time` are all refused with exit 2.
|
|
476
|
+
|
|
477
|
+
`tick` prints one line per entry it touched (`fired`, `skipped`, `missed`,
|
|
478
|
+
`failed`, or `would fire` under `--dry-run`), with the reason; nothing due prints
|
|
479
|
+
`no schedules due`, and a tick that finds the lock held by another prints `no
|
|
480
|
+
schedules due (another tick holds the lock)` — both exit 0, so a crontab line
|
|
481
|
+
that runs every five minutes is quiet. `run-now`, `enable`, `disable` and `cancel`
|
|
482
|
+
take the id, a unique prefix of it, or the title; a missing one exits 1, an ambiguous one 2 naming the matches.
|
|
483
|
+
|
|
484
|
+
Point cron at it (see [Scheduling](scheduling.md#running-the-tick) for systemd
|
|
485
|
+
and launchd), or turn on `schedule.auto_tick` and let chi web's hub tick every
|
|
486
|
+
`schedule.tick_interval_minutes` minutes; the lock covers both, so they never
|
|
487
|
+
fire twice.
|
|
488
|
+
|
|
489
|
+
The model can register its own schedules with `schedule_task` (defaulting to the
|
|
490
|
+
session it is in), list them with `list_schedules` and stop them with
|
|
491
|
+
`cancel_schedule`; like `register_reminder`, these are owner-voiced tools, refused
|
|
492
|
+
in a participant's review or pair turn.
|
|
493
|
+
|
|
439
494
|
## Flags
|
|
440
495
|
|
|
441
496
|
Samagotchi exposes one flag that feeds a prompt (`-p`, `--prompt`) and one that
|
data/docs/configuration.md
CHANGED
|
@@ -174,6 +174,7 @@ or `chi web` restart.
|
|
|
174
174
|
| `recap.*` | Worker start | The engine builds its recap job at start, and again when it binds a session with its own `recap.enabled` or `recap.sentences` (`--set`, see "Per-session settings") and when `/recap on\|off\|default` changes the session's own value. |
|
|
175
175
|
| `session.idle_exit_minutes`, `session.project_env`, `session.carry_last_answer`, `session.keep_empty`, `session.max_children`, `session.delegate_reports`, `session.task_wakes`, `session.max_wakes` | Worker start or next matching lifecycle decision | Existing workers keep their idle-exit timer and project env; the other values are read for the next corresponding decision. |
|
|
176
176
|
| `session.retention_*`, `session.max_count`, `session.keep_status`, `session.sweep_interval_hours` | Next retention sweep | `chi web` sweeps on its configured schedule and on `GET /api/sessions`; `chi sessions prune` uses the values for that invocation. The sweep interval is checked on the next hub tick. |
|
|
177
|
+
| `schedule.*` | Next CLI run | `chi schedule add` and `chi schedule tick` read them for that run; with `schedule.auto_tick` on, `chi web`'s hub ticks on `schedule.tick_interval_minutes`. |
|
|
177
178
|
| `web.*` except `web.participants.*` | `chi web` start | Restart `chi web` to change its bind address, port or page settings. |
|
|
178
179
|
| `log.*` | Process start | Each process holds its configured log sink and level after first use; restart that process. |
|
|
179
180
|
| `history.file` | Next history access | The TUI and web composer resolve its path when they read or append history. |
|
|
@@ -2270,6 +2271,14 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
|
|
|
2270
2271
|
| `session.max_count` | `500` | yes | Keep the newest N; `0` = uncapped. |
|
|
2271
2272
|
| `session.keep_status` | none | yes | Comma list of statuses never pruned (a session a worker or `chi` has open is never pruned anyway). |
|
|
2272
2273
|
| `session.sweep_interval_hours` | `24` | yes | How often the retention sweep runs. |
|
|
2274
|
+
| `schedule.auto_tick` | `false` | | `chi web`'s hub runs `chi schedule tick` too, sharing the schedule lock with cron. Off by default: cron or systemd is the documented driver. See [Scheduling](scheduling.md). |
|
|
2275
|
+
| `schedule.tick_interval_minutes` | `5` | | How often the hub ticks when `schedule.auto_tick` is on, and how late a `catch_up: none` schedule may be before it moves to its next occurrence. |
|
|
2276
|
+
| `schedule.default_keep_links` | `5` | | Chain links a schedule keeps behind its current one (`0` keeps all). Archived links are outside the retention sweep, so this is what bounds them. See [Scheduling](scheduling.md). |
|
|
2277
|
+
| `schedule.default_max_failures` | `3` | | Consecutive failures that disable a schedule. |
|
|
2278
|
+
| `schedule.default_max_skips` | `12` | | Consecutive busy targets that disable a schedule; `list` prints it as a time window. |
|
|
2279
|
+
| `schedule.default_catch_up` | `once` | | An overdue schedule fires once at the next tick (`once`) or moves to its next occurrence (`none`). |
|
|
2280
|
+
| `schedule.max_per_session` | `10` | | Enabled schedules one chain may hold. `schedule_task` (and `chi schedule add`) refuse past it, so a routine that re-schedules itself cannot pile up entries. |
|
|
2281
|
+
| `schedule.max_total` | `50` | | Enabled schedules this machine may hold, across every chain. |
|
|
2273
2282
|
| `image.max_side` | `1568` | | See "Images". |
|
|
2274
2283
|
| `image.max_bytes` | `3750000` | | See "Images". |
|
|
2275
2284
|
| `image.max_per_request` | `20` | | See "Images". |
|
data/docs/context.md
CHANGED
|
@@ -250,7 +250,9 @@ you're away from the terminal. All of these must hold:
|
|
|
250
250
|
project source's first text may predate the session, so neither wakes;
|
|
251
251
|
- `context.wake` is on (default `true`);
|
|
252
252
|
- the session's worker is up and idle: no turn running, nothing queued, no
|
|
253
|
-
question or continue offer waiting, not just started
|
|
253
|
+
question or continue offer waiting, not just started. A changed push from a
|
|
254
|
+
session-scoped source can start an idle-exited worker; it never restarts an
|
|
255
|
+
explicitly stopped session;
|
|
254
256
|
- under `session.max_wakes` turns in a row with no message from you (the
|
|
255
257
|
budget delegate reports and finished background tasks use too; a turn that
|
|
256
258
|
failed, or was stopped before the model call, pauses wakes until you write);
|
|
@@ -343,6 +345,10 @@ the chain's, names what moved and what stayed:
|
|
|
343
345
|
|
|
344
346
|
> Context moved with this session: pr-412, ci. Left with 3f2a9c1e: notes (pushed).
|
|
345
347
|
|
|
348
|
+
On a continue, text from a pushed source that the old session had not absorbed
|
|
349
|
+
or read is also carried in a separate note, clearly framed as third-party
|
|
350
|
+
text. The source itself stays with its old link; forks do not carry this text.
|
|
351
|
+
|
|
346
352
|
`chi send --new --continues` and the web's Continue toast say the same in a
|
|
347
353
|
few words, and `chi context ls` marks a session's source that stays
|
|
348
354
|
(`· stays`; `--format json` has the derived `movable`). Project sources are
|
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,6 +297,21 @@ 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
317
|
Built in, for `write`, `edit` and `memory_write` (symlinks resolved; see
|
|
@@ -527,7 +545,9 @@ message:
|
|
|
527
545
|
`batch`;
|
|
528
546
|
- owner-voiced tools, which act in the owner's name beyond the turn:
|
|
529
547
|
`delegate` and any `delegate_*` tool, `register_reminder`,
|
|
530
|
-
`cancel_reminder`, `
|
|
548
|
+
`cancel_reminder`, `schedule_task`, `list_schedules`, `cancel_schedule`
|
|
549
|
+
(a durable schedule outlives the session and fires on this machine),
|
|
550
|
+
`send_note`, `forget_outputs`, `task_create`,
|
|
531
551
|
`task_stop`, `find_tools` (it loads tools into the session), `objectives`
|
|
532
552
|
(a move on the owner's list), and
|
|
533
553
|
`memory_write` (a memory reaches the owner's later sessions); and the tools
|
|
@@ -1506,6 +1526,7 @@ the text a rule sees, and a function or alias shadowing a reader in the same
|
|
|
1506
1526
|
command isn't seen at all. `outside_repo` covers the file tools and git
|
|
1507
1527
|
only: `sed -i`, `cp` or `cat > file` into another folder from the shell aren't
|
|
1508
1528
|
asked about, and neither is git run through `sh -c`, a script, an alias, `make`
|
|
1509
|
-
or a folder in a variable. `!cmd` lines typed by you are not checked
|
|
1529
|
+
or a folder in a variable. `!cmd` lines typed by you are not checked (see
|
|
1530
|
+
[Who may run a session command](#who-may-run-a-session-command)). The only
|
|
1510
1531
|
real defence against an adversarial model is isolation (a sandbox, a git
|
|
1511
1532
|
identity without push rights).
|
data/docs/hooks.md
CHANGED
|
@@ -623,6 +623,9 @@ stripped of surrounding whitespace, except file text, an edit's
|
|
|
623
623
|
| `register_reminder` | `name` | — | `description`, `interval_minutes` |
|
|
624
624
|
| `cancel_reminder` | `name` | — | — |
|
|
625
625
|
| `list_reminders` | — | — | — |
|
|
626
|
+
| `schedule_task` | `prompt` | — | `title`, `every`, `weekday`, `time`, `date`, `mode`, `keep_links` |
|
|
627
|
+
| `list_schedules` | — | — | `include_disabled`, `all` |
|
|
628
|
+
| `cancel_schedule` | `name` | — | `all` |
|
|
626
629
|
| `list_sessions` | — | — | `cwd` |
|
|
627
630
|
| `send_note` | `text` | — | `session` |
|
|
628
631
|
| `context_read` | `name` | — | `offset`, `limit` |
|
|
@@ -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
|
@@ -235,6 +235,13 @@ The Bridge exposes:
|
|
|
235
235
|
sends its requests there (`worker_changed`); it gives up after ~12 s, or at once when the session
|
|
236
236
|
was stopped.
|
|
237
237
|
|
|
238
|
+
**Adding an event type.** Assign every emitted type a lifetime in `Events::CLASSIFICATION`
|
|
239
|
+
(`row`, `state`, `transient` or `internal`); keep stream frames and synthetic replay types in
|
|
240
|
+
`Events::STREAM_FRAMES` and `Events::SYNTHETIC`. `spec/events_drift_spec.rb` checks event coverage
|
|
241
|
+
and names the live web/TUI handlers; for rows it also checks the reload owner (`TurnAccumulator`
|
|
242
|
+
or `CardStore`) and TUI join replay. Follow any documented exception there when a row reloads
|
|
243
|
+
through session messages or a fallback path instead.
|
|
244
|
+
|
|
238
245
|
The wire chunk (the contract; the engine's own event keeps the provider's `payload` and the raw `content`):
|
|
239
246
|
|
|
240
247
|
```
|
|
@@ -365,6 +372,7 @@ The Bridge exposes:
|
|
|
365
372
|
the kind already taken is answered `200` as that one.
|
|
366
373
|
- `GET /session/:id/state` — `session_state_snapshot` (JSON).
|
|
367
374
|
- `GET /session/:id/stats` — `Engine#stats_snapshot` for attached `/stats`: the metrics, with the context window and prompt profile asked from the server before the first turn.
|
|
375
|
+
- `GET /session/:id/activity` — `Worker#activity_snapshot`: `{session_id, turn_running, input_pending, awaiting_continue, open_streams, reminders, tasks_holding}` — what a `chi schedule tick` asks a live worker before it fires (the tick runs out of process and cannot read a worker's memory). Read-only and small (under 300 bytes). `501 activity_unavailable` when this Bridge was built without an `on_activity:` step; the worker advertises it in `Bridge::FEATURES` (`activity`). See [scheduling.md](../scheduling.md).
|
|
368
376
|
- `GET /session/:id/snapshot` — the snapshot frame's content as one request (the web server renders
|
|
369
377
|
the messages itself, then streams from its `event_seq`). Read by the web's full show
|
|
370
378
|
(`GET /api/sessions/:id`, `?parts=1`) and the plugin's `list_sessions`; every message is copied.
|
data/docs/internals/bundles.md
CHANGED
|
@@ -45,9 +45,10 @@ user-facing side (what a bundle owns, profiles, needs) is in [memory.md](../memo
|
|
|
45
45
|
| `MemoryFiles` | which top-level `*.md` files of a bundle are its memories (`.select`: the `files:` list, else all but README/LICENSE and the like), `.unshipped` (listed, not shipped) and `.symlinks` (skipped: a symlink is never a memory) |
|
|
46
46
|
| `Manifest` | parses/validates `manifest.yml` (`name:` is the bundle's name and a directory under `.bundles/`, so a path such as `../x`, `a/b`, `.` or `..` is refused: `name: … is not a bundle name`; YAML that doesn't parse or isn't a mapping is a `ValidationError` too; files, plugin, scripts, needs, requires_chi, includes, context_providers, session_settings: the keys a session's `chi --set` may give, a list or a mapping of them to types (`SESSION_SETTING_TYPES`), recorded at install in `manifest.json` (`session_settings`, `session_setting_types`) for `SessionConfig` to check; settings: the keys config.yml's `bundles: <name>:` may have, the same shape, recorded as `settings` (nil when the manifest has none) and `setting_types` for `BundleSettingsCheck`, whose warnings the Engine holds for the first turn and `SessionConfig`'s refusals share the words of) and writes it (`Manifest.write`, for Builder). It ignores `requires_chi_next`, a shipped bundle's release-time marker (`script/release/release_tools.rb`, docs/releasing.md). `KEYS` is every top-level key it reads, so a key outside it is one this chi ignores: `#unknown_keys`, in the order the manifest lists them, said in `Manifest.unknown_key_line`'s words by the Installer and `chi bundle check`. `SINCE` is the first release that read each key or shape (taken from the tags), and `#floor_keys` names the ones this manifest uses — including the shapes only a newer chi reads (a `files:` mapping, a typed `session_settings:`/`settings:`) — for check's `requires-chi-floor`. A `hooks:` entry is `Manifest::HooksRetired` (a `ValidationError`; `WORDS` is the one wording of the retirement, `.for_key`, `.for_files`, `.files_in` a source's `hooks/*.rb`) on a strict read and ignored by `Manifest.tolerant` |
|
|
47
47
|
| `Installer` | one install/upgrade/dry run of one bundle (below) |
|
|
48
|
-
| `AssetInstaller` | the parts that live in `.bundles/<name>/`: guardrail rules, plugin, scripts (and `#retire_hooks`: an older chi's `hooks/` removed, below); returns `AssetInstaller::Installed` (the paths the record keeps).
|
|
48
|
+
| `AssetInstaller` | the parts that live in `.bundles/<name>/`: guardrail rules, plugin, scripts (and `#retire_hooks`: an older chi's `hooks/` removed, below); returns `AssetInstaller::Installed` (the paths the record keeps). Each folder is one loop, `#install_set(kind_of_file, incoming)`: every file's `AssetState` (`AssetState.in`: the files there at any depth, the incoming and the recorded ones), its row in `FileActions`' `ASSET_INSTALL` or (an upgrade over a record) `ASSET_UPGRADE`, one `Installer::FileResult` per incoming file with the state and action (a dry run's word through `AssetInstaller::DRY_WORDS`, nothing written), a `save_first` file copied to the Installer's `Trash` first (`Trash#copy`, as `guardrails/x`, `plugin/x`, `scripts/sub/x`), then the folder replaced; the named-but-missing and symlink `InstallError`s come before it, the checksum warning after the write (a plugin or script, not a dev link). An older chi's `hooks/` keep `Trash#copy_edits` |
|
|
49
49
|
| `FileState` (file_state.rb) | one bundle memory as a command meets it, derived, never stored: `FileState.of(key, record:, provenance:, target_dir:, source_dir:, claimants:, shipped:)` reads the file (D), its base (B), the bundle's text (I) and the record's entry (R) once into `Digests` (`disk`, `base`, `incoming`, `recorded`, `marked_incoming`; `:unreadable` for a file it can't read) and names its `kind` and `mark` (`:none`, `:current`, `:legacy` = `InstalledBundle.legacy_mark?`). With a source (install, upgrade, remerge, restore, a plan): `:new`, `:user_same`, `:user_file` (not the bundle's), `:missing`, `:level` (D == I), `:pristine`, `:edited`, `:diverged`, `:pristine_no_base`, `:edited_no_base`, `:dropped_pristine`, `:dropped_edited`, `:dropped_missing` (`shipped: false`: the bundle no longer ships the key), `:unreadable`; with `source_dir: nil` (status, uninstall, diff) only `:pristine`, `:edited`, `:pristine_no_base`, `:edited_no_base`, `:missing`, `:unreadable`. Edited or not is always `#edited?` = `!Provenance.unchanged?` (the one edited rule: the checksum first, then the base); the base then says what changed, and a base the checksum contradicts (equal to an edited file) is no base to merge from (`:edited_no_base`). `#source_moved?` (a mark that speaks of other text than the bundle ships: Remerge's first question), `#shared_with` (the other bundles listing it, `Provenance.claimants` asked lazily, at most once: only a dropped file and an uninstall ask) |
|
|
50
|
-
| `
|
|
50
|
+
| `AssetState` (asset_state.rb) | one rule, plugin or script of a bundle's own folder as a command meets it, derived, never stored: `AssetState.of(path, kind_of_file:, recorded:, incoming:)` → `state` (`:new`, `:unchanged` (the source's bytes, a dev link to them too), `:pristine`, `:edited`, `:linked` (a link to other bytes), `:dropped_pristine`, `:dropped_edited`, `:missing`; with no source, as uninstall and status read it, a file there is a `dropped_*` or `:linked`), `#edited?` (the asset rule: `Trash.edit?`, its bytes neither the recorded sha, none recorded counting as edited, nor the incoming file's; a link never), `#label` (`guardrails/x`, as the trash and the plan name it). `AssetState.in(dir, …)` is a folder's set. Read by `AssetInstaller`, `ShippedUpdate#trash_bound`, `Uninstaller#save_edits`, `Status.asset_states` and `chi bundle diff`; `asset_state_spec` checks it against the corpus's asset cases |
|
|
51
|
+
| `FileActions` (file_actions.rb) | what each command that acts does in each state: one table per command (`INSTALL`, `FORCE` = install and upgrade `--force`, `UPGRADE`, `REMERGE`, `RESTORE`, `UNINSTALL`), a cell keyed `[kind, mark]`, `[kind, :any]` or `[kind, :shared]`, its value an `Action` (`do`: `:write_incoming`, `:merge`, `:decide`, `:keep`, `:skip`, `:move_to_trash`, `:refuse`, `:restore`; `save_first`; `next_base`: `:on_disk`, `:incoming`, `:recorded`, `:none`; `word`, the report bucket; `record`: `:own`, `:keep_entry`, `:mark`, `:hold_mark`, `:clear_mark`, `:drop`, `:not_owned`). `.for(command, state)`. A `:decide` cell (`[:diverged, :any]`, the legacy guard's `[:pristine, :legacy]`) isn't a function of kind and mark: `Merger::Upgrade#decision` answers it with an `Outcome`, which `.decided(outcome, command)` turns into the action. A dry run reads its command's row and doesn't do it (`Installer::DRY_WORDS` gives its word). Assets have their own tables, `ASSET_INSTALL`, `ASSET_UPGRADE` (an upgrade lists a replaced rule as installed, the plugin and a script as updated: the one cell keyed by kind, `[state, kind_of_file]`) and `ASSET_UNINSTALL`, read with `.for_asset(command, asset)`; their `do` adds `:remove` (deleted, `save_first` copying an edit). A new kind or a new acting command adds a row in each table it reaches; `file_actions_spec` checks every cell against the bundle-state corpus |
|
|
51
52
|
| `Merger` | the upgrade's merge. `Merger::Upgrade` (provenance, target dir, source dir) is the one way to ask what an upgrade does to a file, nothing written: `#decision(key[, state])` → a `Decision` (the `FileState`, the UPGRADE action with a `:decide` cell answered, and the `Outcome`), `#outcome` its `Outcome` (`status`: `install`, `fast_forward`, `keep`, `noop`, `conflict`, `merged`; `base`: the next base, `Action#next_base`'s words), `#state`, `#forecast` (a `Forecast` of `merged` and `behind` in one pass), `#behind` (a `conflict` outcome as a `FileStanding::Behind`: why, and from `TextMerge.changed_sections` what it missed, nothing for an older mark, whose base may be the user's text), `#paths`, used by the Installer's merge and dry run, `Remerge`, `ShippedUpdate.plan` and `SystemBundle.sync(dry_run:)`. A `:decide` cell runs `.merge_outcome` (`TextMerge` where both sides changed, git once per call: `merged`, with its `TextMerge::Result` and the digest it read as the Outcome's `merge` and `current_sha`; `ALREADY_MERGED`, a `keep` with base `:incoming`, when the merge adds nothing; or `conflict` with that `merge`, whose `why` says why none was possible; no base: `TextMerge::NO_BASE`, a base the checksum contradicts: `FileStanding::LEGACY_BASE`). `.write_merged` puts a merge in place: the digest checked just before the rename (`AtomicFile.write`'s block), through a symlink, the mode kept, the old file in the `Trash`; false when a live `memory_write` got in. A mark from an older chi (`InstalledBundle.legacy_mark?`: no `incoming_sha`, or `legacy_base`, which keeps that known once the digest is recorded, until the mark goes) may have the user's own text as its base, 0.149's doing, and a merge against it would undo that text with nothing to show, or a fast-forward replace all of it: the upgrade's `[:pristine, :legacy]` cell decides: `#decision` makes a fast-forward that takes a line out of the file, and `.merge_outcome` a merge that does, a `conflict` (`FileStanding::LEGACY_BASE`), on the Installer's and `Remerge`'s path alike, unless `ShippedHistory` proves the base is a version chi shipped. The price, with no such gem left or for a bundle chi doesn't ship: a real base looks the same, so such a file whose bundle rewords or removes a line it still holds stays behind until it is resolved by hand, with `--agent` or with `--force`; no retry fixes it. A fast-forward that goes ahead over a mark is a `save_first` cell (`Installer#save_first`): a file put back to its base under this chi's mark, or an older mark with a proved base or an update that only adds. `.markers?` (a file holding a merge's `<<<<<<<`/`>>>>>>>` lines; `=======` alone is a Markdown underline) |
|
|
52
53
|
| `ShippedHistory` | `.shipped?(bundle, file, path)`: whether those bytes are that file of that bundle (by manifest name, since a shipped dir can be renamed) in an installed `samagotchi-<version>` gem. Reads `<gem dir>/gems/` by hand, for `Gem.path` (set by a chi home's boot string) and the home's `gem_dirs` from home.json (a checkout or another Ruby runs on other gems), never `Gem::Specification`. Size, then bytes, then the manifest. Anything it can't read is no match, which keeps the guard. Asked only when `Merger`'s guard would otherwise fire: with 54 gems on the Studio a miss costs about 4 ms, a hit less |
|
|
53
54
|
| `TextMerge` | the text merge: `.merge(base:, ours:, theirs:)` → a `Result` (`status` `:clean` with `text`, `:conflict` with `hunks`, `:failed` with `why`). `git merge-file -p --diff3 --marker-size=20` on temp copies in a temp cwd, with every `GIT_*` that points at a repository or a config unset or at `/dev/null` (it can run inside another tool's hook). git isn't a runtime dependency: none on PATH, or macOS's `/usr/bin/git` with no developer tools behind it (`.apple_stub?`, by `xcode-select -p`; running it pops a dialog), is `:failed`. So is a git past `TIMEOUT` (10 s; its process group is killed: this runs under `SystemBundle`'s lock), and any doubt about the answer: `min(hunks, 127)` against the exit status, a status over 127, a NUL in a side, marker lines that don't pair up (a user's line equal to one), a line no input holds, or an empty answer to non-empty inputs. `Scan` reads the output: a hunk unions (ours, then the entries of theirs that ours lacks) only when the last heading before it, at level 1 or 2 and outside code fences, HTML comments and any hunk, is titled exactly `Changelog` or `Change log`, with nothing but list entries (an item and its indented lines) since that heading and no fence or comment open, and base, ours and theirs each hold only list entries, with every base entry still on both sides: both only added. Any other hunk is a real conflict, and markers never leave the module. An entry is an item and the lines indented deeper under it, nested items too, and the union compares whole entries. `.changed_sections(base, theirs, ours)`: the headings of the sections the bundle changed and the file hasn't got, and whether text under no heading did, which is what names a miss: a conflict's hunks can't, since the whole file stays and the one hunk may be a Changelog line beside a whole new section. `Hunk#heading` counts the places under no heading |
|
|
@@ -60,9 +61,9 @@ user-facing side (what a bundle owns, profiles, needs) is in [memory.md](../memo
|
|
|
60
61
|
| `BlockRenderers` (`lib/samagotchi/block_renderers.rb`) | a manifest's `block_renderers:`: `.parse_list`, and what chi web asks without loading plugins: `.for_installed` (first bundle by name wins a language; a record with an error, an unmet `requires_chi` or a list that doesn't parse is skipped) and `.script_path` (`load` membership first, then `Provenance.sha_matches?`). `Manifest` checks each `load` is a `scripts:` key; the files are ordinary scripts everywhere else ([block-renderers.md](block-renderers.md)) |
|
|
61
62
|
| `Builder` | `chi bundle build`: memories (+ the installed bundle's rules, plugin, scripts; never an older record's hooks) → zip/tar/dir |
|
|
62
63
|
| `Profile` | meta bundles (`includes:`): install the members, record which; `InstallResult#kept_own` names a new member's memory files the user already had, with other bytes (`kept_own_line`, printed by `chi bundle`, `chi update` and `chi bootstrap`); a dry run runs each member's Installer as a dry run too, so it names them as well |
|
|
63
|
-
| `SystemBundle`, `ShippedUpdate` | the built-in system bundle (`ensure!` at Engine start, `sync` in `chi update`) and `chi update`'s upgrade of shipped bundles; both drive the Installer with `upgrade: true` and `Remerge` at an equal version (`ShippedUpdate::Row`'s `:would_merge`, `:merged`; `SystemBundle::Result`'s `:merged`). `SystemBundle` also puts back a memory missing at an equal version, that file alone, with no Installer (`verify_files_present`, `Provenance#record_restored`), checked against the manifest's `files:` sha as an installed one is (`FileDigest.mismatch`). A sync's `Result#warnings` reach `chi update`'s row (`UpdateCommand#system_bundle_row`) and `chi bootstrap`'s line (`BootstrapCommand#system_bundle_line`); `ShippedUpdate.apply` puts an update's `Checksum mismatch` warnings in `Row#note`. `ensure!` echoes a merge in one line, in the row's words. That reaches a terminal launch; a worker has only its log (`Log.configure(stderr: false)`), so every merge leaves a note on the file's record (`Installer#note_merges`, a failed run's too, and `Remerge#note_merges`; `Provenance#note_merged`: version, time, trash dir). `chi bundle status` shows it, and `chi update` reads the notes before the run (`SystemBundle.merge_notes`, `ShippedUpdate::Row#earlier`), names them in the row and clears after it those exact ones and the notes of the files that run merged (a worker's meanwhile, on another file, stays), unless it was a dry run, the row failed or has no place for them (a skipped system row), or the run is unattended (`ChildEnv::UNATTENDED_UPDATE`, which `Web::UpdateOps` sets for `update.auto`, an env marker so a hand-over's exec keeps it and an older chi ignores it; `--json` alone says nothing, as a person or script may use it). `chi bundle upgrade` clears the notes of what it printed, and `--force` those of the files it replaced. `--json`'s `merged` is this run's, `earlier` the notes'. `ensure!` says what an upgrade left behind and nothing of a file already behind, which `Result#behind` still has for the row. A plan or sync that can't read a file is that bundle's failed row, in `Trash.failure_message`'s words (`ShippedUpdate.plan_row`, `SystemBundle.sync`). At an equal version both keep the shipped `manifest.yml` for a record an older chi wrote (`Provenance#adopt_manifest` from the shipped dir): `SystemBundle.sync` under its lock, not in a dry run (so at the first Engine start or `chi update` after the upgrade, once), and `ShippedUpdate.apply` for a row that ends `:up_to_date` or `:merged` (`#keep_manifest`, through `ManifestStanding.keep`: `Row#manifest` is its line, `would keep its manifest` from the plan's dry run, appended to the row's status); a bumped bundle gets it from its Installer. `ShippedUpdate::Row#replaced` is what the upgrade copies to the trash (`ShippedUpdate.trash_bound`:
|
|
64
|
+
| `SystemBundle`, `ShippedUpdate` | the built-in system bundle (`ensure!` at Engine start, `sync` in `chi update`) and `chi update`'s upgrade of shipped bundles; both drive the Installer with `upgrade: true` and `Remerge` at an equal version (`ShippedUpdate::Row`'s `:would_merge`, `:merged`; `SystemBundle::Result`'s `:merged`). `SystemBundle` also puts back a memory missing at an equal version, that file alone, with no Installer (`verify_files_present`, `Provenance#record_restored`), checked against the manifest's `files:` sha as an installed one is (`FileDigest.mismatch`). A sync's `Result#warnings` reach `chi update`'s row (`UpdateCommand#system_bundle_row`) and `chi bootstrap`'s line (`BootstrapCommand#system_bundle_line`); `ShippedUpdate.apply` puts an update's `Checksum mismatch` warnings in `Row#note`. `ensure!` echoes a merge in one line, in the row's words. That reaches a terminal launch; a worker has only its log (`Log.configure(stderr: false)`), so every merge leaves a note on the file's record (`Installer#note_merges`, a failed run's too, and `Remerge#note_merges`; `Provenance#note_merged`: version, time, trash dir). `chi bundle status` shows it, and `chi update` reads the notes before the run (`SystemBundle.merge_notes`, `ShippedUpdate::Row#earlier`), names them in the row and clears after it those exact ones and the notes of the files that run merged (a worker's meanwhile, on another file, stays), unless it was a dry run, the row failed or has no place for them (a skipped system row), or the run is unattended (`ChildEnv::UNATTENDED_UPDATE`, which `Web::UpdateOps` sets for `update.auto`, an env marker so a hand-over's exec keeps it and an older chi ignores it; `--json` alone says nothing, as a person or script may use it). `chi bundle upgrade` clears the notes of what it printed, and `--force` those of the files it replaced. `--json`'s `merged` is this run's, `earlier` the notes'. `ensure!` says what an upgrade left behind and nothing of a file already behind, which `Result#behind` still has for the row. A plan or sync that can't read a file is that bundle's failed row, in `Trash.failure_message`'s words (`ShippedUpdate.plan_row`, `SystemBundle.sync`). At an equal version both keep the shipped `manifest.yml` for a record an older chi wrote (`Provenance#adopt_manifest` from the shipped dir): `SystemBundle.sync` under its lock, not in a dry run (so at the first Engine start or `chi update` after the upgrade, once), and `ShippedUpdate.apply` for a row that ends `:up_to_date` or `:merged` (`#keep_manifest`, through `ManifestStanding.keep`: `Row#manifest` is its line, `would keep its manifest` from the plan's dry run, appended to the row's status); a bumped bundle gets it from its Installer. `ShippedUpdate::Row#replaced` is what the upgrade copies to the trash (`ShippedUpdate.trash_bound`: the edited `AssetState`s against the shipped source over the rules, plugin and scripts folders, and `Trash.edits` over an older record's `hooks/` (which `#retire_hooks` removes whole), no recorded sha counting as edited), the same rule and files as `AssetInstaller`'s copies. `ShippedUpdate.edited_executables` (`chi bundle check`'s "does chi load it?") stays sha-only, as `Status.plugin_status` does. `ShippedUpdate.external` is the rest: what `chi update` only checks |
|
|
64
65
|
| `InstalledCopy`, `BundleCommand::InstalledCheck` | `chi update`'s re-check of the bundles chi didn't ship ([docs/cli.md](../cli.md#updating)). `InstalledCopy` lays a record and its installed files out as a source folder (manifest.yml from `Manifest.write`, the files' own shas, memories from `bases/`; its `files:`, `plugin:` and `scripts:` are always what the record and the copy hold (no `hooks:`), so a memory the install left out or one with no base gives no `missing-file`, and every other key is the kept `manifest.yml`'s when `InstalledManifest` read it, so the check sees its unknown keys, else the mirror's; a kept key that didn't parse keeps the value `InstalledManifest` fell back to, since `InstalledCheck` already names its problem); `InstalledCheck` forks per bundle (a plugin's `exit` or hang is that bundle's row; stdio to /dev/null, so `--json` stays parseable; `TIMEOUT`, then KILL of its group) and runs `Check.new(dir, installed: true)` there, which leaves out what only suits a source tree. Its `boundary` finding is narrower there than in a source tree: `Testing::Boundary.gone?` calls a reference gone only when its constant doesn't resolve or the object has no method the name names, so a bundle reaching `Samagotchi::Log` gets no row. lib loads lazily, so when a name doesn't resolve `gone?` requires every file of this chi's library in that same child and asks again (about 0.2 s, only for a bundle that reaches into chi at all); anything it can't read — a `const_get`, a require of a file that is there — is taken as still there. The fork is the point: `Sandbox` swaps ENV and the plugin's code is arbitrary. `UpdateCommand#external_rows` makes the rows (`level`, never `failed`) and holds the check back while a newer gem is yet to take over (`@not_checked`), since a fork is this chi's code |
|
|
65
|
-
| `Uninstaller`, `Trash`, `Status`, `Listing` | remove (the bundle's own folder too: an edited file there is copied to the trash first, `Uninstaller#trash_copy_line`); keep removed files (`Trash#move`) and a copy of overwritten ones (`Trash#copy`, its `as:` keeping a subfolder); `Provenance.unchanged?` (the record's checksum, else the base; neither counts as edited) is the one edited rule, read by `prune_dropped_files`, the uninstall's refusal and `chi bundle status`'s `modified`; `chi bundle status` (per file, `behind`, `merge_note`, `unreadable`; `issues=` counts behind, missing and unreadable, not edited); `chi bundle list` (`Installed#behind`) |
|
|
66
|
+
| `Uninstaller`, `Trash`, `Status`, `Listing` | remove (the bundle's own folder too: an edited file there is copied to the trash first, `Uninstaller#trash_copy_line`); keep removed files (`Trash#move`) and a copy of overwritten ones (`Trash#copy`, its `as:` keeping a subfolder); `Provenance.unchanged?` (the record's checksum, else the base; neither counts as edited) is the one edited rule, read by `prune_dropped_files`, the uninstall's refusal and `chi bundle status`'s `modified`; `chi bundle status` (per file, `behind`, `merge_note`, `unreadable`; per rule and script `Status.asset_states`: ok, `modified` = `AssetState#edited?`, missing; `issues=` counts behind, missing and unreadable files, a missing or edited rule (it doesn't load) and a missing script, not an edited memory or script); `chi bundle list` (`Installed#behind`) |
|
|
66
67
|
| `IndexUpdater`, `IndexSync` | the managed `index.md` lines (Installer; and the write/edit tools for memories) |
|
|
67
68
|
| `EntrySize` | one memory's size in tokens: `memory.entry_warn_tokens`' crossing note after a memory write, `chi self`'s `big memories` row |
|
|
68
69
|
| `IndexSize` | one scope's memory index as the prompt carries it (tokens, lines, bytes): `memory.index_warn_tokens`' crossing note after a memory write, `chi self`'s summary, the prompt's figures (`SystemPrompt`) |
|
|
@@ -113,7 +114,7 @@ hooks is refused before its manifest is read (`Installer#refuse_hooks`, then `Ma
|
|
|
113
114
|
run too; `ShippedUpdate`'s plan skips one the same way. Over such a record, `AssetInstaller#retire_hooks` (after the
|
|
114
115
|
plugin and scripts, so a refused install leaves them) copies every edited or unknown file under `hooks/` to the trash
|
|
115
116
|
(`Trash#copy_edits` with the recorded shas; a dev link never), removes the dir and records each as `:removed` (a dry
|
|
116
|
-
run's `:would_remove
|
|
117
|
+
run's `:would_remove`) with the `AssetInstaller::RETIRED` note, which `#summary` lists on a line of
|
|
117
118
|
its own; `ShippedUpdate#trash_bound` names the same files (`Trash.edits` over `hooks/`). `Provenance#write` records
|
|
118
119
|
`"hooks" => {}` and deletes the old hook bases. An older chi reading such a record sees a bundle with no hooks.
|
|
119
120
|
|
|
@@ -148,8 +149,9 @@ The order matters: warnings print in it, and a plugin/script the manifest names
|
|
|
148
149
|
differs copied to the trash first (`Trash#copy`, said by `#trash_copy_line`; a profile's in `InstallResult#trash_copies`). A
|
|
149
150
|
same-name file the bundle didn't write is the user's: skipped and never recorded (`@owned`). Model overlays
|
|
150
151
|
(`<name>.<key>.md`) get no index line (`note_overlay`).
|
|
151
|
-
4. `AssetInstaller#run`: guardrails, plugin, scripts
|
|
152
|
-
replaces the previous version's, an edited file copied to
|
|
152
|
+
4. `AssetInstaller#run`: guardrails, plugin, scripts (`#install_set` each: its `AssetState`s and the `ASSET_*` table),
|
|
153
|
+
then `#retire_hooks` (an older record's `hooks/`); each set replaces the previous version's, an edited file copied to
|
|
154
|
+
the trash first. requires_chi warnings for the plugin and
|
|
153
155
|
rules. A file that is a symlink or resolves outside the bundle dir (`AssetInstaller#linked?`) is skipped with a
|
|
154
156
|
warning (a rule) or raises `InstallError` (the plugin, a script).
|
|
155
157
|
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.
|
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.
|