samagotchi 0.159.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 +40 -1
- data/docs/architecture.md +5 -6
- data/docs/cli.md +1 -1
- data/docs/guardrails.md +21 -2
- data/docs/internals/attached-context.md +4 -2
- data/docs/internals/bridge.md +7 -0
- data/docs/internals/engine.md +7 -1
- data/docs/memory.md +41 -13
- data/docs/scheduling.md +16 -3
- data/docs/sessions.md +3 -1
- data/lib/samagotchi/bridge.rb +11 -2
- data/lib/samagotchi/broadcast/scope_card.rb +2 -1
- data/lib/samagotchi/bundle_command.rb +1 -0
- 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 +4 -0
- data/lib/samagotchi/command_intake.rb +56 -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_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/hook_routes.rb +0 -1
- data/lib/samagotchi/memory_bundle/asset_installer.rb +27 -6
- 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/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 +5 -3
- data/lib/samagotchi/memory_bundle/status.rb +2 -2
- data/lib/samagotchi/memory_bundle/uninstaller.rb +2 -0
- 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/schedules.rb +99 -39
- data/lib/samagotchi/session.rb +17 -2
- data/lib/samagotchi/session_access/local.rb +8 -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 +9 -2
- data/lib/samagotchi/session_manager/worker_command.rb +7 -19
- data/lib/samagotchi/state_format.rb +118 -0
- data/lib/samagotchi/tools/context_read.rb +7 -2
- data/lib/samagotchi/tools/delegate.rb +10 -2
- 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/session_access_routes.rb +3 -3
- data/lib/samagotchi/worker.rb +18 -6
- data/lib/samagotchi/worker_inbound.rb +21 -2
- metadata +5 -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,44 @@ 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
|
+
|
|
11
49
|
## [0.159.0] - 2026-11-09
|
|
12
50
|
|
|
13
51
|
Update with `chi update`.
|
|
@@ -5884,7 +5922,8 @@ and long-lived sessions.
|
|
|
5884
5922
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
5885
5923
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
5886
5924
|
|
|
5887
|
-
[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
|
|
5888
5927
|
[0.159.0]: https://github.com/dm1try/samagotchi/compare/v0.158.0...v0.159.0
|
|
5889
5928
|
[0.158.0]: https://github.com/dm1try/samagotchi/compare/v0.157.0...v0.158.0
|
|
5890
5929
|
[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
|
|
@@ -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,7 +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`). |
|
|
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
|
|
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). |
|
|
659
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)). |
|
|
660
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. |
|
|
661
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
|
@@ -36,7 +36,7 @@
|
|
|
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` (
|
|
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
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)
|
|
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))
|
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
|
|
@@ -1508,6 +1526,7 @@ the text a rule sees, and a function or alias shadowing a reader in the same
|
|
|
1508
1526
|
command isn't seen at all. `outside_repo` covers the file tools and git
|
|
1509
1527
|
only: `sed -i`, `cp` or `cat > file` into another folder from the shell aren't
|
|
1510
1528
|
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
|
|
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
|
|
1512
1531
|
real defence against an adversarial model is isolation (a sandbox, a git
|
|
1513
1532
|
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
|
@@ -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
|
```
|
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.
|
data/docs/memory.md
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Samagotchi stores memories in two scopes:
|
|
4
4
|
|
|
5
|
-
- Project scope: `$XDG_CONFIG_HOME/samagotchi/memories/projects/<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
- Project scope: `$XDG_CONFIG_HOME/samagotchi/memories/projects/<key>/`, one folder per project: a
|
|
6
|
+
git repository, shared by all its worktrees and subdirectories (`<name>_<hash>`, named and hashed
|
|
7
|
+
by its root), or a folder marked by `chi project init` and its subfolders (its id,
|
|
8
|
+
`<name>-<8 hex>`: see [Projects without git](#projects-without-git)); outside both, one per
|
|
9
|
+
working directory (`<name>_<hash>`)
|
|
9
10
|
- System scope: `$XDG_CONFIG_HOME/samagotchi/memories`
|
|
10
11
|
|
|
11
12
|
`$XDG_CONFIG_HOME` falls back to `~/.config` when unset.
|
|
@@ -13,8 +14,9 @@ Samagotchi stores memories in two scopes:
|
|
|
13
14
|
## Projects without git
|
|
14
15
|
|
|
15
16
|
A folder in no git repository becomes a project with `chi project init [DIR]` (see
|
|
16
|
-
[cli.md](cli.md#commands)): it writes `DIR/.chi/project.yml` (
|
|
17
|
-
`.chi/` dir, is the marker), and DIR and every subfolder share one
|
|
17
|
+
[cli.md](cli.md#commands)): it writes `DIR/.chi/project.yml` (a comment line and the project's
|
|
18
|
+
`id:`; the file, not a bare `.chi/` dir, is the marker), and DIR and every subfolder share one
|
|
19
|
+
project memories folder, group their
|
|
18
20
|
sessions (`chi sessions list`, the web's project picker) and take `chi context --project`. Nothing
|
|
19
21
|
chi-specific has to be committed anywhere: a repository never needs the file. It can also be created
|
|
20
22
|
from chi web's project picker (a folder inside a git repository opens that repository's project, and
|
|
@@ -26,10 +28,34 @@ the folder is left as is: it is created when missing, nothing is marked).
|
|
|
26
28
|
at `~` puts every folder under home in that repository, so markers there never apply.
|
|
27
29
|
- **Not at home.** A marker in `$HOME`, in a folder above it, or in `/` is ignored (`chi project init`
|
|
28
30
|
refuses them), so a marker in `~` never makes every folder under home one project.
|
|
29
|
-
- **The key
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
- **The key is the id.** A marked project's memories folder and its project context sources
|
|
32
|
+
(`chi context --project`) are named by the marker's `id:` (`<folder name>-<8 random hex>`), not
|
|
33
|
+
by its path, so moving or renaming the folder keeps them: a session started there after the move
|
|
34
|
+
finds them, and one saved before it (its file stores `project_id`) keys its context sources by the
|
|
35
|
+
id too. A folder chi already ran in at its top keeps its memories after `init`: they move to the
|
|
36
|
+
id. A later `git init` in the folder makes it a repository, keyed by its path again (git wins): its
|
|
37
|
+
memories stay in the id folder until you move them, and chi names the id folder at session start
|
|
38
|
+
(and under a `memory_read` miss) while the repository's own folder holds no memories. Memories saved from a *subfolder* before
|
|
39
|
+
`init` stay in that subfolder's own folder: move them by hand if you want them in the project's.
|
|
40
|
+
A hand-edited id must be letters, digits, `_`, `.` or `-` (up to 64, a letter or digit first);
|
|
41
|
+
anything else (or a broken file) is ignored and the project keyed by its path (a debug log line
|
|
42
|
+
says why, `chi project init` reports it).
|
|
43
|
+
- **Older markers adopt an id** when you rerun `chi project init` in the folder: the folders keyed
|
|
44
|
+
by its path (memories, project context sources, the skills bundle's kept versions) move to the id,
|
|
45
|
+
each leaving a link by its old name (an older chi, keying by path, finds the same folder), then the
|
|
46
|
+
id is written. It refuses while a session of the project runs (stop it first, so nothing writes to
|
|
47
|
+
the old folder mid-move), and names memories an earlier path left (the same folder name with
|
|
48
|
+
another hash: a folder moved before it had an id), merging nothing. A link stays behind: a new
|
|
49
|
+
folder (or git repository) made later at the old path reaches the moved project's memories
|
|
50
|
+
through it; delete the link to stop that. chi web's project picker marks a new folder with an id
|
|
51
|
+
but never moves folders: a folder it marks that already has path-keyed memories gets no id until
|
|
52
|
+
`chi project init`.
|
|
53
|
+
- **A copy shares memories.** A copied marked folder (`cp -R`) keeps the id, so the two share one
|
|
54
|
+
memories folder; at session start in the copy chi says so (a `memory>` load warning: "… share chi
|
|
55
|
+
project id … (a copied folder?)"; the id folder's `.root` names the folder that started a session
|
|
56
|
+
with it last, the original keeps it). `chi project init --new-id` in the copy gives it a fresh id,
|
|
57
|
+
with a copy of the memories and project context sources it had: each folder goes its own way.
|
|
58
|
+
After a move, the new path just takes `.root` over, with no warning.
|
|
33
59
|
- **Restart what runs there.** A session running in the folder when it is marked keeps the old memories
|
|
34
60
|
folder and AGENTS.md chain in its prompt until `chi sessions restart ID` (init names them); a running
|
|
35
61
|
`chi web` groups the folder's older sessions under the project after its restart.
|
|
@@ -52,13 +78,15 @@ Tool behavior:
|
|
|
52
78
|
|
|
53
79
|
## Moving memories to another machine
|
|
54
80
|
|
|
55
|
-
A
|
|
81
|
+
A repository's folder is keyed by its root's absolute path (`<basename>_<8 hex of MD5(root)>`), so project
|
|
56
82
|
memories copied to a machine (or a path) where the repository lives elsewhere sit in a folder chi no
|
|
57
83
|
longer reads. Rename the folder to the new key, or move memories with a bundle zip (see
|
|
58
|
-
[What a bundle owns](#what-a-bundle-owns)).
|
|
84
|
+
[What a bundle owns](#what-a-bundle-owns)). A marked project with an id needs neither: copy its id
|
|
85
|
+
folder along with the marked folder.
|
|
59
86
|
|
|
60
87
|
Inside a project (a git repository or a marked folder), when this project's folder is missing or holds no `*.md` while the projects
|
|
61
|
-
dir has a folder of the same basename with another hash
|
|
88
|
+
dir has a folder of the same basename with another hash (`<basename>_<hash>`, or a marked project's id folder
|
|
89
|
+
`<basename>-<8 hex>`; links, an adopted project's old names, are skipped), chi says so, naming both folders and the
|
|
62
90
|
`mv` that fixes it (a name match can't prove it's the same project, so nothing is renamed for you):
|
|
63
91
|
|
|
64
92
|
- at session start, a `memory>` load warning (a `:guardrail_warning` with label `memory`; the attached
|
data/docs/scheduling.md
CHANGED
|
@@ -8,6 +8,11 @@ chi web's hub can never lose an update to each other). A **tick** — cron, syst
|
|
|
8
8
|
launchd, `chi schedule tick`, or chi web's hub when `schedule.auto_tick` is on —
|
|
9
9
|
walks the entries whose `next_due` has passed and runs the ones that are not busy.
|
|
10
10
|
|
|
11
|
+
The file is versioned: each entry member is required or declares the format
|
|
12
|
+
version and default that introduced it. Rewrites preserve unknown keys and
|
|
13
|
+
entries missing required members; those entries are warned about and ignored by
|
|
14
|
+
`list` and `tick`, never erased. A write never lowers the file's version.
|
|
15
|
+
|
|
11
16
|
Each entry carries `id`, an optional `title`, `anchor` (the session it was made
|
|
12
17
|
against, never rewritten: the chain's root and the id you remember), `head` (the
|
|
13
18
|
chain's current link, re-anchored after every firing: the link a chain continue
|
|
@@ -30,9 +35,17 @@ depends on whether the worker has to be running.
|
|
|
30
35
|
| Longest interval | 1440 minutes | unlimited (`every`, `weekday`, `date`) |
|
|
31
36
|
| What it produces | a turn in the same session | a new chain link, or a queued turn in the same session |
|
|
32
37
|
|
|
33
|
-
A schedule turn carries `client_id: "system:schedule:<id>"
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
A schedule turn carries `client_id: "system:schedule:<id>"` in both modes (a chain
|
|
39
|
+
link's first turn through `Session#first_turn_client_id`, which `Schedules#fire_chain`
|
|
40
|
+
passes to `SessionContinue.run`), so `ClientId.origin` says `schedule` and the terminal
|
|
41
|
+
and the web label it as such rather than as something you typed. For the same reason a
|
|
42
|
+
schedule's prompt is never a session command: a `/word` prompt reaches the model as
|
|
43
|
+
text, and one that starts with `!` is refused by `chi schedule add` and `schedule_task`
|
|
44
|
+
([guardrails.md, Who may run a session command](guardrails.md#who-may-run-a-session-command)).
|
|
45
|
+
An entry stored with one before that fails when it fires (toward its `max_failures`),
|
|
46
|
+
before anything is started or delivered. A chi web
|
|
47
|
+
(or cron tick) still running an older chi starts chain links without the origin, so
|
|
48
|
+
restart it after updating. Like `register_reminder`, the schedule tools are owner-voiced:
|
|
36
49
|
a participant's review or pair turn is denied them ([guardrails.md](guardrails.md)).
|
|
37
50
|
|
|
38
51
|
## The two modes
|
data/docs/sessions.md
CHANGED
|
@@ -115,7 +115,7 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
|
|
|
115
115
|
|
|
116
116
|
## Session file
|
|
117
117
|
|
|
118
|
-
`Session::FIELDS` is the table of `<uuid>.json`, in the order `Session#save` writes it (`Session.new`, `.from_h` and `#to_h` all go by it;
|
|
118
|
+
`Session::FIELDS` is the table of `<uuid>.json`, in the order `Session#save` writes it (`Session.new`, `.from_h` and `#to_h` all go by it; 43 keys). Each key is one `SessionField` row (`session_field.rb`): its default for a file without it, how a value is cast when read and what the file gets, the codec being its type's own (`Toolkit.from_file`, `SessionReview.of`, `PromptNote.list`, `#to_file`…); the typed fields' writers cast through their row, and `#to_h` reads each field's instance variable, not its reader. A key a file lacks reads as the default below, a key present as `null` as `null` (then cast: `test_run` false, `toolkit_origin` chosen); `review`, `first_turn_id`, `first_turn_client_id`, `verdicts` and `verify_fresh` are written only when set (`OPTIONAL_FIELDS`). A key this chi doesn't know is kept and written back as it was (see the rule above). `thinking`, `reasoning_history` and `llm_context` follow one Unread rule (`SessionField.level`): a value this chi can't read (a newer chi's level, a hand edit) reads as none and is kept and written back as it was, until the session sets its own; `llm_context` keeps the parts it can't read under the ones it can. So does `toolkit_origin`: an unknown one (a String) reads as `chosen` and is written back as it was, until the session sets its own. Where the file and the session's dir are, and which ids name a session, is `SessionPaths` (`session_paths.rb`, a leaf that `Session` extends: `Session.session_dir` is `SessionPaths.session_dir`). The bytes a save writes for each field and on each path that makes or rewrites a file are pinned in `spec/session_file_goldens_spec.rb`, with what a fork, a continue, a delegate child and a respawn keep of their source (`carry_matrix.json`). "Who writes it": the Engine runs in the session's worker (or a `--non-interactive` run); the rest are named.
|
|
119
119
|
|
|
120
120
|
| Key | Meaning | Written by |
|
|
121
121
|
|-----|---------|------------|
|
|
@@ -136,6 +136,7 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
|
|
|
136
136
|
| `pending_question` | The question the worker waits on (`PendingQuestion`), `null` with none. | `QuestionDesk` |
|
|
137
137
|
| `used_memory_names` | The memories the session read. | the Engine |
|
|
138
138
|
| `project_root` | The project the session started in, stored because a worktree may be deleted later; `null` outside a repository or a marked folder (looked up again on read). | creation |
|
|
139
|
+
| `project_id` | A marked project's id (its `.chi/project.yml`'s `id:`), saved beside `project_root`: the session's project context sources are keyed by it, so they follow a moved folder. Absent for any other project. | creation |
|
|
139
140
|
| `preloaded_memory_names`, `muted_memory_names` | `--memory` and `--mute`, written before the worker starts so a respawn builds the same prompt. | creation |
|
|
140
141
|
| `prompt_notes` | The model notes the system prompt carried at its last build (`[{name, scope, chars, digest}]`, see [Memory](memory.md#model-notes)). | the Engine, at each prompt build |
|
|
141
142
|
| `parent_id` | The session that delegated this one or forked it (`ctx.sessions.fork`); `null` otherwise. See [Delegating](#delegating). | `SessionManager.spawn_session`; `ChildMove` writes a `parent.json` beside it instead of this file |
|
|
@@ -156,6 +157,7 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
|
|
|
156
157
|
| `working_set_pins` | The working-set items the user pinned (`/working-set pin`); kept by a respawn, a continue and a fork. | the Engine's command |
|
|
157
158
|
| `review` | A review session's subjects and source (`SessionReview`), see [Review sessions](#review-sessions). | `ReviewStart` |
|
|
158
159
|
| `first_turn_id` | The id the creating prompt's turn gets, so a wait can name it (`chi send --new --wait`, `--wait --turn T ID`); `null` for a session created idle. | `SessionManager.spawn_session` |
|
|
160
|
+
| `first_turn_client_id` | Who sent the creating prompt when no person did (`delegate:<parent8>` for a delegate's task, `system:schedule:<id>` for a chain schedule's link): that first turn's origin, so its line is no command of the owner's (docs/guardrails.md, Who may run a session command) and `last_turn` names the sender. Written only when set; not carried by a fork, a continue or a delegate. An older chi keeps it but runs the first prompt as a person's. | `SessionManager.spawn_session` (its `client_id:`) |
|
|
159
161
|
| `verify_fresh` | A Verify fresh child's requester, rules and claim (`VerifyFreshOrigin`). | `VerifyFresh.start`, once |
|
|
160
162
|
|
|
161
163
|
## Context notes
|
data/lib/samagotchi/bridge.rb
CHANGED
|
@@ -9,6 +9,7 @@ require "openssl"
|
|
|
9
9
|
require "time"
|
|
10
10
|
require_relative "atomic_file"
|
|
11
11
|
require_relative "bridge_secret"
|
|
12
|
+
require_relative "command_intake"
|
|
12
13
|
require_relative "degrade"
|
|
13
14
|
require_relative "delivery"
|
|
14
15
|
require_relative "session_inbox"
|
|
@@ -1195,9 +1196,17 @@ module Samagotchi
|
|
|
1195
1196
|
# A session command sent as a message (chi send -m "/model x", chi -p,
|
|
1196
1197
|
# a UI whose command list lags) runs as the command, as typed in a
|
|
1197
1198
|
# TUI; an unknown /word stays a prompt. Not with images: those are
|
|
1198
|
-
# for the model.
|
|
1199
|
+
# for the model. Only from a sender that may send commands
|
|
1200
|
+
# (CommandIntake.allowed?: a person, chi answer): a delegate's, a
|
|
1201
|
+
# schedule's or a plugin's /word line is a prompt, its shell line
|
|
1202
|
+
# refused (403, nothing queued).
|
|
1199
1203
|
if images.empty? && @on_command && command_registry.command?(prompt.to_s)
|
|
1200
|
-
|
|
1204
|
+
if CommandIntake.allowed?(client_id)
|
|
1205
|
+
return queue_command_request(sid, prompt.to_s.strip, client_id: client_id, deadline: deadline, author: author)
|
|
1206
|
+
elsif CommandIntake.shell_line?(prompt)
|
|
1207
|
+
Log.warn(:bridge, "shell_line_refused", client_id: client_id)
|
|
1208
|
+
return [{}, 403, { error: CommandIntake::REFUSED_ERROR, detail: CommandIntake::REFUSED }]
|
|
1209
|
+
end
|
|
1201
1210
|
end
|
|
1202
1211
|
|
|
1203
1212
|
enqueued_id = SecureRandom.uuid
|
|
@@ -46,7 +46,8 @@ module Samagotchi
|
|
|
46
46
|
session = Session.load(recipient.id, state_dir: state_dir)
|
|
47
47
|
prompts = user_texts(session.messages)
|
|
48
48
|
branch = GitHead.branch(session.working_directory)
|
|
49
|
-
sources = ContextSources.attached(session.id, project_root: session.project_root,
|
|
49
|
+
sources = ContextSources.attached(session.id, project_root: session.project_root, project_id: session.project_id,
|
|
50
|
+
state_dir: state_dir)
|
|
50
51
|
.map { |attached| [attached.name, attached.source.hint] }
|
|
51
52
|
ScopeCard.new(id: session.id, project: recipient.project && File.basename(recipient.project),
|
|
52
53
|
folder: folder(session.working_directory, recipient.project), branch: branch,
|
|
@@ -266,6 +266,7 @@ module Samagotchi
|
|
|
266
266
|
# @return [Array(MemoryBundle::PreparedSource, String), nil]
|
|
267
267
|
def prepare_or_fail(expanded_source, word)
|
|
268
268
|
prepared = Samagotchi::MemoryBundle::PreparedSource.prepare(expanded_source)
|
|
269
|
+
Samagotchi::MemoryBundle::Manifest::HooksRetired.refuse_source_files(prepared.dir)
|
|
269
270
|
named = [prepared, Samagotchi::MemoryBundle::Manifest.read(dir: prepared.dir).name]
|
|
270
271
|
rescue Samagotchi::MemoryBundle::SourceNormalizer::UnknownSourceError,
|
|
271
272
|
Samagotchi::MemoryBundle::Manifest::ValidationError => e
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: samagotchi-system
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.160.0
|
|
4
4
|
scope: system
|
|
5
5
|
description: Default system memories — identity, self map, config modification protocol, memory guide and the delegated-session rules
|
|
6
6
|
files:
|
|
7
7
|
identity.md: sha256:29c2f7df9f6c5acbc5ec17000a4490741e55e426af15ca0d81c8706edd9bd027
|
|
8
8
|
self_map.md: sha256:750822b3488b302c6e32638e6224f270d8e06b50ea85cd6e0bb5a6ff12eed1d8
|
|
9
9
|
config_modification_protocol.md: sha256:44cf7bdac8e526805bff36b76b54fefecd41db30ca525c2ed661d78fe9155dd8
|
|
10
|
-
memory_guide.md: sha256:
|
|
10
|
+
memory_guide.md: sha256:bdd34f34063df1b4ae74f7cda7c31080986d155e535ee4526d673c70edac8292
|
|
11
11
|
delegated.md: sha256:a0d6ddcd4a8be06526d964898611775fd251cfd9d145a9d16afaccaa503597bd
|
|
@@ -13,7 +13,7 @@ This memory teaches you (the agent) how to use Samagotchi memories — persisten
|
|
|
13
13
|
| Scope | Path | Use for |
|
|
14
14
|
|-------|------|---------|
|
|
15
15
|
| `system` | `$XDG_CONFIG_HOME/samagotchi/memories/`, default `~/.config/samagotchi/memories/` (`MemoryPaths.system_dir`) | User-wide preferences, identity, cross-project knowledge |
|
|
16
|
-
| `project` | `<system dir>/projects/<
|
|
16
|
+
| `project` | `<system dir>/projects/<key>/` (`MemoryPaths.project_dir`: a git project's `<basename>_<hash>`, `basename(root)` + 8-char `MD5(root)`, root = `MemoryPaths.project_root`; a marked project's id from its `.chi/project.yml`, `<basename>-<8 hex>`, kept when the folder moves) | Repo-specific conventions, workflow, stack decisions |
|
|
17
17
|
|
|
18
18
|
- The project root is the git repository: its common git dir, which every linked worktree shares. All worktrees and subdirectories of one repository share one project folder; the system prompt shows that folder (not the root: in a linked worktree, work stays in the worktree). A separate clone is a different project. Outside a repository, a folder marked by `chi project init` (its `.chi/project.yml`) is the project for it and every subfolder; outside both, the root is the working directory.
|
|
19
19
|
- `index.md` lives in each scope dir and contains auto-managed lines like `- **name** · scope · date · bytes — description`; a memory a bundle installed adds `· from <bundle>` before the description. Your free-form sections in `index.md` are preserved but managed lines are owned by `memory_write` (and refreshed when `write`/`edit` change a memory's file).
|
|
@@ -122,7 +122,7 @@ Bundles are versioned directories/zips/tar.gz/git URLs with a `manifest.yml` and
|
|
|
122
122
|
| `build [--scope system\|project] [--name NAME] [--version VER] [--description DESC] [--out PATH] [FILES...]` | **Inverse of install** — builds a shareable bundle from local memories and an installed bundle's plugin, rules and scripts. Infers `zip` vs `dir`/`tar.gz` from `--out` extension; default `chi_system_memories.zip` (system) or `chi_<repo>_memories.zip` (project, named after the project root) v`1.0.0` in `Dir.pwd`. `FILES...` is an optional allowlist of memory basenames (`identity` or `identity.md`); if omitted, all `*.md` except `index.md`/hidden/non-md and the memories other installed bundles own (each named in a `Left out` line) are included. Computes `sha256:` checksums and writes `manifest.yml` via `Manifest.write`. No provenance write. |
|
|
123
123
|
|
|
124
124
|
**Scope resolution for install/build:**
|
|
125
|
-
- CLI `--scope` wins over `manifest.yml` `scope`. Default is `system` if none given (`Installer#run`). For `project`, target is the project folder `<system dir>/projects/<
|
|
125
|
+
- CLI `--scope` wins over `manifest.yml` `scope`. Default is `system` if none given (`Installer#run`). For `project`, target is the project folder `<system dir>/projects/<key>` of the project (`MemoryPaths.scope_dir`).
|
|
126
126
|
|
|
127
127
|
**Example flows:**
|
|
128
128
|
```bash
|
data/lib/samagotchi/client_id.rb
CHANGED
|
@@ -98,6 +98,10 @@ module Samagotchi
|
|
|
98
98
|
# Whether +client_id+ is a participant's (review:).
|
|
99
99
|
def review?(client_id) = client_id.to_s.start_with?(REVIEW_PREFIX)
|
|
100
100
|
|
|
101
|
+
# The client id a delegating parent's task and follow-ups carry:
|
|
102
|
+
# delegate:<parent8>.
|
|
103
|
+
def delegate(parent_id) = "#{DELEGATE_PREFIX}#{parent_id.to_s[0, 8]}"
|
|
104
|
+
|
|
101
105
|
# The client id a durable schedule's turn carries: system:schedule:<id>.
|
|
102
106
|
def schedule(schedule_id) = "#{SCHEDULE_PREFIX}#{schedule_id}"
|
|
103
107
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "client_id"
|
|
4
|
+
|
|
5
|
+
module Samagotchi
|
|
6
|
+
# Who may send a session command as a message (a turn's text: chi send -m
|
|
7
|
+
# "/model x", a web page's first message, an input file, a session's
|
|
8
|
+
# first prompt), and what a line from anyone else is. The one rule the
|
|
9
|
+
# two intakes ask (WorkerInbound#queue_as_command, Bridge#handle_post_turn)
|
|
10
|
+
# and the senders that refuse up front (Tools::Delegate, Schedules#add).
|
|
11
|
+
# No Engine needed: a schedule's tick and the CLI have none.
|
|
12
|
+
#
|
|
13
|
+
# From a sender that may not, a /word line is plain prompt text (the
|
|
14
|
+
# model reads it) and a ! line (a shell command: SessionCommands#shell
|
|
15
|
+
# runs it with no Gate) is refused with REFUSED, never run.
|
|
16
|
+
module CommandIntake
|
|
17
|
+
# A shell line's prefix: !<cmd>, and !rollback (SessionCommands::
|
|
18
|
+
# SHELL_BANG_PREFIX, which a spec keeps the same: SessionCommands is too
|
|
19
|
+
# big a load for the bundle test kit's budget).
|
|
20
|
+
SHELL_PREFIX = "!"
|
|
21
|
+
|
|
22
|
+
# The one text a refused shell line gets: the Bridge's 403 detail, the
|
|
23
|
+
# worker's command_ran error, a delegate's or a schedule's refusal.
|
|
24
|
+
REFUSED = "a message that starts with ! is a shell command, which only a person runs as a message " \
|
|
25
|
+
"(a web tab, a chi TUI, chi send); from a delegate, a schedule or another automatic sender it is not run. " \
|
|
26
|
+
"Say what to run in words instead (\"run `ls`\"), or leave out the leading !"
|
|
27
|
+
|
|
28
|
+
# The Bridge's error code for it (403).
|
|
29
|
+
REFUSED_ERROR = "command_not_permitted"
|
|
30
|
+
|
|
31
|
+
# chi answer's marker (a parent agent's /continue yes and
|
|
32
|
+
# /parent-answers; Worker#as_parent? holds those to its own rules): any
|
|
33
|
+
# command, with chi send's trust.
|
|
34
|
+
ALLOWED_IDS = [ClientId::CLI_ANSWER].freeze
|
|
35
|
+
|
|
36
|
+
module_function
|
|
37
|
+
|
|
38
|
+
# Whether a message from +client_id+ may run as a session command: a
|
|
39
|
+
# human's input (ClientId.human?: a web tab, a chi TUI, chi send, no
|
|
40
|
+
# client) but a participant's (review:, refused on its own), and chi
|
|
41
|
+
# answer's. A delegate, a child's report, a context or task wake, a
|
|
42
|
+
# schedule, a reminder, a relay and a plugin may not (the bundles that
|
|
43
|
+
# send, coordinator and skills, send prose).
|
|
44
|
+
def allowed?(client_id)
|
|
45
|
+
ALLOWED_IDS.include?(client_id.to_s) || (ClientId.human?(client_id) && !ClientId.review?(client_id))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Whether +text+ would be a shell line (!<cmd>, !rollback) if a command:
|
|
49
|
+
# what a delegate's task or a schedule's prompt may not start with.
|
|
50
|
+
def shell_line?(text) = text.to_s.lstrip.start_with?(SHELL_PREFIX)
|
|
51
|
+
|
|
52
|
+
# Whether the command line +line+ from +client_id+ is a shell line that
|
|
53
|
+
# must not run.
|
|
54
|
+
def refused?(line, client_id) = shell_line?(line) && !allowed?(client_id)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -59,17 +59,19 @@ module Samagotchi
|
|
|
59
59
|
VERDICT = /\bVERDICT:?\s*([A-Z]+)(?![A-Za-z|])/
|
|
60
60
|
|
|
61
61
|
# @param project_root [String, nil] Session#project_root
|
|
62
|
+
# @param project_id [String, nil] Session#project_id
|
|
62
63
|
# @param peer_sources [PeerSources, nil] the peer bundles' declared sources
|
|
63
64
|
# @param peer_turns [#call, nil] the peer wake turns in a row
|
|
64
65
|
# (WorkerWakes#peer_turns); 0 or nil: no source is a peer's
|
|
65
|
-
def initialize(session_id:, state_dir:, project_root:, peer_sources: nil, peer_turns: nil)
|
|
66
|
+
def initialize(session_id:, state_dir:, project_root:, project_id: nil, peer_sources: nil, peer_turns: nil)
|
|
66
67
|
@session_id = session_id
|
|
67
68
|
@peer_sources = peer_sources
|
|
68
69
|
@peer_turns = peer_turns
|
|
69
70
|
@state_dir = state_dir
|
|
70
71
|
@own = ContextSources.session_location(session_id, state_dir: state_dir)
|
|
71
|
-
@project = ContextSources.project_location_for(project_root, state_dir: state_dir)
|
|
72
|
+
@project = ContextSources.project_location_for(project_root, project_id: project_id, state_dir: state_dir)
|
|
72
73
|
@project_root = project_root
|
|
74
|
+
@project_id = project_id
|
|
73
75
|
@signature = nil
|
|
74
76
|
end
|
|
75
77
|
|
|
@@ -79,7 +81,8 @@ module Samagotchi
|
|
|
79
81
|
return nil if signature == @signature
|
|
80
82
|
|
|
81
83
|
subs = @own.subscriptions
|
|
82
|
-
visible = ContextSources.attached(@session_id, project_root: @project_root,
|
|
84
|
+
visible = ContextSources.attached(@session_id, project_root: @project_root, project_id: @project_id,
|
|
85
|
+
state_dir: @state_dir)
|
|
83
86
|
deliveries = visible.filter_map do |attached|
|
|
84
87
|
next if @own.muted?(attached.name)
|
|
85
88
|
|