samagotchi 0.145.0 → 0.147.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 +49 -1
- data/docs/architecture.md +14 -5
- data/docs/cli.md +6 -3
- data/docs/configuration.md +16 -3
- data/docs/guardrails.md +7 -2
- data/docs/internals/bridge.md +3 -1
- data/docs/internals/engine.md +1 -1
- data/docs/internals/web-frontend.md +3 -2
- data/docs/memory.md +45 -0
- data/docs/sessions.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/child_reports.rb +13 -6
- data/lib/samagotchi/guardrails/approval.rb +17 -4
- data/lib/samagotchi/guardrails/parent_approvals.rb +5 -1
- data/lib/samagotchi/guardrails/verdict.rb +5 -3
- data/lib/samagotchi/memory_command.rb +135 -7
- data/lib/samagotchi/memory_paths.rb +9 -0
- data/lib/samagotchi/memory_usage.rb +368 -0
- data/lib/samagotchi/relay.rb +2 -2
- data/lib/samagotchi/tool_activity.rb +3 -3
- data/lib/samagotchi/tool_output_cut.rb +12 -1
- data/lib/samagotchi/toolkit_suggest.rb +7 -4
- data/lib/samagotchi/tools/delegate.rb +88 -13
- data/lib/samagotchi/tools/memory.rb +12 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +2 -2
- data/lib/samagotchi/web/auto_update.rb +60 -5
- data/lib/samagotchi/web/public/app.js +11 -18
- data/lib/samagotchi/web/public/participant_annotate.js +2 -1
- data/lib/samagotchi/web/public/question_cards.js +33 -9
- data/lib/samagotchi/web/public/revise_stack.js +6 -5
- data/lib/samagotchi/web/public/update_notice.js +22 -11
- data/lib/samagotchi/web/public/update_sheet.js +4 -1
- data/lib/samagotchi/web/public/verify_preset.js +5 -1
- data/lib/samagotchi/web/public/worker_features.js +14 -0
- data/lib/samagotchi/web/session_access_routes.rb +1 -1
- data/lib/samagotchi/web/session_hub.rb +17 -4
- data/lib/samagotchi/working_set.rb +3 -2
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 29546fb5487086302ed4d68c168fa9e7f4f576bee252e59cfdaf08a4e74081b4
|
|
4
|
+
data.tar.gz: 3c05877e89bccf719dbdf8f4287c8343222f897484c2f22a4c3245bdc92a2af1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 953166016279e2b81ebe64c9642df2a245cc3d7fe4520850bf4023557b5d1b7fd620589fd7c49df66d458e59eeae32dffb3020e6286ea8be85edf2710c2bd7d4
|
|
7
|
+
data.tar.gz: 6c01b8ac5e044770f34e4fd5057915116e8b56b81a6f505fac3259449911a02b091262aad2554417ebf0493068ace77b1636d3fb8187769225268d12540c5354
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,52 @@ and commands may change between minor versions. How releases are made:
|
|
|
7
7
|
[docs/releasing.md](docs/releasing.md).
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.147.0] - 2026-11-03
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- With `update.auto: install` a newer chi installed some other way, such as `chi update` in a terminal, now gets the
|
|
15
|
+
same unattended restarts as one chi web installed itself: idle workers and chi web move onto it within about 10
|
|
16
|
+
minutes once no turn runs, and the phone hears "chi updated to X". The page offers no **Restart** buttons meanwhile,
|
|
17
|
+
unless the restart failed or has waited an hour; `update.restart: never` keeps them.
|
|
18
|
+
[docs/configuration.md](docs/configuration.md#unattended-updates-updateauto)
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- An automatic update's sheet no longer offers **Restart workers** and **Restart chi web** beside a run already doing
|
|
23
|
+
both, and its worker restarts no longer stack sticky toasts unless one fails.
|
|
24
|
+
|
|
25
|
+
## [0.146.0] - 2026-11-03
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- `chi memory usage [--scope project|system] [--top N] [--format text|json]` reports what the memories
|
|
30
|
+
cost, counted from the saved sessions: each scope's index tokens against `memory.index_warn_tokens` and
|
|
31
|
+
how many of its lines name a memory no session read, the memories read most by tokens with the share they
|
|
32
|
+
make of all read tokens, and the user's own memories no saved session read. It reads only — no grades, no
|
|
33
|
+
advice to delete, nothing written. [docs/memory.md](docs/memory.md) has the details.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- A `delegate` child started with no `cwd:` whose task names another checkout of the repository gets a
|
|
38
|
+
`Warning:` line in the tool result naming that checkout and how to start the child there; the child still
|
|
39
|
+
runs in the parent's folder. [docs/sessions.md](docs/sessions.md#delegating)
|
|
40
|
+
- The session option on a file ask says what it covers: "Allow edits to config.yml for the session"
|
|
41
|
+
rather than "Allow this call for the session".
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- The session bar's status chip follows a question or approval card the moment it is answered,
|
|
46
|
+
instead of holding "waiting" until the next tick while the turn is already running again.
|
|
47
|
+
- A parent agent may give Allow once on a delegate child's `child-boundary` ask under
|
|
48
|
+
`guardrails.parent_answers: soft`. It was refused with a false "changes chi's own config"; an ask
|
|
49
|
+
that really names chi's own files stays yours.
|
|
50
|
+
- A parent agent's Deny on a child's approval says so: the child read "The user declined this call".
|
|
51
|
+
- A delegate child's second report in one parent turn reaches the parent. A reply that came after a
|
|
52
|
+
reported question, while the parent's turn was still running, was lost with nothing left to wake it.
|
|
53
|
+
|
|
54
|
+
Update with `chi update`.
|
|
55
|
+
|
|
10
56
|
## [0.145.0] - 2026-11-02
|
|
11
57
|
|
|
12
58
|
### Changed
|
|
@@ -5549,7 +5595,9 @@ and long-lived sessions.
|
|
|
5549
5595
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
5550
5596
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
5551
5597
|
|
|
5552
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
5598
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.147.0...HEAD
|
|
5599
|
+
[0.147.0]: https://github.com/dm1try/samagotchi/compare/v0.146.0...v0.147.0
|
|
5600
|
+
[0.146.0]: https://github.com/dm1try/samagotchi/compare/v0.145.0...v0.146.0
|
|
5553
5601
|
[0.145.0]: https://github.com/dm1try/samagotchi/compare/v0.144.0...v0.145.0
|
|
5554
5602
|
[0.144.0]: https://github.com/dm1try/samagotchi/compare/v0.143.0...v0.144.0
|
|
5555
5603
|
[0.143.0]: https://github.com/dm1try/samagotchi/compare/v0.142.0...v0.143.0
|
data/docs/architecture.md
CHANGED
|
@@ -335,9 +335,10 @@ reload in a toast, since an open tab keeps its old JS across a `chi web` upgrade
|
|
|
335
335
|
`installed`, the newest chi on this machine (`InstalledVersions`: the highest `samagotchi-<v>.gemspec`
|
|
336
336
|
in RubyGems' specification dirs, or a checkout's `version.rb` on disk; looked at again on each full
|
|
337
337
|
probe, every 10 s). A `chi` frame and the `GET /api/events` snapshot carry the full state
|
|
338
|
-
`{version, installed, available, update_blocked, auto, restart}` (`auto`: `update.auto`, `off` or `install`, which the page's
|
|
339
|
-
"available" wording follows; `restart`: `update.restart
|
|
340
|
-
|
|
338
|
+
`{version, installed, available, update_blocked, auto, restart, auto_offers}` (`auto`: `update.auto`, `off` or `install`, which the page's
|
|
339
|
+
"available" wording follows; `restart`: `update.restart`; `auto_offers`: the version AutoUpdate says the page should offer
|
|
340
|
+
the restarts for. With `auto: install`, `restart: idle` and no block the page offers no Restart toast for a newer installed
|
|
341
|
+
chi, however it got installed, unless `auto_offers` names it); `/api/info` and the session-command wake response carry the same fields. `SessionHub#update_available` publishes the checker result under the hub's
|
|
341
342
|
Monitor, but fetching never holds it.
|
|
342
343
|
|
|
343
344
|
`Web::UpdateCheck` runs its own thread beside the hub. When `update.notify: on` (the default) or
|
|
@@ -385,7 +386,14 @@ is `update.auto: install`. Every 5 minutes (the first step 60 s after start) it
|
|
|
385
386
|
and installs it through `UpdateOps#install(wait: true, auto: true)` when the install isn't blocked, no operation
|
|
386
387
|
runs, no hub summary has `status: running` or a `pending_question`, and the version isn't in `auto_failed` or
|
|
387
388
|
already in the `auto` record (one automatic try per version). Its frames carry `auto: true` (the page: "Automatic
|
|
388
|
-
chi update in progress", "(automatic)" in the sheet's title
|
|
389
|
+
chi update in progress", "(automatic)" in the sheet's title, no restart buttons in the sheet, no toast for a worker pass
|
|
390
|
+
unless it fails). A newer chi installed some other way (`AutoUpdate#adopt_installed`: `update.restart: idle`, the same gate, no
|
|
391
|
+
available release to install first, none on record for that version) is **adopted** once seen for `SETTLE` (60 s, in
|
|
392
|
+
practice the next step, as `chi update` merges the bundles after installing the gem and leaves no lock to look at): its
|
|
393
|
+
record starts at `phase: installed` and the run goes on as below. Ahead of every gate each step calls
|
|
394
|
+
`SessionHub#auto_offer_restart` (`auto_offers`, in memory) when a run on record for the newest installed chi is closed with
|
|
395
|
+
this chi web still older, or that chi has waited `HELD_FOR` (1 h): the trying goes on, and the page has its buttons back;
|
|
396
|
+
`restart_failed` and a failed install call it at once. Its record is `update_check.json`'s `auto`:
|
|
389
397
|
`{version, from, at, kept, phase: installed|workers|web|retrying|done|failed, workers: {restarted, left}, detail, pushed}`,
|
|
390
398
|
in Ruby the value object `Web::AutoUpdateRecord`, which owns that shape (`from_h`, `to_store`).
|
|
391
399
|
With `update.restart: never` the run ends at the install (`chi_notice` `installed`). With `idle` it goes on
|
|
@@ -395,7 +403,8 @@ the worker's own atomic refusal still applies); then, with no
|
|
|
395
403
|
turn running, `Server.restart_preflight` (the restart's command with `--version`, 20 s, killed on a timeout; it must
|
|
396
404
|
print a chi at least the release) and, if it passes, the record's `phase: web` is written **before**
|
|
397
405
|
`UpdateOps#restart_web`. A failed preflight, or a refused restart, publishes `restart_failed` and ends the run
|
|
398
|
-
(`failed`). `
|
|
406
|
+
(`failed`). A run at `installed` or `workers` that finds this chi web already on its version (restarted by hand from the
|
|
407
|
+
page's offer) goes straight to the finish. `AutoUpdate#stop` waits `STOP_WAIT` (25 s, longer than the preflight's timeout) for its thread, and a
|
|
399
408
|
preflight that ends after a stop leads to nothing (no `web`, no exec): the next start takes the run up again. The restarted chi web's AutoUpdate (first step after 5 s) finds `phase: web`, checks it runs the
|
|
400
409
|
release, counts the workers once more and publishes `updated`, then `done`, or `retrying` while workers are left. Those are
|
|
401
410
|
retried each step until none is left or 24 h (`RETRY_FOR`) have passed since the install, under the same filter, and from
|
data/docs/cli.md
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
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))
|
|
31
31
|
- `chi --remote NAME send|answer|note|context|sessions|share|review …` (or `CHI_REMOTE=NAME`) — those commands on that machine's sessions, through its chi web (see [Remote sessions](#remote-sessions))
|
|
32
32
|
- `chi --remote NAME --attach ID` (`--resume ID`, `--dir DIR [-p TEXT]`) — the attached TUI on a session of that machine (see [Remote sessions](#remote-sessions))
|
|
33
|
-
- `chi memory ls [--scope project|system] [--format text|json]` — the memories of both scopes (this folder's project first), one row each: scope, name, size, date, `from <bundle>`, the index description and its model overlays; `chi memory rm NAME [--scope project|system]` removes one as `memory_write` with `remove: true` does: its file and model overlays go to the bundle trash (the line names the dir; `chi bundle trash` lists and empties it) and its index line is dropped. NAME in both scopes without `--scope` exits 2 naming both; a missing NAME or a bundle's memory (`chi bundle uninstall` removes it) exits 1
|
|
33
|
+
- `chi memory ls [--scope project|system] [--format text|json]` — the memories of both scopes (this folder's project first), one row each: scope, name, size, date, `from <bundle>`, the index description and its model overlays; `chi memory rm NAME [--scope project|system]` removes one as `memory_write` with `remove: true` does: its file and model overlays go to the bundle trash (the line names the dir; `chi bundle trash` lists and empties it) and its index line is dropped. NAME in both scopes without `--scope` exits 2 naming both; a missing NAME or a bundle's memory (`chi bundle uninstall` removes it) exits 1. `chi memory usage [--scope project|system] [--top N] [--format text|json]` reports what the memories cost over the saved sessions: each scope's index tokens and how many of its lines name a memory nobody read, the memories read most by tokens, and the user's own memories no saved session read (`--top` caps the rows per section, 10; no sessions says so and exits 0) — see [Memory: What the memories cost](memory.md#what-the-memories-cost)
|
|
34
34
|
- `chi desktop install|upgrade|uninstall|status` — the macOS "Send to chi" helper: a Service and a ⌃⌥⌘N hotkey that send text or images to a session (live, stopped or new) as a message (⏎, a turn runs) or a context note (⌘⏎) (see [Desktop helper](desktop.md))
|
|
35
35
|
- `chi models [--format text|json] [--timeout S] [TEXT]` — list the models every configured host offers, as the names `--model` takes (see [Listing the models](#listing-the-models))
|
|
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)
|
|
@@ -267,7 +267,9 @@ done
|
|
|
267
267
|
in place, the notice gives the blocker and its command instead.
|
|
268
268
|
With `update.auto: install` chi web installs the release by itself once no
|
|
269
269
|
turn runs anywhere, then restarts idle workers and itself (`update.restart`),
|
|
270
|
-
and the notice reads "installs by itself" with no **Update** button
|
|
270
|
+
and the notice reads "installs by itself" with no **Update** button. A newer
|
|
271
|
+
chi installed from a terminal gets those restarts too, and no **Restart**
|
|
272
|
+
buttons unless the restart failed or has waited an hour; see
|
|
271
273
|
[Unattended updates](configuration.md#unattended-updates-updateauto).
|
|
272
274
|
|
|
273
275
|
- **The chi home.** With a [chi home](configuration.md#the-chi-home), a `chi
|
|
@@ -355,7 +357,8 @@ done
|
|
|
355
357
|
terminal, no reminder) and `session.idle_exit_minutes` pass (never with 0).
|
|
356
358
|
A new or woken worker always starts on the newest chi installed. A running
|
|
357
359
|
`chi web` (on the port it was started with, else `web.port`) on an older chi needs a restart (Ctrl-C, then `chi web`):
|
|
358
|
-
its page says so too.
|
|
360
|
+
its page says so too. With `update.auto: install` it restarts by itself once no turn
|
|
361
|
+
runs ([Unattended updates](configuration.md#unattended-updates-updateauto)). One from before workers started on the newest chi
|
|
359
362
|
also starts its new sessions on its own version until then (the row says so).
|
|
360
363
|
|
|
361
364
|
`--dry-run` shows the table with "would update" and changes nothing. It is
|
data/docs/configuration.md
CHANGED
|
@@ -2163,9 +2163,22 @@ Both are read when chi web starts (restart it to apply a change). What it does,
|
|
|
2163
2163
|
([Phone notifications](#phone-notifications-web-push)); without it the page's toasts and
|
|
2164
2164
|
`$XDG_STATE_HOME/samagotchi/update_check.json` (`auto`) are the record.
|
|
2165
2165
|
|
|
2166
|
+
**A chi installed some other way** (`chi update` in a terminal, a release newer than `update.min_age_hours` allows, a
|
|
2167
|
+
hand `gem install`) gets steps 3 and 4 too, with `update.restart: idle` and an install that isn't blocked: chi web notices it,
|
|
2168
|
+
waits for the next 5-minute look (`chi update` merges the bundles after installing the gem) and for the quiet moment of
|
|
2169
|
+
step 1, then restarts as above, so chi web is on it within about 10 minutes when nothing runs. A release it can install
|
|
2170
|
+
goes first. One run per version here too: a restart that didn't take isn't tried again.
|
|
2171
|
+
|
|
2172
|
+
The page offers no **Restart** buttons while chi web is going to restart by itself. They come back, as a toast, when it
|
|
2173
|
+
won't (a failed run) or when something has held it for an hour (a forgotten question, a long turn); it keeps trying behind
|
|
2174
|
+
them. Worker restarts in such a run show no toast unless one fails, and the sheet behind **View** has only **Close**;
|
|
2175
|
+
the session's own **Restart** in its bar is always there.
|
|
2176
|
+
|
|
2166
2177
|
There is no watchdog: chi web restarts into the newest chi it just proved can start, and nothing brings an old one
|
|
2167
|
-
back by itself. To roll back,
|
|
2168
|
-
|
|
2178
|
+
back by itself. To roll back, set `update.auto: off` (or the next check installs the release again), uninstall the newer
|
|
2179
|
+
gem with the chi home's Ruby (`gem uninstall samagotchi -v Y`: chi always starts the newest one installed, so installing an
|
|
2180
|
+
older gem beside it changes nothing), and restart chi web. `chi _X_ web` stays on X only with `update.auto: off`: a restart
|
|
2181
|
+
starts the newest. `chi update --to` refuses a version that isn't newer than the running chi. The desktop helper isn't rebuilt (that
|
|
2169
2182
|
stays a manual `chi update`).
|
|
2170
2183
|
|
|
2171
2184
|
## All settings
|
|
@@ -2288,7 +2301,7 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
|
|
|
2288
2301
|
| `update.bundles` | `true` | | `false`: `chi update` leaves the shipped bundles to `chi bundle upgrade` (`--no-bundles`). |
|
|
2289
2302
|
| `update.desktop` | `true` | | `false`: `chi update` leaves the desktop helper alone (`--no-desktop`). |
|
|
2290
2303
|
| `update.notify` | `on` | | `on`: chi web checks RubyGems once a day and shows aged stable releases newer than both this chi and the newest installed chi; `off`: no thread or network check. |
|
|
2291
|
-
| `update.auto` | `off` | | `install`: chi web installs the release its checker found (aged by `update.min_age_hours`) once no turn runs and no question waits in any session, as the **Update** button would (shipped bundles merged, your edits kept and reported), then pushes how it went; `update.restart` says what follows. An install that fails is tried once per version (a newer release, or the **Update** button, tries again). The check runs even with `update.notify: off` (which then only silences the page's and the phone's "available"). An install that can't run here (a checkout, Bundler, an unwritable gem dir) stays a notice. `off`: notice only. See [Unattended updates](#unattended-updates-updateauto). |
|
|
2304
|
+
| `update.auto` | `off` | | `install`: chi web installs the release its checker found (aged by `update.min_age_hours`) once no turn runs and no question waits in any session, as the **Update** button would (shipped bundles merged, your edits kept and reported), then pushes how it went; `update.restart` says what follows, also for a chi installed some other way (`chi update` in a terminal). An install that fails is tried once per version (a newer release, or the **Update** button, tries again). The check runs even with `update.notify: off` (which then only silences the page's and the phone's "available"). An install that can't run here (a checkout, Bundler, an unwritable gem dir) stays a notice. `off`: notice only. See [Unattended updates](#unattended-updates-updateauto). |
|
|
2292
2305
|
| `update.restart` | `idle` | | With `update.auto: install`: `idle` restarts the stale workers whose session is idle and untouched for 10 minutes, then chi web itself, once no turn runs anywhere and the newest chi has answered `chi --version`; it pushes "chi updated to X". `never`: the install only (the page's toast offers the restarts, workers also exit on their own when idle). |
|
|
2293
2306
|
| `update.check_hours` | `24` | | Hours between RubyGems checks (at least 1). The first check starts about 30 seconds after chi web starts. |
|
|
2294
2307
|
| `update.min_age_hours` | `24` | | A release must be this old before chi web offers it; `0` disables the age delay. |
|
data/docs/guardrails.md
CHANGED
|
@@ -81,7 +81,8 @@ Who answers:
|
|
|
81
81
|
|
|
82
82
|
The model gets one line on a deny. A rule's or hook's deny reads
|
|
83
83
|
`[execute] Error: denied by guardrail (rule git-push, bundle guardrails): git push publishes commits. The user was not asked. Do not retry it or reach the same result another way; ask the user how to proceed.`
|
|
84
|
-
When the user picks Deny on an ask, it leads with the user's answer
|
|
84
|
+
When the user picks Deny on an ask, it leads with the user's answer (a parent
|
|
85
|
+
agent's Deny says `The parent agent declined this call`):
|
|
85
86
|
`[execute] Error: The user declined this call: "open a PR instead". It needed approval (rule git-push, bundle guardrails): git push publishes commits. Do not retry it or reach the same result another way; ask the user how to proceed.`
|
|
86
87
|
|
|
87
88
|
## Approvals
|
|
@@ -91,7 +92,7 @@ Allowing beyond "once" is stored in `$XDG_STATE_HOME/samagotchi/guardrails/appro
|
|
|
91
92
|
|
|
92
93
|
| Scope | Allows |
|
|
93
94
|
|---|---|
|
|
94
|
-
| session | this exact call
|
|
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
96
|
| repo | this exact call in this repo (the cwd outside a repo), any session |
|
|
96
97
|
| rule | anything this rule asks about in this repo |
|
|
97
98
|
|
|
@@ -234,6 +235,10 @@ other user-only rules (`chi-context-cmd`, `chi-broadcast`,
|
|
|
234
235
|
`prompt-memory-write`, `model-overlay-write`, and `chi-answer-piped` /
|
|
235
236
|
`chi-answer-http`, which answer a grandchild's question as the user).
|
|
236
237
|
|
|
238
|
+
`child-boundary` isn't one of them: an ask before a delegate child acts outside
|
|
239
|
+
its folder follows the level, so a `soft` parent may give Allow once. If that
|
|
240
|
+
call names chi's own files, it is the user's as above.
|
|
241
|
+
|
|
237
242
|
A plugin's or a hook's question (`ask_user`) tags its options the same way
|
|
238
243
|
(`effects:`, see [Plugins](plugins.md)); an untagged one is a plain `answer`.
|
|
239
244
|
|
data/docs/internals/bridge.md
CHANGED
|
@@ -66,7 +66,9 @@ the "asking again" and steer-cut rows, ✂ rows and questions (with how they wer
|
|
|
66
66
|
One per `GET /session/:id/stream` connection, and itself that connection's observer: `Engine#emit_event` calls
|
|
67
67
|
`#call` on the turn's thread, so it only pushes onto a `BoundedQueue` (when full, the oldest entry goes and an
|
|
68
68
|
overflow flag is set). The connection's own thread (`#serve!`) drains the queue onto the socket, so a slow client
|
|
69
|
-
cannot stall a turn or another subscriber.
|
|
69
|
+
cannot stall a turn or another subscriber. An event holding bytes JSON can't take goes out scrubbed
|
|
70
|
+
(`SSEWriter#frame_json`, `LLM::Utf8Scrub`): a raise there would end this stream, and every later one for as long as the
|
|
71
|
+
ring holds the event. `serve!` orders a join so that nothing is missed or doubled:
|
|
70
72
|
|
|
71
73
|
1. subscribe the live queue first;
|
|
72
74
|
2. capture `event_count` (the snapshot seq);
|
data/docs/internals/engine.md
CHANGED
|
@@ -805,7 +805,7 @@ Both loops:
|
|
|
805
805
|
guessed from the block and it never enters the history (`:generation_completed`
|
|
806
806
|
and `:after_generation` have already seen the text): `:generation_retrying` with `restarted` voids the streamed text, `:empty_answer_retry` with
|
|
807
807
|
`malformed` goes out, and a hidden `TurnNote.malformed_retry` asks again at the retry temperature. It has its own budget, so `retry.empty_answer: 0`
|
|
808
|
-
doesn't turn it off, and a real tool call resets it. Two in a row fail the turn with `LLM::MalformedGeneration`, keeping its earlier steps. Each is
|
|
808
|
+
doesn't turn it off, and a real tool call resets it. Two in a row fail the turn with `LLM::MalformedGeneration`, keeping its earlier steps; a Stop that lands on a stray reply ends the turn cancelled, not failed, because a stopped turn skips this path. Each is
|
|
809
809
|
logged as `generation_malformed` with the shape, finish reason, served model and the text's tail. `KernelLoop` has its own path (`ToolCallParser#malformed`).
|
|
810
810
|
- **Empty-answer retry** (`EmptyAnswerRetry`): a generation with no visible text and no tool calls is asked
|
|
811
811
|
again, up to `retry.empty_answer` times (at most 3), with a hidden nudge (`TurnNote.empty_retry`) on the tail.
|
|
@@ -23,7 +23,7 @@ page. A factory dep a new version stops passing is the same hazard the other way
|
|
|
23
23
|
out of `send_turn.js`, `session_actions.js` and `lists_view.js`); a page loaded before keeps all its old modules
|
|
24
24
|
until a reload, so the risk is a mixed cache, not an open tab. `answer_fold.js`, `details_motion.js`,
|
|
25
25
|
`turn_range.js`, `history_reveal.js`, `latest.js`, `session_payload.js`, `method_api.js`, `session_store.js`,
|
|
26
|
-
`session_fields.js`, `feature_gates.js`, `revise_stack.js`, `revise_switch.js`, `shell_command.js`, `stale_params.js`, `popover_modal.js` and `
|
|
26
|
+
`session_fields.js`, `feature_gates.js`, `revise_stack.js`, `revise_switch.js`, `shell_command.js`, `stale_params.js`, `popover_modal.js`, `more_sheet.js`, `hash_intent.js` (what the hash asks for: session, all, start or keep) and `worker_features.js` are new modules for that reason.
|
|
27
27
|
|
|
28
28
|
## Two pages, two models
|
|
29
29
|
|
|
@@ -152,7 +152,8 @@ restart's hub event that brings the feature enables it, the worker going (`worke
|
|
|
152
152
|
it's unavailable. `forgetSession` and `renderSessionView` draw the history with the answer already, so they `prime()`
|
|
153
153
|
the gates in the batch of their write, and nothing fires. Today two gates: `verify` (Verify fresh on a verdict's
|
|
154
154
|
card, `redrawFresh`) and `revise` (the answers' Revise buttons and the composer's Revising chip, `redrawRevise`).
|
|
155
|
-
Both refuse only a live worker without the feature
|
|
155
|
+
Both refuse only a live worker without the feature — `workerPredates(features, up, feature)` in
|
|
156
|
+
`worker_features.js`, the rule both gates share: a worker that isn't up is woken by the send as a new one, which
|
|
156
157
|
has it. A new action that needs a worker feature is one more entry.
|
|
157
158
|
|
|
158
159
|
### Exceptions
|
data/docs/memory.md
CHANGED
|
@@ -229,6 +229,8 @@ or trims them; chi measures them (tokens estimated as characters /
|
|
|
229
229
|
`system ~2.0k tokens (54 lines), project ~1.4k tokens (35 lines)`, with
|
|
230
230
|
`, over 2500` inside the parentheses for a scope over
|
|
231
231
|
`memory.index_warn_tokens`.
|
|
232
|
+
- `chi memory usage` counts what the memories cost over the saved sessions: each scope's index tokens,
|
|
233
|
+
the memories read most, and the ones no session read (see [What the memories cost](#what-the-memories-cost)).
|
|
232
234
|
- `/stats` has `memory index: ~3.4k tokens in this session's prompt (system 2.0k, project 1.4k)`,
|
|
233
235
|
and the web's ctx tooltip (info bar and session card) the same line. It is
|
|
234
236
|
what this session's prompt holds, measured when the prompt was built (after
|
|
@@ -282,6 +284,49 @@ every read. `chi self` has a `big memories` row instead: the memories over the l
|
|
|
282
284
|
`config_modification_protocol` is over on every install; the fix is upstream's). This is how a
|
|
283
285
|
memory already over the limit gets seen.
|
|
284
286
|
|
|
287
|
+
## What the memories cost
|
|
288
|
+
|
|
289
|
+
`chi memory usage [--scope project|system] [--top N] [--format text|json]` answers
|
|
290
|
+
"where do my memories cost the most?" from the saved session files. It reads only: no
|
|
291
|
+
writes, no model call, no grades, and no advice to delete anything. `chi self`'s `big memories`
|
|
292
|
+
row still lists memories over `memory.entry_warn_tokens`; this is the other half — cost is
|
|
293
|
+
size times reads, and a memory of 2k tokens read fifty times costs more than one of 8k read once.
|
|
294
|
+
|
|
295
|
+
- **The header** says how many sessions were counted and the days they span, because retention
|
|
296
|
+
limits what can be seen: 14 days and 500 sessions by default, so "not read" means "not read
|
|
297
|
+
in the sessions still saved". Archived sessions count; test runs and sessions with no user
|
|
298
|
+
message do not, and the header says how many of those were left out. With nothing counted at
|
|
299
|
+
all, the line says what is in the dir instead of "nothing saved": the files that could not be
|
|
300
|
+
read and the sessions not counted.
|
|
301
|
+
- **The indexes come first**, since both are sent with every prompt: their tokens (the same
|
|
302
|
+
figure `chi self` and `/stats` show), the `memory.index_warn_tokens` limit, and how many of
|
|
303
|
+
their *memory* lines name a memory no counted session read. The index's heading and a bold
|
|
304
|
+
line of prose are not memory lines, so they sit in neither the count nor its denominator.
|
|
305
|
+
- **Most read, by tokens**: name, tokens, reads, the memory's size now (`-` when the line has no
|
|
306
|
+
file behind it), its scope, and `from <bundle>` for a bundle's memory (which the user cannot
|
|
307
|
+
tighten in place). The header of the section says what share the shown rows make of all read
|
|
308
|
+
tokens (`--top` caps the rows, 10 by default, at most 1000). If every read is of a name with
|
|
309
|
+
no memory now, the section says that instead of listing rows.
|
|
310
|
+
- **Not read in saved sessions**: the count of the user's own memories no counted session read,
|
|
311
|
+
then the largest of them. A bundle's memories are not listed, and a memory the session was
|
|
312
|
+
started with (`--memory`, or config.yml's `memories:`) counts as used: the model has it in the
|
|
313
|
+
prompt without a read. A line says an index line is paid whether or not the memory is read, so
|
|
314
|
+
a never-read memory still costs its line.
|
|
315
|
+
- Names read that no longer have a memory behind them are counted in one line, not as rows.
|
|
316
|
+
|
|
317
|
+
A read is a `memory_read`, or a `read` of a memory's file, in either loop's saved shape (the chat
|
|
318
|
+
loop's calls paired by `tool_call_id`, the native loop's call markup with one joined result). Its
|
|
319
|
+
cost is the size of the result as it was saved — a cut result counts as it happened, a model overlay
|
|
320
|
+
too, and `read`ing an overlay's file is a read of the memory it overlays — in tokens as the prompt
|
|
321
|
+
estimates them (characters / `context.chars_per_token`). A comma list of names splits that one result
|
|
322
|
+
by share, over every name asked for: `index` is not a row, but it is one of the names the share is
|
|
323
|
+
worked out over. A project memory is credited only with the reads of
|
|
324
|
+
sessions of this folder's project (worktrees of one repository share it); a system memory with every
|
|
325
|
+
session's, whichever project they came from.
|
|
326
|
+
|
|
327
|
+
Reads saved before whole reads hold cut results; when any counted read does, one line under "Most
|
|
328
|
+
read" says so, since a big memory may cost more per read now than those numbers show.
|
|
329
|
+
|
|
285
330
|
## Skills
|
|
286
331
|
|
|
287
332
|
A **skill** is a memory named `skill_<name>` that holds the steps of a
|
data/docs/sessions.md
CHANGED
|
@@ -452,7 +452,7 @@ mise.toml pins Ruby 3.4 gets that `ruby`, `gem` and `bundle` in its tools while
|
|
|
452
452
|
The `delegate` tool hands a task to a **child session**: an ordinary chi session in a worker of its own, started in the parent's `working_directory` with the task, verbatim, as its first user message, on the parent's model unless the call names one (`model:` takes a name or an alias, resolved as `--model` is; an unknown host comes back as the tool's `Error:` line; an id the host's saved model list doesn't have starts the child with a `Warning: host 'h' doesn't list model 'x' (did you mean: …?)` line in the result). Children run in parallel and only their **final reply** comes back to the parent: the child's newest `output/<timestamp>.txt` file, which the worker writes at the end of each turn that produced visible text, cut head-and-tail above 64 KB (`DelegateWait.cut`); a reply longer than `max_tool_output_chars` keeps its start and its end (the conclusion), with a gap line naming that `output/` file, where the whole reply is (`ToolOutputCut`'s `:head_tail_ref`; see [Tool output guardrails](internals/tool-output-limits.md#the-runners-cut-max_tool_output_chars)). Nothing of the child's trace enters the parent's context. Nothing is hidden: a child shows in `chi sessions list` (`↳ <parent>`), the web (folded into its parent's card on the strip and in all sessions, whose `▸ N delegates` chip lists the delegates at any depth, see Web UI above; a card of its own with a `↳` chip when its parent isn't listed; `↑ <parent id>` in its info bar, its tooltip `delegated by session …`; the parent's info bar carries a `⑂ N` chip counting its delegates, with how many run, wait, are done, failed, stopped or idle in the tooltip and the waiting ones named in the attention colour; a click opens all sessions), the terminal's status row (a parent's `⑂ 2 running · 1 waiting`, see [Status Line](cli.md#status-line)) and the `list_sessions` tool (`child`; the parent shows as `parent` in the child's own list); the user can `chi --attach <child id>` and steer it mid-run, and `chi send` and `send_note` reach it like any session.
|
|
453
453
|
|
|
454
454
|
- `delegate(task, model:, session:, cwd:, wait:, timeout:)`: starts a child and, by default, waits for its reply. `wait: false` is the usual way for work that takes a while, several children side by side, or to keep talking with the user: it returns at once, the parent ends its turn or works on, and the reply comes back by itself as a *delegate report* (below), in a turn of its own if the parent is idle. The tool descriptions tell the model so, and to wait (`wait: true`, `delegate_result`) only when its turn can't go on without the reply. `timeout`, default 600 s, leaves a slow child running; 0 checks once without waiting. With `session:` (a child's id or prefix) the task goes to that child as a follow-up instead, delivered like `chi send` (client id `delegate:<parent short id>`, shown as `delegate>` in the child's UIs); only this session's children take one. The wait (`wait: true`, a later `delegate_result`) and the delegate report are for the reply of the child's turn the follow-up went to: its running turn, which it merges into, or the turn of its own it runs when the child is idle (`turn` in `delegates.json`, set when the child's worker names `turn_ids`), not a newer reply of a turn queued behind it. A follow-up merged as a steer into a turn the child started at that moment (a wake, a reminder, another message) gets that turn's reply or question. A child on its first turn, or with an older worker, gives its next reply, as before.
|
|
455
|
-
- `cwd:` starts the child in another folder of the parent's repository: a linked worktree (`git worktree add ../<repo>-<task> -b <branch>`, which the model runs with `execute` first, under guardrails; chi runs no git here) or a subfolder; a relative path is taken from the parent's folder. The folder must exist, have the parent's project root (the same common git dir, `MemoryPaths.project_root`) and be in a work tree: a git dir (`.git`, `.git/objects`, a bare layout's `.bare`) or a bare layout's container is refused (`git rev-parse --is-inside-work-tree`, read-only, decides). Outside git only a subfolder of the parent's folder is allowed. Otherwise the tool returns `Error: cwd must be a folder of this session's repository …` or `… is not in a work tree …`. A follow-up (`session:`) refuses `cwd:`: the child keeps its folder. The child's guardrails measure from its own worktree, and project memories, `list_sessions` and the web's project are the same as the parent's.
|
|
455
|
+
- `cwd:` starts the child in another folder of the parent's repository: a linked worktree (`git worktree add ../<repo>-<task> -b <branch>`, which the model runs with `execute` first, under guardrails; chi runs no git here) or a subfolder; a relative path is taken from the parent's folder. The folder must exist, have the parent's project root (the same common git dir, `MemoryPaths.project_root`) and be in a work tree: a git dir (`.git`, `.git/objects`, a bare layout's `.bare`) or a bare layout's container is refused (`git rev-parse --is-inside-work-tree`, read-only, decides). Outside git only a subfolder of the parent's folder is allowed. Otherwise the tool returns `Error: cwd must be a folder of this session's repository …` or `… is not in a work tree …`. A follow-up (`session:`) refuses `cwd:`: the child keeps its folder. The child's guardrails measure from its own worktree, and project memories, `list_sessions` and the web's project are the same as the parent's. With no `cwd:` and a task that names another checkout of the same repository (a path in the task belonging to a linked worktree, read from the files with `Guardrails::GitInfo.checkouts`, no git process), the result carries `Warning: this child runs in <folder>, but its task names <checkout>. If it should work there, stop it (chi sessions stop <id>) and call delegate again without session, with cwd: <checkout>.` One line however many are named; nothing for a path of the child's own checkout, a folder that isn't one, or a path with spaces. The child starts as it would anyway, and with `wait: true` (the default) the line comes after the child ran, so it only helps with `wait: false`. A `Warning:` for an unknown model id comes first, then this one. `Log.info(:worker, "delegate_other_checkout", parent:)` records each line.
|
|
456
456
|
- `delegate_result(session:, timeout:)`: waits for a child's next reply: the one named, or the newest running child (`timeout` as for `delegate`: 0 checks once and returns at once). The reply comes back once; a later call waits for the next one. After `wait: false` it isn't needed: the report brings the reply.
|
|
457
457
|
- A wait also ends early when a person's message waits for the parent's turn's next step (the `task_wait` rule: `Engine#input_waiting?`, `SessionInbox.steer_waiting?`, never in a participant's turn): `ReplyWait`'s `message_waiting:` probe gives `:message_waiting`, checked after every outcome the child can arrive at (a reply, a question, an end, a gone worker) and a cancel, before the deadline, and the result says `no reply yet: a message from the user is waiting; the child keeps running. Read the message first.` with the same last line as a timeout's. The child is never touched, the cursor doesn't move (the reply still comes, as a report or at the next `delegate_result`), a relayed approval's outcome line stays in the result, and a reserved report reads as for a timeout. A delegate child's report doesn't end a wait. This holds for `delegate`'s own wait too (both go through `DelegateWait`).
|
|
458
458
|
- A wait that timed out or was canceled still gets its delegate report: neither moves the parent's cursor, so the child's reply comes as a report when its turn ends. The result says so (`chi brings its reply here by itself as a delegate report …`; with `queue`, that it joins the next step or turn) and that `delegate_result` waits again only if the turn needs the reply now. A parent that gets no reports (`session.delegate_reports: off`, a `--no-shared` REPL, `--non-interactive`) is told `delegate_result <id> waits again` instead, and `wait: false`'s result says `delegate_result waits for its reply`.
|
|
@@ -312,12 +312,12 @@ module Samagotchi
|
|
|
312
312
|
own + strays
|
|
313
313
|
end
|
|
314
314
|
|
|
315
|
-
# A second ring for a child taken earlier in this turn:
|
|
316
|
-
#
|
|
315
|
+
# A second ring for a child taken earlier in this turn: this report's
|
|
316
|
+
# cursors (#report_for), both reports' rings.
|
|
317
317
|
def merge_taken(earlier, report)
|
|
318
318
|
return report unless earlier
|
|
319
319
|
|
|
320
|
-
report.with(
|
|
320
|
+
report.with(rings: earlier.rings + report.rings)
|
|
321
321
|
end
|
|
322
322
|
|
|
323
323
|
# @return [Report, nil] nil when the child has nothing to report
|
|
@@ -325,7 +325,14 @@ module Samagotchi
|
|
|
325
325
|
child = Session.load(child_id, state_dir: @state_dir)
|
|
326
326
|
return nil unless child.parent_id == @session_id
|
|
327
327
|
|
|
328
|
-
|
|
328
|
+
current = Tools::DelegateCursors.get(@session_id, child_id, state_dir: @state_dir)
|
|
329
|
+
earlier = @taken[child_id]
|
|
330
|
+
# Until the turn commits, the file holds the cursor from before the
|
|
331
|
+
# first report, plus its claim. A second ring reads on from where that
|
|
332
|
+
# report left it, and a commit moves on from the file's. If the model
|
|
333
|
+
# moved the cursor meanwhile (delegate_result), the file's is the truth.
|
|
334
|
+
kept = earlier && current.without_claim == earlier.before.without_claim ? earlier : nil
|
|
335
|
+
before = kept ? kept.after : current
|
|
329
336
|
# A follow-up's turn (DelegateCursor#turn) is reported by its own
|
|
330
337
|
# reply, as delegate_result takes it, not a newer queued turn's.
|
|
331
338
|
wait = ReplyWait.call(child_id, state_dir: @state_dir, cursor: before.reply_file, baseline: before.baseline,
|
|
@@ -337,14 +344,14 @@ module Samagotchi
|
|
|
337
344
|
turn: wait.status == :waiting_for_answer ? before.turn : nil)
|
|
338
345
|
.with_baseline(ReplyWait.baseline_of(wait.session))
|
|
339
346
|
claim = Tools::ReportClaim.new(id: claim_id, file: wait.file, cursor: after)
|
|
340
|
-
return nil unless Tools::DelegateCursors.claim_report(@session_id, child_id, expected:
|
|
347
|
+
return nil unless Tools::DelegateCursors.claim_report(@session_id, child_id, expected: current, claim: claim,
|
|
341
348
|
state_dir: @state_dir)
|
|
342
349
|
|
|
343
350
|
# A reported end is the follow-up turn's too (DelegateCursor#turn):
|
|
344
351
|
# a later wait on it would repeat its reply.
|
|
345
352
|
after = after.with(report_claim: claim)
|
|
346
353
|
text = Tools::DelegateWait.finish(wait, child_id, timeout: 0, report: true)
|
|
347
|
-
Report.new(child_id: child_id, text: text, before: before, after: after, rings: rings, claim_id: claim_id,
|
|
354
|
+
Report.new(child_id: child_id, text: text, before: kept ? kept.before : before, after: after, rings: rings, claim_id: claim_id,
|
|
348
355
|
relay: relay?(wait) ? wait.question : nil)
|
|
349
356
|
rescue ArgumentError
|
|
350
357
|
nil # the child is gone
|
|
@@ -181,16 +181,17 @@ module Samagotchi
|
|
|
181
181
|
return verdict.settle!(:deny, decided_by: "user", note: "The approval was cancelled.")
|
|
182
182
|
end
|
|
183
183
|
|
|
184
|
+
who = answer.parent_agent? ? Verdict::PARENT_AGENT : "user"
|
|
184
185
|
index = answer.selected_indices.first
|
|
185
186
|
if index && index < scopes.size
|
|
186
|
-
verdict.settle!(:allow, decided_by:
|
|
187
|
+
verdict.settle!(:allow, decided_by: who)
|
|
187
188
|
verdict.scope = scopes[index]
|
|
188
189
|
return verdict
|
|
189
190
|
end
|
|
190
191
|
|
|
191
192
|
freeform = answer.freeform.to_s.strip
|
|
192
|
-
|
|
193
|
-
verdict.settle!(:deny, decided_by:
|
|
193
|
+
declined = "The #{who} declined this call"
|
|
194
|
+
verdict.settle!(:deny, decided_by: who, note: freeform.empty? ? "#{declined}." : "#{declined}: #{freeform.inspect}.")
|
|
194
195
|
end
|
|
195
196
|
|
|
196
197
|
# The rule's scopes; "rule" only when a rule id names what to approve.
|
|
@@ -206,12 +207,24 @@ module Samagotchi
|
|
|
206
207
|
place = verdict.targets&.repo_root ? "in this repo (#{verdict.targets.repo_name})" : "in this directory"
|
|
207
208
|
case scope
|
|
208
209
|
when "once" then ONCE
|
|
209
|
-
when "session" then "Allow
|
|
210
|
+
when "session" then "Allow #{session_subject(verdict.targets)} for the session"
|
|
210
211
|
when "repo" then "Allow this call #{place}"
|
|
211
212
|
when "rule" then "Allow rule #{verdict.rule} #{place}"
|
|
212
213
|
end
|
|
213
214
|
end
|
|
214
215
|
|
|
216
|
+
# What a session answer covers, as Approvals.key_for keys it: a file
|
|
217
|
+
# tool's later calls on the same paths, else this one exact call.
|
|
218
|
+
def session_subject(targets)
|
|
219
|
+
return "this call" if targets.nil? || targets.command || targets.paths.empty?
|
|
220
|
+
|
|
221
|
+
what = targets.paths.one? ? File.basename(targets.paths.first) : "#{targets.paths.size} files"
|
|
222
|
+
"#{SESSION_VERBS.fetch(targets.tool, "#{targets.tool} on")} #{what}"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# How a session label names a file tool's calls.
|
|
226
|
+
SESSION_VERBS = { "edit" => "edits to", "write" => "writes to" }.freeze
|
|
227
|
+
|
|
215
228
|
# How much of a plugin tool's arguments the question shows.
|
|
216
229
|
ARGS_CHARS = 300
|
|
217
230
|
|
|
@@ -40,6 +40,10 @@ module Samagotchi
|
|
|
40
40
|
# The guardrails bundle's ask before a memory removal: its paths are in
|
|
41
41
|
# the config dir, yet a parent may answer it.
|
|
42
42
|
MEMORY_REMOVE_RULE = "memory-remove"
|
|
43
|
+
# A delegate child's ask before it acts outside its folder. It carries
|
|
44
|
+
# the core source but isn't about chi's own files, so it follows the
|
|
45
|
+
# level like any other ask, unless it names them (#chi_files?).
|
|
46
|
+
CHILD_BOUNDARY_RULE = "child-boundary"
|
|
43
47
|
# The guardrails bundle's shell-touches-chi, for a word it can't
|
|
44
48
|
# resolve to a path (Guardrails::ShellPaths): CHI_TEXT and .git/hooks.
|
|
45
49
|
CHI_SHELL_TEXT = Regexp.union(CHI_TEXT, %r{\.git/(?:modules/\S+/)?hooks})
|
|
@@ -68,7 +72,7 @@ module Samagotchi
|
|
|
68
72
|
facts = ApprovalFacts.from_h(facts)
|
|
69
73
|
return false if facts.nil?
|
|
70
74
|
return true if PROTECTED_RULES.include?(facts.rule.to_s)
|
|
71
|
-
return true if facts.source.to_s == ProtectedPaths::SOURCE
|
|
75
|
+
return true if facts.source.to_s == ProtectedPaths::SOURCE && facts.rule.to_s != CHILD_BOUNDARY_RULE
|
|
72
76
|
# A memory's file sits in the config dir, but removing one is the
|
|
73
77
|
# memory-remove rule's ask, which a parent may answer.
|
|
74
78
|
return false if facts.rule.to_s == MEMORY_REMOVE_RULE
|
|
@@ -26,6 +26,8 @@ module Samagotchi
|
|
|
26
26
|
PROTECTED_RULES = %w[chi-config chi-home chi-shim chi-hooks shell-touches-chi chi-context-cmd chi-broadcast prompt-memory-write
|
|
27
27
|
model-overlay-write chi-answer-piped chi-answer-http].freeze
|
|
28
28
|
SCOPES = %w[once session repo rule].freeze
|
|
29
|
+
# Who answered an ask when it was a parent agent, not the user.
|
|
30
|
+
PARENT_AGENT = "parent agent"
|
|
29
31
|
DO_NOT_RETRY = "Do not retry it or reach the same result another way; ask the user how to proceed."
|
|
30
32
|
|
|
31
33
|
attr_reader :decision, :reason, :rule, :source, :scopes, :decided_by
|
|
@@ -118,12 +120,12 @@ module Samagotchi
|
|
|
118
120
|
|
|
119
121
|
# What the model gets for a deny (after the "[tool] Error: " prefix):
|
|
120
122
|
# who decided, why, and not to route around it (or the voter's own
|
|
121
|
-
# advice). A
|
|
122
|
-
# read as the rule refusing.
|
|
123
|
+
# advice). A deny on an ask leads with the answer, the user's or a
|
|
124
|
+
# parent agent's, so it doesn't read as the rule refusing.
|
|
123
125
|
def deny_text
|
|
124
126
|
reason = @reason.to_s.strip
|
|
125
127
|
reason += "." unless reason.empty? || reason.match?(/[.!?]\z/)
|
|
126
|
-
parts = if
|
|
128
|
+
parts = if ["user", PARENT_AGENT].include?(@decided_by) && @note
|
|
127
129
|
[@note, "It needed approval (#{decider}):", reason]
|
|
128
130
|
else
|
|
129
131
|
["denied by guardrail (#{decider}):", reason, @note || "The user was not asked."]
|