samagotchi 0.138.0 → 0.139.1
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 +44 -1
- data/docs/architecture.md +33 -7
- data/docs/cli.md +6 -2
- data/docs/configuration.md +60 -5
- data/docs/internals/background-tasks.md +3 -3
- data/docs/internals/engine.md +2 -1
- data/docs/internals/tool-guardrails.md +2 -2
- data/docs/internals/web-push.md +20 -0
- data/docs/sessions.md +5 -3
- data/lib/samagotchi/bundle_command.rb +21 -21
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/plugin.rb +8 -3
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/self_map.md +2 -2
- data/lib/samagotchi/cancellation_controller.rb +4 -4
- data/lib/samagotchi/config.rb +4 -0
- data/lib/samagotchi/context_fetch.rb +2 -2
- data/lib/samagotchi/cut.rb +21 -0
- data/lib/samagotchi/cut_policy.rb +6 -5
- data/lib/samagotchi/engine.rb +4 -2
- data/lib/samagotchi/engine_peers.rb +2 -1
- data/lib/samagotchi/hook_routes.rb +3 -2
- data/lib/samagotchi/kernel_loop.rb +8 -6
- data/lib/samagotchi/launch_mode.rb +5 -2
- data/lib/samagotchi/llm/chat_loop.rb +3 -2
- data/lib/samagotchi/llm/openai_chat.rb +1 -1
- data/lib/samagotchi/memory_bundle/status.rb +13 -3
- data/lib/samagotchi/project_env.rb +16 -2
- data/lib/samagotchi/reply_wait/poll.rb +9 -2
- data/lib/samagotchi/reply_wait.rb +9 -3
- data/lib/samagotchi/steer_cut.rb +2 -1
- data/lib/samagotchi/task_finish_note.rb +8 -5
- data/lib/samagotchi/task_watch.rb +11 -6
- data/lib/samagotchi/testing/boundary.rb +3 -1
- data/lib/samagotchi/tool_activity.rb +3 -3
- data/lib/samagotchi/tool_declarations.rb +2 -2
- data/lib/samagotchi/tools/builtins.rb +2 -1
- data/lib/samagotchi/tools/delegate_wait.rb +23 -2
- data/lib/samagotchi/tools/execute.rb +51 -9
- data/lib/samagotchi/tools/task_runtime.rb +1 -1
- data/lib/samagotchi/turn_note.rb +6 -6
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +30 -158
- data/lib/samagotchi/web/auto_update.rb +282 -0
- data/lib/samagotchi/web/public/app.js +6 -4
- data/lib/samagotchi/web/public/composer.css +1 -1
- data/lib/samagotchi/web/public/index.html +10 -9
- data/lib/samagotchi/web/public/notifications.js +10 -1
- data/lib/samagotchi/web/public/revise_stack.js +6 -5
- data/lib/samagotchi/web/public/task_stop.js +3 -3
- data/lib/samagotchi/web/public/update_notice.js +10 -4
- data/lib/samagotchi/web/public/update_sheet.js +3 -2
- data/lib/samagotchi/web/push/notifier.rb +66 -5
- data/lib/samagotchi/web/server.rb +45 -1
- data/lib/samagotchi/web/session_access_routes.rb +1 -1
- data/lib/samagotchi/web/session_hub.rb +20 -3
- data/lib/samagotchi/web/update_check.rb +56 -10
- data/lib/samagotchi/web/update_ops.rb +240 -0
- data/lib/samagotchi/worker_task_wakes.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1ef79d43ec1d5fc441ca8892ceb240a42fca4629295e71debc495df44b5efde8
|
|
4
|
+
data.tar.gz: 39b1d809ff216fcd1a1af146dc7858b45e80f94a8018e00841839573b753be0d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 228b7f233e3d6fd752ba0dbad22f4fed1e27910eb97a37190ec9172db94d1369dffe13f64389b3e1f77914bc845d2625c273c7569594fe76ffc4aa18edf749f6
|
|
7
|
+
data.tar.gz: f6439358124c6785a2ab54947e97928f725a817146bcd24e2263734e48bd2718b693e6573d4253fc64bbfe660a390125720f6a71662f31400bb7fbd39a4dd9da
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,47 @@ 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.139.1] - 2026-10-31
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- A project's mise.toml can no longer set `TMPDIR` for the session's tools: the guardrails judge `$TMPDIR` paths
|
|
15
|
+
by chi's own, so a write there could have been taken for a tmp write while it landed in the project.
|
|
16
|
+
- MCP servers (mcp 0.7.4) and context sources start like `execute`'s commands: without chi's own Bundler setup
|
|
17
|
+
(`RUBYOPT=-rbundler/setup`, `BUNDLE_GEMFILE`) or the project's `RUBYOPT`/`RUBYLIB`/Bundler variables, so a
|
|
18
|
+
chi run from a checkout no longer hands them its Gemfile.
|
|
19
|
+
- A worker that never starts its Bridge (`chi --attach`, the web's wake) now points at `chi home doctor`, which names
|
|
20
|
+
a worker that failed to boot the chi home's chi.
|
|
21
|
+
- The system bundle's self-knowledge names `execute`'s early hand-over to a task (a message for the turn), not only
|
|
22
|
+
its time limit.
|
|
23
|
+
- Web: a Revise stack's version chip names the version you are looking at ("v1 of 3"), and an earlier version you
|
|
24
|
+
switched to has no Copy link (it linked the newest turn with the earlier version's quote).
|
|
25
|
+
- Web: the bell is one control in each place: a toggle in a browser tab, the notifications sheet's opener in the
|
|
26
|
+
Home Screen app.
|
|
27
|
+
- Web: a markdown report in the stage's one-line rows is exactly a plain row's height.
|
|
28
|
+
- Web: on a phone, Revise, the stack switcher, Copy, the composer's Revise ✕, a fold's size and the activity
|
|
29
|
+
summaries no longer stay tinted after a tap.
|
|
30
|
+
|
|
31
|
+
Update with `chi update` (system, mcp 0.7.4).
|
|
32
|
+
|
|
33
|
+
## [0.139.0] - 2026-10-30
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- Unattended updates (opt-in): `update.auto: install` makes chi web install a new release by itself once no turn runs
|
|
38
|
+
and no question waits anywhere (one try per release). With `update.restart: idle` (the default) it then restarts
|
|
39
|
+
workers whose sessions have been idle for 10 minutes, then chi web itself after checking the new chi starts; the
|
|
40
|
+
phone gets "chi updated to X", or a push saying what failed. See docs/configuration.md (Unattended updates).
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- A message you send while a turn waits on a delegate (`delegate_result`, `delegate` with wait) ends that wait; the
|
|
45
|
+
child keeps running and its reply still comes.
|
|
46
|
+
- A message you send while `execute` runs a command hands the command over as a background task after 3 seconds,
|
|
47
|
+
instead of at its 120 s limit (never in pair turns or `!cmd`).
|
|
48
|
+
|
|
49
|
+
Update with `chi update`.
|
|
50
|
+
|
|
10
51
|
## [0.138.0] - 2026-10-30
|
|
11
52
|
|
|
12
53
|
### Changed
|
|
@@ -5285,7 +5326,9 @@ and long-lived sessions.
|
|
|
5285
5326
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
5286
5327
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
5287
5328
|
|
|
5288
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
5329
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.139.1...HEAD
|
|
5330
|
+
[0.139.1]: https://github.com/dm1try/samagotchi/compare/v0.139.0...v0.139.1
|
|
5331
|
+
[0.139.0]: https://github.com/dm1try/samagotchi/compare/v0.138.0...v0.139.0
|
|
5289
5332
|
[0.138.0]: https://github.com/dm1try/samagotchi/compare/v0.137.0...v0.138.0
|
|
5290
5333
|
[0.137.0]: https://github.com/dm1try/samagotchi/compare/v0.136.1...v0.137.0
|
|
5291
5334
|
[0.136.1]: https://github.com/dm1try/samagotchi/compare/v0.136.0...v0.136.1
|
data/docs/architecture.md
CHANGED
|
@@ -152,7 +152,7 @@ home's Ruby is gone, when no `samagotchi-*.gemspec` is under its gem path, and f
|
|
|
152
152
|
|---|---|---|
|
|
153
153
|
| `SessionManager.worker_command`: every worker (attach, `chi send`, chi web, delegate and Verify-fresh children, `resume_session`, `:restart`) | the home's ruby, `Gem.paths`, `>= 0.a` with the fallback pin (`ChiHome.fallback_version`: the spawner's version when the home has it, else the home's newest; a home older than the spawner is warned once); a chi that still can't load writes one `boot_failed` line to `SAMAGOTCHI_LOG_FILE` and exits 1 | `RbConfig.ruby -e` with `>= 0.a` (a checkout: `-I lib`) |
|
|
154
154
|
| `Web::Server.reexec_command` (chi web's restart) | `CHI_BOOT -- web …`; the `:no_wrapper` refusal never applies; a home whose newest chi is older than this one is passed over (logged `restart_home_older`) | `[RbConfig.ruby, wrapper, …]` (`$PROGRAM_NAME` in a checkout) |
|
|
155
|
-
| `Web::
|
|
155
|
+
| `Web::UpdateOps#update_argv` (the **Update** button's child) | `CHI_BOOT -- update --to V --no-desktop --json` | `[RbConfig.ruby, wrapper, update, …]` |
|
|
156
156
|
| `UpdateCommand` | on another Ruby than the home (`Record#running_here?`): exec `CHI_BOOT -- update …` once (`SAMAGOTCHI_UPDATE_IN_HOME=1`: an older home chi ignores it); the install (`GemUpdate`) runs `ChiHome.gem_bin` with `ChiHome.install_env` (the home's GEM_HOME/GEM_PATH for that child, `mise`'s dir on PATH); the hand-over execs `CHI_BOOT -- update --no-gem --gem-from …` | this Ruby's `gem`, the wrapper |
|
|
157
157
|
| `BootstrapCommand` (commonmarker) | `ChiHome.gem_bin` + `install_env`; installed? = in the home's specifications | this Ruby's `gem` |
|
|
158
158
|
| `Desktop::MacOS#launch_config` | argv `CHI_BOOT + ["--"]`; `launch_ok?` / `launch_problem` check only absolute argv entries | `[ruby, wrapper or bin/chi]` |
|
|
@@ -606,23 +606,30 @@ reload in a toast, since an open tab keeps its old JS across a `chi web` upgrade
|
|
|
606
606
|
`installed`, the newest chi on this machine (`InstalledVersions`: the highest `samagotchi-<v>.gemspec`
|
|
607
607
|
in RubyGems' specification dirs, or a checkout's `version.rb` on disk; looked at again on each full
|
|
608
608
|
probe, every 10 s). A `chi` frame and the `GET /api/events` snapshot carry the full state
|
|
609
|
-
`{version, installed, available, update_blocked}
|
|
610
|
-
carry the same fields. `SessionHub#update_available` publishes the checker result under the hub's
|
|
609
|
+
`{version, installed, available, update_blocked, auto}` (`auto`: `update.auto`, `off` or `install`, which the page's
|
|
610
|
+
"available" wording follows); `/api/info` and the session-command wake response carry the same fields. `SessionHub#update_available` publishes the checker result under the hub's
|
|
611
611
|
Monitor, but fetching never holds it.
|
|
612
612
|
|
|
613
|
-
`Web::UpdateCheck` runs its own thread beside the hub. When `update.notify: on` (the default)
|
|
613
|
+
`Web::UpdateCheck` runs its own thread beside the hub. When `update.notify: on` (the default) or
|
|
614
|
+
`update.auto: install` (then with `notify: off` the hub's `available` stays nil: nothing is shown or pushed, only
|
|
615
|
+
AutoUpdate reads `UpdateCheck#available`), its
|
|
614
616
|
first RubyGems check is about 30 seconds after chi web starts, then it checks at `update.check_hours`
|
|
615
617
|
intervals. A stable release is available only after `update.min_age_hours` and when newer than both
|
|
616
618
|
the running and newest installed chi. Its atomic, mode-0600
|
|
617
619
|
`$XDG_STATE_HOME/samagotchi/update_check.json` keeps the check cooldown and versions already
|
|
618
|
-
announced across restarts. `update.notify: off`
|
|
619
|
-
request. `update_blocked` names why the local gem cannot be updated in place (checkout, Bundler, or
|
|
620
|
+
announced across restarts, and AutoUpdate's record (`auto`, `auto_failed`; `save_auto`). `update.notify: off`
|
|
621
|
+
with `update.auto: off` starts no checker thread and makes no network request. `update_blocked` names why the local gem cannot be updated in place (checkout, Bundler, or
|
|
620
622
|
an unwritable gem directory). The page tells the owner to run `chi update`; a newer `installed`
|
|
621
623
|
version than the server itself still means restart chi web (its workers already run the newest chi,
|
|
622
624
|
[Sessions](sessions.md)). `GET /api/info` also names `newest_workers`; next to `cwd` (the folder chi
|
|
623
625
|
web runs in) it names `start_dir`, the folder it starts in (`web.start_dir`, `--start-dir`;
|
|
624
626
|
`Web::StartDir`), which a second `chi web` given another one mentions.
|
|
625
627
|
|
|
628
|
+
The three update operations live in `Web::UpdateOps` (`web/update_ops.rb`), which `App`'s routes ask for
|
|
629
|
+
(`handle_update`, `handle_restart_workers` and `handle_restart_web` only validate, ask and map `:started` /
|
|
630
|
+
`:busy` / a refusal to 202 / 409 / 503) and `Web::AutoUpdate` asks for too; the one-at-a-time rule is its mutex.
|
|
631
|
+
`Server.start` hands `app.update_ops` to AutoUpdate and sets its restart lambda through `App#restart_web=`.
|
|
632
|
+
|
|
626
633
|
The owner's update routes publish progress and results as `chi_update` events with an
|
|
627
634
|
`operation` (`install` or `workers`), `state`, and `rows`; install events also name the target
|
|
628
635
|
`version` and may include a progress line. The hub retains the latest event for a reconnecting
|
|
@@ -632,7 +639,7 @@ it holds with `update.notify: off`; a child that prints no rows fails with its o
|
|
|
632
639
|
done install refreshes the hub's installed version (`SessionHub#refresh_installed`) before its `done`.
|
|
633
640
|
Worker restarts target every worker older than the version installed now (refreshed the same way)
|
|
634
641
|
and publish per-session results. Only one of an install, a worker restart pass and a web restart runs
|
|
635
|
-
at a time (409 `update_running`). `POST /api/update/restart_workers`
|
|
642
|
+
at a time (409 `update_running`). `POST /api/update/restart_workers` starts the worker pass
|
|
636
643
|
by itself. `POST /api/update/restart_web` (409 `restart_unavailable` without a
|
|
637
644
|
chi wrapper to exec) schedules WEBrick shutdown only after its response is sent; after the hub and
|
|
638
645
|
listener stop, `Server` execs the RubyGems chi wrapper with every flag chi web was started with, as
|
|
@@ -642,6 +649,24 @@ listener stop, `Server` execs the RubyGems chi wrapper with every flag chi web w
|
|
|
642
649
|
themselves are detached), and adopts an exact Tailscale Serve handler. A failed exec, or a restarted
|
|
643
650
|
chi web that doesn't start for any reason, turns off only that exact adopted handler.
|
|
644
651
|
|
|
652
|
+
`Web::AutoUpdate` (`web/auto_update.rb`; its own thread, started and stopped by `Server.start` beside the checker)
|
|
653
|
+
is `update.auto: install`. Every 5 minutes (the first step 60 s after start) it looks at `UpdateCheck#available`
|
|
654
|
+
and installs it through `UpdateOps#install(wait: true, auto: true)` when the install isn't blocked, no operation
|
|
655
|
+
runs, no hub summary has `status: running` or a `pending_question`, and the version isn't in `auto_failed` or
|
|
656
|
+
already in the `auto` record (one automatic try per version). Its frames carry `auto: true` (the page: "Automatic
|
|
657
|
+
chi update in progress", "(automatic)" in the sheet's title). Its record is `update_check.json`'s `auto`:
|
|
658
|
+
`{version, from, at, kept, phase: installed|workers|web|done|failed, workers: {restarted, left}, detail, pushed}`.
|
|
659
|
+
With `update.restart: never` the run ends at the install (`chi_notice` `installed`). With `idle` it goes on
|
|
660
|
+
(`phase` `installed` → `workers` → `web` → `done`): `UpdateOps#restart_workers(select:)` over the stale workers whose
|
|
661
|
+
summary is idle with no question and `updated_at` at least 10 minutes old (`QUIET`; the others are `skipped` rows;
|
|
662
|
+
the worker's own atomic refusal still applies), the left ones retried each step for 24 h (`RETRY_FOR`); then, with no
|
|
663
|
+
turn running, `Server.restart_preflight` (the restart's command with `--version`, 20 s, killed on a timeout; it must
|
|
664
|
+
print a chi at least the release) and, if it passes, the record's `phase: web` is written **before**
|
|
665
|
+
`UpdateOps#restart_web`. A failed preflight, or a refused restart, publishes `restart_failed` and ends the run
|
|
666
|
+
(`failed`). The restarted chi web's AutoUpdate (first step after 5 s) finds `phase: web`, checks it runs the
|
|
667
|
+
release, counts the workers once more and publishes `updated`, then `done`. Results reach the phone as `chi_notice`
|
|
668
|
+
hub events (`SessionHub#publish_notice`), see [internals/web-push.md](internals/web-push.md).
|
|
669
|
+
|
|
645
670
|
The page's scope is in its URL, and the server picks the default: `?dir=<folder>` is that folder's
|
|
646
671
|
project (the list, the events stream and a new chat follow it), `?scope=all` every session, and a bare
|
|
647
672
|
`/` the start folder's project when that folder is in a repo (`App#index_data_attributes` writes
|
|
@@ -862,6 +887,7 @@ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/
|
|
|
862
887
|
| Review sessions | `review_start.rb` (`ReviewStart`), `session_review.rb` (`SessionReview`, the session's `review` field), `review_command.rb` (`ReviewCommand`, `chi review`), `review_apply.rb` (`ReviewApply`) | A session made to be shared: idle, its subjects project files checked by `Guardrails::ReviewPaths`, a fork of a working session from its recap (`--from`); Apply quotes a participant's review turn into the owner's composer ([sessions.md](sessions.md#review-sessions), [internals/participants.md](internals/participants.md)). |
|
|
863
888
|
| chi web's routes | `web/app.rb` (`Web::App`: the gate, `ROUTES`, the shared helpers), `web/message_display.rb` (`Web::MessageDisplay`: saved messages and cards as display items, `POST /api/render`), `web/session_show.rb` (`Web::SessionShow`: `GET /api/sessions/:id` with its `cards`, `tail` and `timing` re-reads, `GET …/output`, the timing payload), `web/session_create.rb` (`Web::SessionCreate`: `POST /api/sessions`, a continue, a review fork), `web/session_control.rb` (`Web::SessionControl`: answer, command, turn and images, cancel, stop, restart, archive, delete), `web/push_routes.rb` (`Web::PushRoutes`: `/api/push`, its subscriptions, prefs, test and presence) | The handlers `App`'s `ROUTES` name, in the file that holds their kind; each mixin is included in `App` and reads its helpers and ivars, so `app.send(:handle_show, …)` and the specs that stub `App` methods work as before. |
|
|
864
889
|
| Web Push | `web/push.rb` (`Web::Push`: `Runtime`, `preflight!`, the `web.push.*` settings), `web/push/crypto.rb` (`Push::Crypto`: RFC 8291 aes128gcm, RFC 8292 VAPID), `web/push/store.rb` (`Push::Store`, `Store::Sub`: `web-push/vapid.pem`, `subscriptions.json`), `web/push/client.rb` (`Push::Client`, `Client::Result`), `web/push/attention.rb` (`Push::Attention`: notify.js's rules in Ruby, shared cases in `spec/fixtures/attention_cases.json`), `web/push/presence.rb` (`Push::Presence`: the pages' "in front" reports), `web/push/notifier.rb` (`Push::Notifier`, a hub subscriber with a sender thread), `web/push_routes.rb` (`Web::PushRoutes`), `web/private_files.rb` (`Web::PrivateFiles`, shared with the grants), `public/sw.js`, `public/push.js`, `public/manifest.webmanifest` | chi web pushes what the tab notifies for (approvals, questions, step-limit questions, cards, failed turns; done turns to a device that asks) to the owner's Home Screen app over an https public URL: the Notifier turns the hub's new attentions into show pushes after a hold, unless a chi web page is in front, at most 30 an hour, with one retry and a restart catch-up (`sent.json`) (each carries the keys still open, and the badge; no close push: every push shows a notification); the bell (`notifications.js`) subscribes the device, and the service worker and the page close the answered ones. See [internals/web-push.md](internals/web-push.md). |
|
|
890
|
+
| chi web's updates | `web/update_check.rb` (`Web::UpdateCheck`), `web/update_ops.rb` (`Web::UpdateOps`), `web/auto_update.rb` (`Web::AutoUpdate`) | The RubyGems checker and its `update_check.json`; the install, worker-restart and chi-web-restart operations (one at a time) the owner's routes and AutoUpdate share; `update.auto: install`'s thread (install when no turn runs, then idle workers and chi web). See [configuration](configuration.md#unattended-updates-updateauto). |
|
|
865
891
|
| Behind an auth proxy | `web/public_url.rb` (`Web::PublicUrl`: `web.public_url`, `web.proxy.identity_header`, `web.proxy.owner`), `web/proxy_gate.rb` (`Web::ProxyGate`, mixed into App; `RawHeaderNames`, prepended to WEBrick's request by `Web::Server`), `web/link_codes.rb` (`Web::LinkCodes`: the links' one-time codes), `proxy_trace.rb` (`ProxyTrace`), `web/public/token_fragment.js`, `web/public/auth_lost.js`, `web/tunnel.rb` + `web/tunnel/tailscale.rb` (`Web::Tunnel`, `Web::Tunnel::Tailscale`: `chi web --tunnel tailscale`, Tailscale Serve set for one run and the PublicUrl it makes) | Experimental: chi web behind an auth proxy. Every request needs the token; a proxied one's owner also needs the identity header (one clean value under one name); the `__Host-` cookie, an absolute `Location` from the public URL, https links with a one-time code in the fragment; the page's lost-sign-in banner. Bridges refuse proxy traces ([configuration.md](configuration.md#behind-an-auth-proxy)). |
|
|
866
892
|
| Participants | `participants.rb` (`Participants`, the `participants.json` sidecar), `share_command.rb` (`ShareCommand`, `chi share`), `session_participants.rb`, `participants_watch.rb`, `web/grants.rb` (`Web::Grants`), `web/principal.rb` (`Web::Principal`), `web/participant_access.rb` (`Web::ParticipantAccess`: the participant's gate and routes), `web/participants_admin.rb` (`Web::ParticipantsAdmin`: the owner's invite, list, revoke, cap and Apply routes), `web/participant_review.rb` + `web/participant_view.rb` (`Web::ParticipantReview`, `Web::ParticipantView`), `web/participant_stream_filter.rb`, `guardrails/review_boundary.rb` + `guardrails/review_paths.rb` | The people or agents an owner invites to review one session through chi web: their grants and gate, what they read, their read-only review turns ([internals/participants.md](internals/participants.md), [guardrails.md](guardrails.md)). |
|
|
867
893
|
| Analytics | `analytics_file.rb` (`AnalyticsFile`), `session_metrics/totals.rb` (`SessionMetrics::Totals`), `session_metrics/model_segment.rb` (`SessionMetrics::ModelSegment`) | A session's `analytics.json` (`SessionMetrics#persist`); the sums over its turn records (`Totals`: tokens, cost, speeds, kept as running counters) and their split per model after a `/model` switch (`ModelSegment`: a run of turns on one model, the `model_segments` rows); `session_metrics/cost.rb` (`SessionMetrics::Cost`): the spend `ctx.cost` reads; `session_metrics/usage.rb` (`SessionMetrics::Usage`): the work `ctx.usage` reads. `session_metrics/side_totals.rb` (`SessionMetrics::SideTotals`) and `side_usage.rb` (`SideUsage`): the side requests' sums, apart from the turns'. |
|
data/docs/cli.md
CHANGED
|
@@ -256,6 +256,10 @@ done
|
|
|
256
256
|
`update.min_age_hours` tune the interval and release age (see
|
|
257
257
|
[configuration](configuration.md#all-settings)). If the gem can't be updated
|
|
258
258
|
in place, the notice gives the blocker and its command instead.
|
|
259
|
+
With `update.auto: install` chi web installs the release by itself once no
|
|
260
|
+
turn runs anywhere, then restarts idle workers and itself (`update.restart`),
|
|
261
|
+
and the notice reads "installs by itself" with no **Update** button; see
|
|
262
|
+
[Unattended updates](configuration.md#unattended-updates-updateauto).
|
|
259
263
|
|
|
260
264
|
- **The chi home.** With a [chi home](configuration.md#the-chi-home), a `chi
|
|
261
265
|
update` started on another Ruby first says `updating the chi home (<ruby>)`
|
|
@@ -462,7 +466,7 @@ of UIs at once: the Web UI and attached terminals (`chi`, `--resume`,
|
|
|
462
466
|
`--attach`). They all see the same turns as they happen, and any of them can send
|
|
463
467
|
a prompt, also while a turn runs (it merges into that turn as steering: the model
|
|
464
468
|
is told it is a steer and who sent it). Steering waits for the model's next step
|
|
465
|
-
and cuts nothing; a `task_wait` the model is blocked in ends early for it (the task keeps running). `/cut TEXT` in a terminal or the web composer (or `chi send --cut`) asks for a cut:
|
|
469
|
+
and cuts nothing; a `task_wait` or `delegate_result` the model is blocked in ends early for it (the task or child keeps running), and a running `execute` command moves to a background task once it has run 3 s. `/cut TEXT` in a terminal or the web composer (or `chi send --cut`) asks for a cut:
|
|
466
470
|
once the model has streamed only thinking for `steer.cut_after` seconds (per host/model: `steer_cut_after`; default
|
|
467
471
|
20), now or while your message waits, that generation is cut and the step starts again with your message, marked
|
|
468
472
|
`↪ cut in for your message` (see
|
|
@@ -1057,7 +1061,7 @@ moves into the later stub. The newest version has a chip, "v3 · 2 earlier ▾",
|
|
|
1057
1061
|
listing every version by its prompt; pick one to jump to its stub. Before
|
|
1058
1062
|
that chip, a "‹ 3/3 ›" switcher reads an earlier version's answer in the
|
|
1059
1063
|
newest one's place (its left edge dashed while it shows; Copy copies the
|
|
1060
|
-
version on view); › back to the newest. Switching only changes what this
|
|
1064
|
+
version on view, and the chip names it, "v1 of 3"); › back to the newest. Switching only changes what this
|
|
1061
1065
|
page shows, and a reload shows the newest again. Revise
|
|
1062
1066
|
again on the newest version adds to the stack. Stacks are session data (the
|
|
1063
1067
|
stamp is on the Revise prompt, see [sessions.md](sessions.md), Revise
|
data/docs/configuration.md
CHANGED
|
@@ -179,7 +179,7 @@ or `chi web` restart.
|
|
|
179
179
|
| `history.file` | Next history access | The TUI and web composer resolve its path when they read or append history. |
|
|
180
180
|
| `broadcast.*` | Next `chi broadcast` | Each broadcast invocation reads its targeting and triage settings. |
|
|
181
181
|
| `update.gem`, `update.bundles`, `update.desktop` | Next `chi update` | The command reads which components to update when it runs. |
|
|
182
|
-
| `update.notify`, `update.check_hours`, `update.min_age_hours` | `chi web` start | The update checker starts with chi web; restart it to apply changes. |
|
|
182
|
+
| `update.notify`, `update.check_hours`, `update.min_age_hours`, `update.auto`, `update.restart` | `chi web` start | The update checker starts with chi web; restart it to apply changes. |
|
|
183
183
|
| `kitty.*`, `tmux.*` | `chi desktop upgrade` or desktop update | These settings are copied into the desktop helper's launch file. |
|
|
184
184
|
| `model.profile`, `reasoning_history` | Not from config.yml | These are CLI/environment settings; `models:` and `hosts:` provide their model- and host-specific forms. |
|
|
185
185
|
|
|
@@ -1143,8 +1143,13 @@ thinking is lost. A generation that has streamed visible text or a tool call is
|
|
|
1143
1143
|
(`ctx.steer`, `ctx.sessions.send`) never cuts.
|
|
1144
1144
|
|
|
1145
1145
|
A message that would go in at the next step (any but a queued one, one with images or a participant's) also ends a
|
|
1146
|
-
`task_wait` the model is blocked in, within half a second: the task
|
|
1147
|
-
reads it at once and decides whether to wait again.
|
|
1146
|
+
`task_wait` or a `delegate_result` (and `delegate`'s own wait) the model is blocked in, within half a second: the task
|
|
1147
|
+
or the child keeps running, the model is told a message waits, reads it at once and decides whether to wait again. It
|
|
1148
|
+
also moves a running `execute` command to a background task early, once the command has run 3 seconds (a command that
|
|
1149
|
+
ends sooner just returns its result): the result says `Still running at the 4s mark: a message from the user is
|
|
1150
|
+
waiting …` and names the `task_id`, and `task_wait` waits for it as for any hand-over. With `execute.on_timeout:
|
|
1151
|
+
kill`, in a participant's pair turn and for the `!cmd` bang an `execute` is never handed over early (they kill at the
|
|
1152
|
+
limit). A plugin's steer and a delegate child's report don't end any of them.
|
|
1148
1153
|
|
|
1149
1154
|
The wait resolves per model, in this order: env `SAMAGOTCHI_STEER_CUT_AFTER`, then
|
|
1150
1155
|
`models.<key>.steer_cut_after`, `hosts.<name>.steer_cut_after`, `steer.cut_after` in config.yml, then 20. `0` means
|
|
@@ -1397,7 +1402,9 @@ launches it. It is recorded in `~/.config/samagotchi/home.json` (`$XDG_CONFIG_HO
|
|
|
1397
1402
|
`home_older_than_spawner`), but chi web's restart runs the newest chi it knows outside the home instead
|
|
1398
1403
|
(`restart_home_older`): its flags may be ones the older chi doesn't know.
|
|
1399
1404
|
- **A home that still fails at boot** (a broken install): the worker writes one `boot_failed` line to the debug log
|
|
1400
|
-
(`log.file`) and exits, rather than leave the spawner waiting; `chi home doctor` reports the newest one
|
|
1405
|
+
(`log.file`) and exits, rather than leave the spawner waiting; `chi home doctor` reports the newest one, and an
|
|
1406
|
+
attach whose worker never starts its Bridge says so ("… did not start its Bridge in time (chi home doctor shows a
|
|
1407
|
+
worker that failed to boot)").
|
|
1401
1408
|
- Each machine has its own home (a remote's is that machine's).
|
|
1402
1409
|
|
|
1403
1410
|
### Checking it: chi home doctor
|
|
@@ -2033,6 +2040,9 @@ there; the new subscription replaces the dead one.
|
|
|
2033
2040
|
approval, which its parent may relay instead); one push each. A delegated child's failed or finished turn is its
|
|
2034
2041
|
parent's to report. **Done turns** (10 s or longer, or one nobody started: a delegate's report, a context change, a
|
|
2035
2042
|
finished task) are **off** by default, per device: the **Long turns done** switch in the devices sheet turns them on.
|
|
2043
|
+
- **Updates**: a new chi release ("chi X is available", once per version) and, with
|
|
2044
|
+
[`update.auto: install`](#unattended-updates-updateauto), how the automatic update went ("chi updated to X", "chi X
|
|
2045
|
+
didn't install", …): kept 24 h for an offline phone, `normal` urgency, never the file names.
|
|
2036
2046
|
- **Quiet while you look**: nothing is pushed while any chi web page (a tab on the Mac, or the app) is in front and
|
|
2037
2047
|
was touched in the last 2 minutes; what came meanwhile is never pushed later (the page has it). A page left in
|
|
2038
2048
|
front untouched stops quieting the phone after about 2 minutes.
|
|
@@ -2052,6 +2062,49 @@ there; the new subscription replaces the dead one.
|
|
|
2052
2062
|
(it shows the "needs chi web's access token" page; paste the token there). Pushes keep coming meanwhile.
|
|
2053
2063
|
- chi web not running: no pushes (Serve answers 502; a tap couldn't open the page anyway).
|
|
2054
2064
|
|
|
2065
|
+
## Unattended updates (`update.auto`)
|
|
2066
|
+
|
|
2067
|
+
By default chi web only tells you a release is out (`update.notify`, [CLI: Updating](cli.md#updating)). With
|
|
2068
|
+
`update.auto: install` in `config.yml` it also installs the release its checker found, aged by `update.min_age_hours`
|
|
2069
|
+
(24 h by default, so a release is never installed the day it ships), and goes on to `update.restart`:
|
|
2070
|
+
|
|
2071
|
+
```yaml
|
|
2072
|
+
update:
|
|
2073
|
+
auto: install # off (default): a notice only
|
|
2074
|
+
restart: idle # idle (default): restart workers and chi web onto it; never: install only
|
|
2075
|
+
```
|
|
2076
|
+
|
|
2077
|
+
Both are read when chi web starts (restart it to apply a change). What it does, in order:
|
|
2078
|
+
|
|
2079
|
+
1. **Waits for a quiet moment.** The install starts when no turn runs and no question waits for an answer in any
|
|
2080
|
+
session (checked every 5 minutes), no other update operation runs (the **Update** button, a restart) and the
|
|
2081
|
+
install isn't blocked (a source checkout, Bundler, a RubyGems directory chi can't write: those stay a notice, with
|
|
2082
|
+
the command to run). `chi update` also merges the shipped bundles that live sessions read, hence "no turn anywhere".
|
|
2083
|
+
2. **Installs** exactly the release the notice named, as the **Update** button does (the system bundle and shipped
|
|
2084
|
+
bundles are merged three ways; files you edited are kept and counted in the push). The page's sticky toast says
|
|
2085
|
+
"Automatic chi update in progress" and its sheet shows the rows. A failed install is tried **once per version**:
|
|
2086
|
+
it pushes the failed row's reason and stays quiet until a newer release (or you, with the button) tries again.
|
|
2087
|
+
3. **Restarts** (`update.restart: idle`). Stale workers first, only those whose session is idle and untouched for
|
|
2088
|
+
10 minutes (so nobody who is about to type loses their worker), and each worker still refuses by itself when
|
|
2089
|
+
something holds it ([sessions](sessions.md)); the ones left are tried again for a day, then wait for their own
|
|
2090
|
+
idle exit or the session's **Restart**. Then chi web itself, once no turn runs anywhere: it first runs the
|
|
2091
|
+
restart's own command with `--version` (20 s at most) and restarts only if the newest chi answers with the
|
|
2092
|
+
release's version; otherwise chi web stays up and the phone hears "chi X is installed; chi web didn't restart".
|
|
2093
|
+
The restart is the **Restart chi web** one: same port, same token, same Tailscale Serve handler, the open page's
|
|
2094
|
+
streams reconnect in a few seconds and a Reload toast follows. It doesn't wait for a page in front: the page isn't
|
|
2095
|
+
reloaded by itself, so a draft in the composer stays. A **Send** or **Allow** pressed in those few seconds fails
|
|
2096
|
+
with its error and leaves the text in the composer: press it again.
|
|
2097
|
+
4. **Says so.** The restarted chi web pushes "chi updated to X" ("chi web and 3 sessions restarted; 1 restarts when
|
|
2098
|
+
idle. Kept your edits in 2 files."). With `update.restart: never` the push is "chi X is installed" and chi web and
|
|
2099
|
+
the workers move over when you restart them (the page's toast offers it). The push needs the phone set up
|
|
2100
|
+
([Phone notifications](#phone-notifications-web-push)); without it the page's toasts and
|
|
2101
|
+
`$XDG_STATE_HOME/samagotchi/update_check.json` (`auto`) are the record.
|
|
2102
|
+
|
|
2103
|
+
There is no watchdog: chi web restarts into the newest chi it just proved can start, and nothing brings an old one
|
|
2104
|
+
back by itself. To roll back, install the older gem with `gem install samagotchi -v X` and restart chi web
|
|
2105
|
+
(`chi update --to` refuses a version that isn't newer than the running chi). The desktop helper isn't rebuilt (that
|
|
2106
|
+
stays a manual `chi update`).
|
|
2107
|
+
|
|
2055
2108
|
## All settings
|
|
2056
2109
|
|
|
2057
2110
|
Every setting below takes the three forms described in
|
|
@@ -2165,6 +2218,8 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
|
|
|
2165
2218
|
| `update.bundles` | `true` | | `false`: `chi update` leaves the shipped bundles to `chi bundle upgrade` (`--no-bundles`). |
|
|
2166
2219
|
| `update.desktop` | `true` | | `false`: `chi update` leaves the desktop helper alone (`--no-desktop`). |
|
|
2167
2220
|
| `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. |
|
|
2221
|
+
| `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). |
|
|
2222
|
+
| `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). |
|
|
2168
2223
|
| `update.check_hours` | `24` | | Hours between RubyGems checks (at least 1). The first check starts about 30 seconds after chi web starts. |
|
|
2169
2224
|
| `update.min_age_hours` | `24` | | A release must be this old before chi web offers it; `0` disables the age delay. |
|
|
2170
2225
|
| `kitty.listen_on` | none | | The desktop helper's kitty socket, copied from `kitty.conf`; unset = no kitty targets. See "Desktop helper: agents in kitty". |
|
|
@@ -2182,7 +2237,7 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
|
|
|
2182
2237
|
| `execute.telemetry_threshold_pct` | `80` | yes | |
|
|
2183
2238
|
| `execute.cut_head_chars` | `1000` | yes | An `execute` result over `max_tool_output_chars` keeps at most this many characters of its start (and never more than a quarter of the cap); its end gets the rest. |
|
|
2184
2239
|
| `execute.timeout_sec` | `120` | yes | Seconds one `execute` command may run before `execute.on_timeout` applies. |
|
|
2185
|
-
| `execute.on_timeout` | `task` | yes | What happens to a command still running at that limit: `task` keeps it running as a background task (the result names its `task_id`; `task_wait` waits for it, `task_stop` stops it), `kill` stops it (`Error: command timed out after …s`). The `!cmd` bang always kills. See [Background tasks](internals/background-tasks.md). |
|
|
2240
|
+
| `execute.on_timeout` | `task` | yes | What happens to a command still running at that limit: `task` keeps it running as a background task (the result names its `task_id`; `task_wait` waits for it, `task_stop` stops it; also early, after 3 s, when a message from the user waits for the turn), `kill` stops it (and never hands over early) (`Error: command timed out after …s`). The `!cmd` bang always kills. See [Background tasks](internals/background-tasks.md). |
|
|
2186
2241
|
| `execute.description` | `true` | | `execute` offers the model an optional `description` (a few words on what the command does), shown as the tool row's title in `chi web` and in place of the command in the TUI's tool line. `false`: the parameter isn't declared, and rows show the command's first step. |
|
|
2187
2242
|
| `web.port` | `4567` | `--port` | `chi web`'s port. See [CLI](cli.md). |
|
|
2188
2243
|
| `web.host` | `127.0.0.1` | yes | `127.0.0.1`, `::1` or `localhost`; `lan` (this machine's private IPv4 address) or one of its IPv4 addresses also opens `chi web` to the network, with an access token, asked of this machine's 127.0.0.1 too (its `Local:` link carries a one-time code; `chi web --new-token` replaces the token). Anything else binds `127.0.0.1` with a warning. See [CLI: chi web on your phone](cli.md#chi-web-on-your-phone). |
|
|
@@ -25,7 +25,7 @@ Behavior:
|
|
|
25
25
|
- `execute` runs its command with stdout and stderr in two logs under `$XDG_STATE_HOME/samagotchi/execute/<id>/` (always an absolute path: a relative `XDG_STATE_HOME` is taken from the directory chi started in; outside the workspace: `git status` and `rm -rf tmp` never see them), stdin `/dev/null` and the same `exit_code` wrapper as a task. Its own copies of the logs hold a shared `flock` across the spawn: while a fresh exclusive `flock` fails, some process the command started still has them open. When the shell exits, a background child still holding them (`server &`) gets 1.5 s, then its group is stopped; one holding none (`nohup server >log 2>&1 &`) is left alone. A holder that left the group (setsid) is out of the stop's reach: it runs on, the result ends with the "left running" hint, and its dir stays for it to write to (the sweep removes it once no one holds the logs). Otherwise a finished command's dir is removed. Known gap on Linux: a command that writes through `/dev/stdout` or `/dev/stderr` (`echo b >/dev/stdout`) reopens the log file itself with `O_TRUNC`, so the output before it is lost (macOS dups the descriptor; the fix would be a relay pipe). The check needs the state dir on a local file system: over NFS, `flock` is emulated with `fcntl` locks, which children don't inherit.
|
|
26
26
|
- The exit code: the command runs after an EXIT trap that writes the shell's status to the `exit_code` file (`TaskRuntime.wrapped_shell_command`, shared with `task_create`), so `exit N`, a `set -e` failure and a trailing `\` or `|` in the command all record what the shell exited with. The trap runs wherever the shell is when it exits (a `task_create` `cwd`, or a `cd` in the command), so `task_create`'s `exit_code_path` is absolute; its `output_path` stays relative (`tmp/tasks/<id>/output.log`, opened by chi in the worker's cwd before the spawn). A command that sets its own EXIT trap replaces it, and a shell killed by a signal skips it (zsh's sh mode, on TERM, INT and KILL): `execute` still reports the shell's status, and a `task_create` task or a handed-over execute gets its code from chi's waiter while the chi that started it runs (`TaskRuntime.record_exit_status`): the exit status, or `128 + the signal` (TERM → 143), with no new `stop_reason`. A record a refresh already finished without a code in between (`failed`, `process_ended_without_exit_code` or the not-chi's-process reason) is corrected, its `finished_at` kept; a stopped task (its stop flag on disk) is never touched. `process_ended_without_exit_code` is left only when the shell wrote nothing and the chi that started it was gone too.
|
|
27
27
|
- A stop of an `execute` (the user's Stop, the limit in kill mode) is done once the shell is reaped and nothing holds the logs: a child that ignores TERM after its shell died gets the KILL.
|
|
28
|
-
- A command still running at `execute.timeout_sec` is handed over as a task (`execute.on_timeout: task`, the default; `kill` stops it instead, and the `!cmd` bang always kills): the result starts `Still running at the 120s limit: moved to a background task, not killed.`, then `task_id: …` and the tails of both logs so far. A timeout during the 1.5 s background grace is a kill (the shell is gone, nothing to hand over). If the record can't be written, the command is killed. `TaskRuntime.adopt` writes a record for the process execute already started, with `handed_over_by: "execute"`. Its logs stay where execute wrote them, so the record's paths are absolute: `output_path` (stdout), `stderr_path` (stderr, a log of its own) and `exit_code_path` (a `task_create` record's `exit_code_path` is absolute too; its `output_path` is relative). `started_at` (and `created_at`) is the spawn time, not the hand-over's; `pid_started` is read at the hand-over, while execute's detach waiter is still running. From there it is an ordinary task: `task_wait`, `task_get`, `task_list` and `task_stop` treat it as any other. Its dir gets a `task` back-pointer file holding the record's absolute path. Once per process, the first `execute` sweeps `…/execute/`: a dir whose logs no process holds is removed when it has no back-pointer and is a day old (chi died mid-command), when its record is gone, or when it is 14 days old. The records live in each project's `tmp/tasks`, which the state dir can't list: hence the back-pointer.
|
|
28
|
+
- A command still running at `execute.timeout_sec` is handed over as a task (`execute.on_timeout: task`, the default; `kill` stops it instead, and the `!cmd` bang always kills): the result starts `Still running at the 120s limit: moved to a background task, not killed.`, then `task_id: …` and the tails of both logs so far. A command is also handed over early when a person's message waits for the turn's next step (the `message_waiting:` probe, the same `Engine#input_waiting?` as `task_wait`'s): after `Execute::EARLY_HAND_OVER_FLOOR_SEC` (3 s: a short command ends first and its result is the better answer), in the next 0.2 s slice. The first line then reads `Still running at the 4s mark: a message from the user is waiting, so the command moved to a background task, not killed.` (the seconds it had run), and the line after `task_id:` starts `Read the message first.`; the rest is the same hand-over, and the task stays watched. Never in `kill` mode (`execute.on_timeout: kill`, a participant's pair turn, the `!cmd` bang): there the probe is not even read and the command runs to its limit and is killed. A command that can't be adopted at that moment (a record that can't be written) isn't killed early: it runs on to its limit. Everything that matches the first line builds on `Execute::HANDED_OVER` (`Still running at the`) and `Execute::HANDED_OVER_AFTER` (`120s limit:` or `4s mark:`): `TaskRuntime.handed_over_pattern`, `ToolActivity::HANDED_OVER_START`, `TaskWatch` (`limit_sec` for the limit's form, `early_sec` for the mark's, which the finish note words as `handed over by execute after 4s: a message from the user was waiting`) and the web's `task_stop.js`; the slow-commands bundle reads only the limit's form, which is the only one that is a timeout. A timeout during the 1.5 s background grace is a kill (the shell is gone, nothing to hand over). If the record can't be written, the command is killed. `TaskRuntime.adopt` writes a record for the process execute already started, with `handed_over_by: "execute"`. Its logs stay where execute wrote them, so the record's paths are absolute: `output_path` (stdout), `stderr_path` (stderr, a log of its own) and `exit_code_path` (a `task_create` record's `exit_code_path` is absolute too; its `output_path` is relative). `started_at` (and `created_at`) is the spawn time, not the hand-over's; `pid_started` is read at the hand-over, while execute's detach waiter is still running. From there it is an ordinary task: `task_wait`, `task_get`, `task_list` and `task_stop` treat it as any other. Its dir gets a `task` back-pointer file holding the record's absolute path. Once per process, the first `execute` sweeps `…/execute/`: a dir whose logs no process holds is removed when it has no back-pointer and is a day old (chi died mid-command), when its record is gone, or when it is 14 days old. The records live in each project's `tmp/tasks`, which the state dir can't list: hence the back-pointer.
|
|
29
29
|
- `task_wait` and `task_get` show `stderr_path` when the record has one; `task_wait`'s output tail is followed by a `stderr_tail:` of at most 10 lines when that log isn't empty. A `task_create` task has one log (stdout and stderr together) and no `stderr_path`. A `task_wait` result longer than `max_tool_output_chars` keeps its header and the tail's end, with a gap line naming `output_path`, where the whole log already is (and `stderr_path`, when the record has one) (`ToolOutputCut`'s `:head_tail_ref`; nothing is copied; see [Tool output guardrails](tool-guardrails.md#the-runners-cut-max_tool_output_chars)).
|
|
30
30
|
- Which tasks are a session's own (a cancelled turn's note, a worker's restart hold, the finish wake below, the web's stop-task button): the ids its `task_create` results and its handed-over executes name (`TaskRuntime.created_ids_in`, `created_id`). An execute's id is read only after its first line (`Execute::HANDED_OVER`, through `TaskRuntime.handed_over_id`; the web has a JS twin in `task_stop.js`), not from a `task_id:` line inside a run, which the command can print. (A command that prints the `---` separator chi joins a turn's results with, then a hand-over line, still fakes one: a known gap.) Either result may start with the `ran as: …` line of a gate-corrected call.
|
|
31
31
|
- `task_get` returns metadata and `output_path`; use `read` for output contents.
|
|
@@ -40,12 +40,12 @@ Behavior:
|
|
|
40
40
|
(`--no-shared`) a line in its steering queue. The probe only peeks: the drain still merges the message at the
|
|
41
41
|
next boundary. The result says `note: a message from the user is waiting; the task is STILL RUNNING.`, how to wait
|
|
42
42
|
again or stop it (`TaskWait::MESSAGE_WAITING_NOTE`), and `waited: Ns`; the task keeps running and stays watched
|
|
43
|
-
(below). A task that finished meanwhile gives its finished result, as after a Stop. A `chi -p` turn has no probe.
|
|
43
|
+
(below). A task that finished meanwhile gives its finished result, as after a Stop. A `chi -p` turn has no probe. The same probe ends a blocking `delegate_result` (and `delegate`'s wait) early; see [Sessions: Delegating](../sessions.md).
|
|
44
44
|
- `task_create` accepts `env` as a JSON object string for deterministic overrides such as `PATH`; use an absolute interpreter path when that is simpler. Ruby/Bundler isolation variables remain protected.
|
|
45
45
|
- `execute` and `task_create` both spawn with chi's own Ruby/Bundler setup scrubbed (`TaskRuntime.sanitized_spawn_env`): `RUBYOPT` loses its `-r…bundler/setup` flags (others stay) and `RUBYLIB`, `BUNDLE_GEMFILE`, `BUNDLE_BIN_PATH`, `BUNDLER_VERSION` are unset, so a project's `ruby -e` or `bundle exec` uses its own Gemfile when chi runs under `bundle exec`. `task_create` refuses those keys in the model's `env`; `execute`'s `env` comes from chi's callers (`Builtins`, `!cmd`), not the model, and is set over the scrub. An `execute` handed over at its limit keeps the process it spawned, so the task runs with the same env.
|
|
46
46
|
- `execute` and `task_create` export chi's own markers into the command, set over the model's `env` (which can't set them: `Error: env key is reserved`): `SAMAGOTCHI_PARENT_SESSION` (the session's id, `chi` without one; a chi started there answers as a parent agent), `SAMAGOTCHI_SESSION_MODEL` (the resolved ref the session runs on, read at each call so it follows `/model`; unset when unknown) `SAMAGOTCHI_SESSION_THINKING` (the session's own thinking level; unset when it has none) and `SAMAGOTCHI_SESSION_REASONING_HISTORY` (its own reasoning_history level, the same way). `chi self` reads the last three for its model, thinking level and reasoning history. The four are `ChildEnv::SESSION_MARKERS` (`TaskRuntime::MARKER_ENV_KEYS`), which a spawned worker unsets (`ChildEnv.unset`, merged into its spawn env). A session's worker never inherits them from whoever started it (a `chi send` run by another session's `execute`): its other commands (a user's `!cmd`, an MCP server, a context command) have none.
|
|
47
47
|
- They also pass on a set `XDG_STATE_HOME` / `XDG_CONFIG_HOME` made absolute (`Builtins.xdg_absolute`, via `Paths`), so a chi the command runs in another `cwd` (`chi send`) reads this chi's state and config. These aren't reserved: an XDG dir the model sets in `task_create`'s `env` stays its own.
|
|
48
|
-
- Every runner starts from the session's project env, `ProjectEnv.tools_env`: the overlay a spawned worker computed once at boot (`SessionManager.apply_project_env` → `ProjectEnv.compute` → `ProjectEnv.install!`; its folder's `mise env`, and the chi shim's dir first on PATH so `chi` is the chi home's), less `ProjectEnv::TOOLS_DROP` (`RUBYOPT`, `RUBYLIB`, Bundler's: the scrub above sets those). Outside a worker (`ProjectEnv::NONE`) it is `{}`. The sites: `execute` and `task_create` (`Tools::Builtins#project_env`, under the markers and the model's `env`; a participant's turn gets `{}`), the user's `!cmd` (`SessionCommands#shell`) and a context source's command (`ContextFetch#env_for`)
|
|
48
|
+
- Every runner starts from the session's project env, `ProjectEnv.tools_env`: the overlay a spawned worker computed once at boot (`SessionManager.apply_project_env` → `ProjectEnv.compute` → `ProjectEnv.install!`; its folder's `mise env`, and the chi shim's dir first on PATH so `chi` is the chi home's), less `ProjectEnv::TOOLS_DROP` (`RUBYOPT`, `RUBYLIB`, Bundler's: the scrub above sets those). Outside a worker (`ProjectEnv::NONE`) it is `{}`. The sites: `execute` and `task_create` (`Tools::Builtins#project_env`, under the markers and the model's `env`; a participant's turn gets `{}`), the user's `!cmd` (`SessionCommands#shell`), and through `ProjectEnv.spawn_env` (the scrub above, then `tools_env`: what `execute` spawns with, for the runners that don't go through it) a context source's command (`ContextFetch#env_for`) and an MCP server, under the server's own keys (`bundles/mcp/plugin.rb` `server_env`, from mcp 0.7.4; no participant rule: the server is the session's). chi's own process env is never changed, and the project env never enters the prompt. User docs: [Project env](../sessions.md#project-env); the launch side: [How chi launches itself](../architecture.md#how-chi-launches-itself).
|
|
49
49
|
|
|
50
50
|
## Finish wake
|
|
51
51
|
|
data/docs/internals/engine.md
CHANGED
|
@@ -23,7 +23,8 @@ Engine#run_turn(session, prompt, on_event:, max_iterations:, cancel_controller:,
|
|
|
23
23
|
│ # turn.resume_from = resume_from (its run's start; nil: it starts cold))
|
|
24
24
|
│ # @session = session, session.status = RUNNING, used memories absorbed,
|
|
25
25
|
│ # TurnState#begin!(controller:, sink:, input_waiting:) (a new CancellationController when none
|
|
26
|
-
│ # is given; input_waiting: the caller's peek, Engine#input_waiting?, which task_wait
|
|
26
|
+
│ # is given; input_waiting: the caller's peek, Engine#input_waiting?, which task_wait,
|
|
27
|
+
│ # delegate_result's wait and execute's early hand-over poll),
|
|
27
28
|
│ # a recap in flight invalidated, GuardrailWiring#begin_turn(origin)
|
|
28
29
|
│ # Returns a Turn {session, prompt (nil on a continue), continue, on_event, controller,
|
|
29
30
|
│ # origin, started_at, id (the caller's id:, else a new uuid), resumed, resume_from,
|
|
@@ -48,9 +48,9 @@ Configuration (config.yml `execute.*` keys and CLI flags too, as for `read`):
|
|
|
48
48
|
- `SAMAGOTCHI_EXECUTE_PREVIEW_BYTES` (default `12288`): total preview budget split across head and tail.
|
|
49
49
|
- `SAMAGOTCHI_EXECUTE_TELEMETRY_THRESHOLD_PCT` (default `80`): include estimated output token impact only when threshold is crossed.
|
|
50
50
|
- `SAMAGOTCHI_EXECUTE_TIMEOUT_SEC` (`execute.timeout_sec`, default `120`): what a command still running then gets is `execute.on_timeout`.
|
|
51
|
-
- `SAMAGOTCHI_EXECUTE_ON_TIMEOUT` (`execute.on_timeout`, default `task`): `task` hands the command over as a background task, not killed; the result starts `Still running at the 120s limit: moved to a background task, not killed.` (`Execute::HANDED_OVER`), then its `task_id:` and the tails of stdout and stderr so far, and `task_wait` waits for it. `kill` stops it and answers `Error: command timed out after …s` (`Execute::TIMEOUT_PREFIX`) with the hint to use `task_create`. The `!cmd` bang always kills. See [Background tasks](background-tasks.md).
|
|
51
|
+
- `SAMAGOTCHI_EXECUTE_ON_TIMEOUT` (`execute.on_timeout`, default `task`): `task` hands the command over as a background task, not killed; the result starts `Still running at the 120s limit: moved to a background task, not killed.` (`Execute::HANDED_OVER`), then its `task_id:` and the tails of stdout and stderr so far, and `task_wait` waits for it. `kill` stops it and answers `Error: command timed out after …s` (`Execute::TIMEOUT_PREFIX`) with the hint to use `task_create`. The `!cmd` bang always kills. With `task`, a message from the user waiting for the turn hands a command over early, after 3 s, with the first line `Still running at the 4s mark: a message from the user is waiting, …` (same `Execute::HANDED_OVER` start; never under `kill`). See [Background tasks](background-tasks.md).
|
|
52
52
|
|
|
53
|
-
The env a command runs in (the session's project env, `ProjectEnv.tools_env`, then the Ruby/Bundler scrub and chi's markers) is the same for `execute`, `task_create`, `!cmd
|
|
53
|
+
The env a command runs in (the session's project env, `ProjectEnv.tools_env`, then the Ruby/Bundler scrub and chi's markers) is the same for `execute`, `task_create`, `!cmd`, context sources and MCP servers (`ProjectEnv.spawn_env`): see [Background tasks](background-tasks.md).
|
|
54
54
|
|
|
55
55
|
Implementation note:
|
|
56
56
|
|
data/docs/internals/web-push.md
CHANGED
|
@@ -169,6 +169,26 @@ each `session` / `session_gone` event into a `Show` (with its text) or a `Close`
|
|
|
169
169
|
Queue. A `chi` event marked `update_notice` becomes a `chi:update:<version>` show for the bare page; the checker
|
|
170
170
|
sets that marker only for a newly selected version not already in its persistent `notified` list. It uses the same
|
|
171
171
|
Presence skip and hourly CAP as session shows, and the 24-hour TTL lets an offline phone receive it later.
|
|
172
|
+
With `update.auto: install` (and an install that is not blocked) that marker is ignored: the page says the release
|
|
173
|
+
installs by itself, and the phone hears the result instead.
|
|
174
|
+
|
|
175
|
+
**The automatic update's shows.** `Web::AutoUpdate` says how a run went with `SessionHub#publish_notice`, a
|
|
176
|
+
`chi_notice` hub event (`{kind, version, …notes}`): not a `chi_update` frame, which is the page's (it would repeat
|
|
177
|
+
in a reconnect's snapshot and toast), and not kept. The Notifier turns it into a show with reason `update` (24 h TTL,
|
|
178
|
+
`Urgency: normal`, counted in CAP, skipped with the key marked handled while a page is in front, shown for the bare
|
|
179
|
+
page `/`), once per key in its process (`@updates_seen`; AutoUpdate's own record in `update_check.json` keeps a run
|
|
180
|
+
from saying it twice across restarts). The key's `chi:` prefix keeps sw.js from treating it as a waiting key:
|
|
181
|
+
|
|
182
|
+
| `kind` | key | title / body |
|
|
183
|
+
|---|---|---|
|
|
184
|
+
| `installed` (`update.restart: never`) | `chi:installed:<X>` | "chi X is installed" / "chi web and its sessions switch over when you restart them." + kept edits |
|
|
185
|
+
| `failed` | `chi:update_failed:<X>` | "chi X didn't install" (or "installed with a problem" when the gem row was fine) / the first failed row's status, "Open chi web for details." |
|
|
186
|
+
| `updated` | `chi:updated:<X>` | "chi updated to X" / "chi web and 3 sessions restarted; 1 restarts when idle." + kept edits |
|
|
187
|
+
| `restart_failed` | `chi:restart_failed:<X>` | "chi X is installed; chi web didn't restart" / why (a failed `--version` preflight, no wrapper), "Restart chi web in its terminal." |
|
|
188
|
+
|
|
189
|
+
`updated` is sent by the **restarted** chi web (its AutoUpdate finds the run at `phase: web`): the exec ends the
|
|
190
|
+
old process, and `Notifier#stop` does not flush the shows still in their hold, so a push queued just before it would
|
|
191
|
+
be lost. Sending from the new process also proves it came up.
|
|
172
192
|
|
|
173
193
|
**Seeding**: `Notifier#start(hub)` subscribes with `snapshot: true` and takes the summaries as the seed, so what is
|
|
174
194
|
open already never pushes. The hub's projection is empty before its first scan, so `Server.start` runs `hub.scan`
|
data/docs/sessions.md
CHANGED
|
@@ -8,7 +8,7 @@ Sessions are plain files — no DB. Each session is `~/.local/state/samagotchi/s
|
|
|
8
8
|
|
|
9
9
|
**Revise stacks:** a web Revise turn (POST /turn `revises: {turn_id}`, the answer it rewrites) stamps its opening message (its prompt) with `revises: {turn_id}` at the turn's end, only when the turn completed with an answer (`AnswerTail`): a failed, canceled or empty turn revised nothing (`Engine#stamp_opening`). A draft no prompt carries (a stale page, a rolled-back turn) is no target: no stamp. A draft a stamped turn already revised is re-targeted to its stack's top, the newest version, so a stack is a chain `v1 ← v2 ← v3`. Like `verify`, it is metadata only, never added to the prompt (no change to a request's bytes); chi web's history passes it on the opening message (and on a wake turn's note), and `:turn_completed` carries the stamped `revises` (the participant view's too). The history also marks a Verify turn's opening `verify: true`, and so does its live end event (`:turn_completed`'s `verify: true`, with its `turn_id`): the page offers no Revise on its answer. The web page draws the stacks from these stamps (docs/cli.md, Revise). Like a Verify turn's, a Revise message waits for the running turn's end (`SessionInbox.turn_end_data?`; the Bridge takes either as `delivery: queue`, so it never cuts and its ack and `:turn_enqueued` say it queues), is sent only to a worker whose `bridge.json` names the `revise` feature (else 409 `revise_unavailable`), and a failed turn gives it back (`:prompt_restored`'s `revises`). Participants can't Revise: chi web and the Bridge drop it from their turns. Both anchors travel as one `TurnAnchors` inside chi; on the wire and in an input file each keeps its own key (`verify`, `revises`).
|
|
10
10
|
|
|
11
|
-
**Input files:** a message for a session's worker waits in `input/<timestamp>.json` (`SessionInbox.write_input`: `prompt`, `client_id`, `enqueued_id`, and `images`, `no_interrupt` and `delivery` when they apply; `delivery` is written only for `"queue"`). While a turn runs, the worker merges them at each step boundary in name order, except a queued one or one with images: those wait for the turn's end and run one turn each, in name order, and a file after them still merges (`Worker#pending_input_drain`). Such a file, with a prompt, from the user, `chi send` or a delegating parent, also ends a running `task_wait` (`SessionInbox.steer_waiting?`, a peek the worker's turn polls through `Engine#input_waiting?`; never in a participant's turn): the file stays for the drain. A queue is sent only to a worker whose `bridge.json` names the `delivery` feature; an older one would steer it, so `SessionManager.deliver_turn` refuses it (409 `delivery_unavailable`), and any other 4xx the Bridge answers is a refusal too, never a fallback file (except a 413, a body over its size limit: that one still goes in as a file, its `delivery` kept). A session command recognized in a file runs without a turn; its outcome is kept at `input/settled/<enqueued_id>` (`completed`, `failed` for a command error, `canceled` with `cancel_reason` `busy` or `dropped` for one refused mid-turn or dropped as the worker left; `origin: command`) so `chi send --wait` ends instead of waiting for a turn that will not run, its detail saying `the command failed` or `the command was refused (busy)` rather than a turn's words. **Reply files:** a turn that ends with visible text leaves its reply in `output/<timestamp>.txt`, or `output/<timestamp>-<enqueued_id>.txt` when the turn ran an input file with an id (`SessionInbox.write_output`), and the session's `last_turn` keeps that `enqueued_id`; so a wait for one queued message (`chi send --queue --wait`, `ReplyWait`'s `turn:`) finds its own turn's reply or end, never another turn's. The names still sort by time, and every reader takes `*.txt`.
|
|
11
|
+
**Input files:** a message for a session's worker waits in `input/<timestamp>.json` (`SessionInbox.write_input`: `prompt`, `client_id`, `enqueued_id`, and `images`, `no_interrupt` and `delivery` when they apply; `delivery` is written only for `"queue"`). While a turn runs, the worker merges them at each step boundary in name order, except a queued one or one with images: those wait for the turn's end and run one turn each, in name order, and a file after them still merges (`Worker#pending_input_drain`). Such a file, with a prompt, from the user, `chi send` or a delegating parent, also ends a running `task_wait` or `delegate_result` and hands a running `execute` over as a task after 3 s (`SessionInbox.steer_waiting?`, a peek the worker's turn polls through `Engine#input_waiting?`; never in a participant's turn): the file stays for the drain. A queue is sent only to a worker whose `bridge.json` names the `delivery` feature; an older one would steer it, so `SessionManager.deliver_turn` refuses it (409 `delivery_unavailable`), and any other 4xx the Bridge answers is a refusal too, never a fallback file (except a 413, a body over its size limit: that one still goes in as a file, its `delivery` kept). A session command recognized in a file runs without a turn; its outcome is kept at `input/settled/<enqueued_id>` (`completed`, `failed` for a command error, `canceled` with `cancel_reason` `busy` or `dropped` for one refused mid-turn or dropped as the worker left; `origin: command`) so `chi send --wait` ends instead of waiting for a turn that will not run, its detail saying `the command failed` or `the command was refused (busy)` rather than a turn's words. **Reply files:** a turn that ends with visible text leaves its reply in `output/<timestamp>.txt`, or `output/<timestamp>-<enqueued_id>.txt` when the turn ran an input file with an id (`SessionInbox.write_output`), and the session's `last_turn` keeps that `enqueued_id`; so a wait for one queued message (`chi send --queue --wait`, `ReplyWait`'s `turn:`) finds its own turn's reply or end, never another turn's. The names still sort by time, and every reader takes `*.txt`.
|
|
12
12
|
|
|
13
13
|
**Removing a queued message:** a message queued for after the running turn (`chi send --queue`, the web's Alt+Enter, `/queue TEXT` in a terminal), or one with images (it waits for the turn's end too), can be taken back while it still waits: the worker's Bridge (`POST /session/:id/turn/dequeue {enqueued_id}`, the `unqueue` feature) deletes its input file and every UI drops it (`:turn_dequeued`). It is a race with the turn's end, decided by the same rename the worker uses to take the file: once a turn took it the answer is `409 already_started` (it runs), and an id the worker no longer knows is `404 unknown_enqueued_id`. A plain message sent without `--queue` is not removable (`409 not_queued`): a running turn's next step may take it at any moment. A wait for its turn (`chi send --queue --wait`, a delegate follow-up's wait) ends at once with `removed` (the tombstone `input/removed/<enqueued_id>` says so), and a `chi -p` run whose queued prompt went ends with exit 1. The `200` carries the removed `prompt`: the web and the attached terminal put it back into the composer to edit (not its images). The images a removed message named stay in the session's `images/`. Not removable: a queued session command (`/queue /model x`: it waits in the worker, not in `input/`), a message in a stopped worker's inbox (the next worker runs it), and the plain REPL's (`--no-shared`) own queue.
|
|
14
14
|
|
|
@@ -386,7 +386,8 @@ mise.toml pins Ruby 3.4 gets that `ruby`, `gem` and `bundle` in its tools while
|
|
|
386
386
|
belong to the starter's Ruby), unless mise sets its own; kept when the Ruby stays the same.
|
|
387
387
|
- **Never what a `chi` in a tool reads.** The tools' env never takes `HOME`, any `XDG_*`, any `SAMAGOTCHI_*`,
|
|
388
388
|
`CHI_REMOTE`, `RACK_ENV` or `CI` from the project (a `chi` run there would read another config or state dir, or
|
|
389
|
-
send its session commands to another machine)
|
|
389
|
+
send its session commands to another machine), nor `TMPDIR` (the guardrails judge a tool's `$TMPDIR/x` and its
|
|
390
|
+
tmp dirs by the worker's own `TMPDIR`, so the tools run on that one): they are dropped
|
|
390
391
|
and the debug log names them once (`project_env_reserved`). `mise env` runs without `MISE_YES` (it would trust an
|
|
391
392
|
untrusted mise.toml for you) and `CI`; your `MISE_TRUSTED_CONFIG_PATHS` stays.
|
|
392
393
|
- **Only this session.** A worker started from this one (a restart, a delegate child) starts on the worker's own
|
|
@@ -403,7 +404,7 @@ mise.toml pins Ruby 3.4 gets that `ruby`, `gem` and `bundle` in its tools while
|
|
|
403
404
|
included, asks you first; a read-only mention (`find . -name mise.toml`) doesn't. `.envrc` and mise's local files
|
|
404
405
|
(`mise.local.toml`, `mise.<env>.local.toml`, dotted or not, and `config.local.toml` / `config.<env>.local.toml` in
|
|
405
406
|
`.config/mise/`, `mise/`, `.mise/`) are secrets to them, as `.env` is.
|
|
406
|
-
- **What tools still drop**: every `execute`, task
|
|
407
|
+
- **What tools still drop**: every `execute`, task, `!cmd`, context source and MCP server takes no `RUBYOPT`, `RUBYLIB`, `BUNDLE_GEMFILE`,
|
|
407
408
|
`BUNDLE_BIN_PATH` or `BUNDLER_VERSION` from the project's mise env: it runs without `RUBYLIB` and Bundler's
|
|
408
409
|
variables, and with the worker's own `RUBYOPT`, Bundler's part removed (`TaskRuntime`).
|
|
409
410
|
- **`chi` in a tool** stays the home's: with a chi home the tools' PATH starts with `$XDG_STATE_HOME/samagotchi/bin`,
|
|
@@ -428,6 +429,7 @@ The `delegate` tool hands a task to a **child session**: an ordinary chi session
|
|
|
428
429
|
- `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.
|
|
429
430
|
- `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.
|
|
430
431
|
- `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.
|
|
432
|
+
- 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`).
|
|
431
433
|
- 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`.
|
|
432
434
|
- What a wait returns, `session: <id>` then `status:` and the text, with the status words `chi send --wait --format json` uses: `answered` with the reply after a `---` line; `question` as soon as the child opens a question (the model's own, or a hook's), with the question's text, options and the `chi answer` command for it (nothing is answered for it: `chi answer`, attach or use the web, then wait again); `failed`, `canceled` or `no_answer` when the child's turn ended with nothing to say (its session shows what happened); `error` (its worker crashed) or `stopped` (`chi sessions stop`) at once; `worker_gone` when the child has had no worker for 15 seconds (it crashed without saying so, or idle-exited): `delegate` with `session:` starts it again; `running` on timeout, or when the parent's own turn is canceled (the child keeps running; the next turn can wait again). A reply saved before the child's worker idle-exited is returned at once.
|
|
433
435
|
- **A child's step-limit question goes back to the parent model**, not to its user: `delegate_result` returns `status: question` with the `chi answer` commands for Continue and Stop, and says the model may instead send a narrower follow-up (`delegate session:`, which drops the question) or report back. A parent's `chi answer` follows `guardrails.parent_answers`, or the child's own when stricter (`hard`: Stop only; see [chi as a sub-agent](sub-agent.md#the-step-limit)).
|
|
@@ -447,17 +447,17 @@ module Samagotchi
|
|
|
447
447
|
@stdout.puts "Bundle '#{name}' not installed."
|
|
448
448
|
return 0
|
|
449
449
|
end
|
|
450
|
-
@stdout.puts "Bundle: #{name} v#{st
|
|
451
|
-
if st
|
|
450
|
+
@stdout.puts "Bundle: #{name} v#{st.provenance.version}#{dev_label(st.provenance)} scope=#{status_scope(st)} installed=#{st.provenance.installed_at}"
|
|
451
|
+
if st.scope_error
|
|
452
452
|
@stdout.puts "Target: (unknown scope: this chi can't resolve it; upgrade chi or reinstall the bundle)"
|
|
453
453
|
else
|
|
454
|
-
@stdout.puts "Target: #{st
|
|
454
|
+
@stdout.puts "Target: #{st.target_dir}"
|
|
455
455
|
end
|
|
456
|
-
st
|
|
456
|
+
st.files.each do |k, info|
|
|
457
457
|
next @stdout.puts(" #{k}: unchecked") if info[:unchecked]
|
|
458
458
|
|
|
459
459
|
mods = []
|
|
460
|
-
mods << "conflict (kept your edits over v#{st
|
|
460
|
+
mods << "conflict (kept your edits over v#{st.provenance.version}: chi bundle diff #{name} #{k})" if info[:conflict]
|
|
461
461
|
mods << "modified" if info[:modified]
|
|
462
462
|
mods << "missing" if info[:missing]
|
|
463
463
|
mods << "no-index" unless info[:index_present] || info[:overlay]
|
|
@@ -466,9 +466,9 @@ module Samagotchi
|
|
|
466
466
|
@stdout.puts " #{k}: #{label}"
|
|
467
467
|
end
|
|
468
468
|
# Hooks
|
|
469
|
-
hooks = st
|
|
470
|
-
trust = st
|
|
471
|
-
commit = st
|
|
469
|
+
hooks = st.provenance.hooks
|
|
470
|
+
trust = st.provenance.trust_level || "experimental"
|
|
471
|
+
commit = st.provenance.source_commit
|
|
472
472
|
@stdout.puts " trust_level: #{trust}" if hooks.any? || trust
|
|
473
473
|
@stdout.puts " source_commit: #{commit}" if commit
|
|
474
474
|
if hooks.any?
|
|
@@ -478,17 +478,17 @@ module Samagotchi
|
|
|
478
478
|
exists = File.exist?(hook_path) ? "ok" : "missing"
|
|
479
479
|
@stdout.puts " #{hk}: #{hook.events_field} on_error=#{hook.on_error} priority=#{hook.priority} [#{exists}]"
|
|
480
480
|
end
|
|
481
|
-
@stdout.puts " requires_chi: #{st
|
|
482
|
-
@stdout.puts " not loaded: #{st
|
|
481
|
+
@stdout.puts " requires_chi: #{st.provenance.requires_chi}" if st.hooks_requires_failure
|
|
482
|
+
@stdout.puts " not loaded: #{st.hooks_requires_failure}" if st.hooks_requires_failure
|
|
483
483
|
end
|
|
484
|
-
if (plugin = st
|
|
484
|
+
if (plugin = st.plugin)
|
|
485
485
|
note = plugin[:state] == "modified" ? " (edited after install: not loaded; reinstall the bundle)" : ""
|
|
486
486
|
@stdout.puts " Plugin: #{plugin[:file]} [#{plugin[:state]}]#{note}"
|
|
487
487
|
@stdout.puts " requires_chi: #{plugin[:requires_chi]}" if plugin[:requires_chi]
|
|
488
488
|
@stdout.puts " not loaded: #{plugin[:requires_failure]}" if plugin[:requires_failure]
|
|
489
489
|
end
|
|
490
|
-
st
|
|
491
|
-
(st
|
|
490
|
+
st.needs.each { |need| @stdout.puts " #{Samagotchi::MemoryBundle::Status.need_line(need)}" }
|
|
491
|
+
(st.provenance.includes || []).each do |member|
|
|
492
492
|
state = Samagotchi::MemoryBundle::Provenance.new(name: member).installed? ? "installed" : "not installed: chi bundle install #{member}"
|
|
493
493
|
@stdout.puts " includes #{member} [#{state}]"
|
|
494
494
|
end
|
|
@@ -499,17 +499,17 @@ module Samagotchi
|
|
|
499
499
|
next @stdout.puts(" #{bname} (manifest.json unreadable)") if record.error?
|
|
500
500
|
|
|
501
501
|
st = Samagotchi::MemoryBundle::Status.bundle_status(bname)
|
|
502
|
-
mods = st
|
|
503
|
-
hooks_count = st
|
|
502
|
+
mods = st.files.values.count { |v| v[:modified] || v[:missing] }
|
|
503
|
+
hooks_count = st.provenance.hooks.size
|
|
504
504
|
hook_info = hooks_count > 0 ? " hooks=#{hooks_count}" : ""
|
|
505
|
-
plugin = st
|
|
505
|
+
plugin = st.plugin
|
|
506
506
|
mods += 1 if plugin && (plugin[:state] != "ok" || plugin[:requires_failure])
|
|
507
|
-
mods += 1 if st
|
|
508
|
-
mods += 1 if st
|
|
507
|
+
mods += 1 if st.hooks_requires_failure
|
|
508
|
+
mods += 1 if st.scope_error
|
|
509
509
|
plugin_info = plugin ? " plugin=#{plugin[:file]}" : ""
|
|
510
|
-
includes = st
|
|
510
|
+
includes = st.provenance.includes
|
|
511
511
|
includes_info = includes ? " includes=#{includes.join(",")}" : ""
|
|
512
|
-
@stdout.puts " #{bname} v#{st
|
|
512
|
+
@stdout.puts " #{bname} v#{st.provenance.version}#{dev_label(st.provenance)} scope=#{status_scope(st)} files=#{st.files.size}#{hook_info}#{plugin_info}#{includes_info} issues=#{mods}"
|
|
513
513
|
end
|
|
514
514
|
end
|
|
515
515
|
0
|
|
@@ -523,7 +523,7 @@ module Samagotchi
|
|
|
523
523
|
end
|
|
524
524
|
|
|
525
525
|
# "team (unknown)" for a scope this chi can't resolve (Status#bundle_status).
|
|
526
|
-
def status_scope(st) = st
|
|
526
|
+
def status_scope(st) = st.scope_error ? "#{st.scope} (unknown)" : st.scope
|
|
527
527
|
|
|
528
528
|
def diff(rest)
|
|
529
529
|
args = rest.reject { |a| a.start_with?("--") }
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mcp
|
|
3
|
-
version: 0.7.
|
|
3
|
+
version: 0.7.4
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Tools from MCP servers (stdio): each server in config.yml bundles: mcp: servers: runs for the session; their tools go into chi's tool index: the model searches them with find_tools and calls one with call_tool (<server>/<tool>); /mcp lists them"
|
|
6
6
|
trust_level: reviewed
|
|
7
7
|
files: {}
|
|
8
8
|
plugin:
|
|
9
9
|
file: plugin.rb
|
|
10
|
-
sha256: sha256:
|
|
10
|
+
sha256: sha256:b892de35b1fc16dd22152cd95a6c90447a2be45f4daff04a4648ab2653463a20
|
|
11
11
|
# 0.67.0 is the release with chi.tool_source (the core tool index the bundle publishes into).
|
|
12
12
|
requires_chi: ">= 0.121.0"
|