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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -1
  3. data/docs/architecture.md +33 -7
  4. data/docs/cli.md +6 -2
  5. data/docs/configuration.md +60 -5
  6. data/docs/internals/background-tasks.md +3 -3
  7. data/docs/internals/engine.md +2 -1
  8. data/docs/internals/tool-guardrails.md +2 -2
  9. data/docs/internals/web-push.md +20 -0
  10. data/docs/sessions.md +5 -3
  11. data/lib/samagotchi/bundle_command.rb +21 -21
  12. data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
  13. data/lib/samagotchi/bundles/mcp/plugin.rb +8 -3
  14. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  15. data/lib/samagotchi/bundles/system/self_map.md +2 -2
  16. data/lib/samagotchi/cancellation_controller.rb +4 -4
  17. data/lib/samagotchi/config.rb +4 -0
  18. data/lib/samagotchi/context_fetch.rb +2 -2
  19. data/lib/samagotchi/cut.rb +21 -0
  20. data/lib/samagotchi/cut_policy.rb +6 -5
  21. data/lib/samagotchi/engine.rb +4 -2
  22. data/lib/samagotchi/engine_peers.rb +2 -1
  23. data/lib/samagotchi/hook_routes.rb +3 -2
  24. data/lib/samagotchi/kernel_loop.rb +8 -6
  25. data/lib/samagotchi/launch_mode.rb +5 -2
  26. data/lib/samagotchi/llm/chat_loop.rb +3 -2
  27. data/lib/samagotchi/llm/openai_chat.rb +1 -1
  28. data/lib/samagotchi/memory_bundle/status.rb +13 -3
  29. data/lib/samagotchi/project_env.rb +16 -2
  30. data/lib/samagotchi/reply_wait/poll.rb +9 -2
  31. data/lib/samagotchi/reply_wait.rb +9 -3
  32. data/lib/samagotchi/steer_cut.rb +2 -1
  33. data/lib/samagotchi/task_finish_note.rb +8 -5
  34. data/lib/samagotchi/task_watch.rb +11 -6
  35. data/lib/samagotchi/testing/boundary.rb +3 -1
  36. data/lib/samagotchi/tool_activity.rb +3 -3
  37. data/lib/samagotchi/tool_declarations.rb +2 -2
  38. data/lib/samagotchi/tools/builtins.rb +2 -1
  39. data/lib/samagotchi/tools/delegate_wait.rb +23 -2
  40. data/lib/samagotchi/tools/execute.rb +51 -9
  41. data/lib/samagotchi/tools/task_runtime.rb +1 -1
  42. data/lib/samagotchi/turn_note.rb +6 -6
  43. data/lib/samagotchi/version.rb +1 -1
  44. data/lib/samagotchi/web/app.rb +30 -158
  45. data/lib/samagotchi/web/auto_update.rb +282 -0
  46. data/lib/samagotchi/web/public/app.js +6 -4
  47. data/lib/samagotchi/web/public/composer.css +1 -1
  48. data/lib/samagotchi/web/public/index.html +10 -9
  49. data/lib/samagotchi/web/public/notifications.js +10 -1
  50. data/lib/samagotchi/web/public/revise_stack.js +6 -5
  51. data/lib/samagotchi/web/public/task_stop.js +3 -3
  52. data/lib/samagotchi/web/public/update_notice.js +10 -4
  53. data/lib/samagotchi/web/public/update_sheet.js +3 -2
  54. data/lib/samagotchi/web/push/notifier.rb +66 -5
  55. data/lib/samagotchi/web/server.rb +45 -1
  56. data/lib/samagotchi/web/session_access_routes.rb +1 -1
  57. data/lib/samagotchi/web/session_hub.rb +20 -3
  58. data/lib/samagotchi/web/update_check.rb +56 -10
  59. data/lib/samagotchi/web/update_ops.rb +240 -0
  60. data/lib/samagotchi/worker_task_wakes.rb +1 -1
  61. metadata +4 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 57a409114b8194ae2d17416055b7f52c468e6a2ab5a650d8f36a4b6a0e0b0b3f
4
- data.tar.gz: 78f20cbc62ab65e54c300bad75ec2478388810c1328679c3c48d3842f80cc0a3
3
+ metadata.gz: 1ef79d43ec1d5fc441ca8892ceb240a42fca4629295e71debc495df44b5efde8
4
+ data.tar.gz: 39b1d809ff216fcd1a1af146dc7858b45e80f94a8018e00841839573b753be0d
5
5
  SHA512:
6
- metadata.gz: e285adda4382592d1cca2811e05eebdc5999d5808fc0f23a0ff7f73a87db6b4cbb504cbc43647feab56db006b66c7439d075398ef2a8eae6339bae3d42824887
7
- data.tar.gz: c796ce3b9121c74bf838959f4f1ea97b9213930bd449922f42f617afab92c8eb38f685b7098f9c67c4d20ab1622e51b7fbed99de24a71964159fa892ecec827f
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.138.0...HEAD
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::App#update_argv` (the **Update** button's child) | `CHI_BOOT -- update --to V --no-desktop --json` | `[RbConfig.ruby, wrapper, update, …]` |
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}`; `/api/info` and the session-command wake response
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), its
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` starts no checker thread and makes no network
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` (`handle_restart_workers`) starts the worker pass
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
@@ -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 keeps running, the model is told a message waits,
1147
- reads it at once and decides whether to wait again. A plugin's steer and a delegate child's report don't end one.
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`). MCP servers take `ProjectEnv.current.env` whole under the server's own keys (`bundles/mcp/plugin.rb` `server_env`: no `TOOLS_DROP`, no participant rule). 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).
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
 
@@ -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 polls),
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` and context sources: see [Background tasks](background-tasks.md).
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
 
@@ -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): they are dropped
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 and `!cmd` takes no `RUBYOPT`, `RUBYLIB`, `BUNDLE_GEMFILE`,
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[:provenance].version}#{dev_label(st[:provenance])} scope=#{status_scope(st)} installed=#{st[:provenance].installed_at}"
451
- if st[:scope_error]
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[:target_dir]}"
454
+ @stdout.puts "Target: #{st.target_dir}"
455
455
  end
456
- st[:files].each do |k, info|
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[:provenance].version}: chi bundle diff #{name} #{k})" if info[:conflict]
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[:provenance].hooks
470
- trust = st[:provenance].trust_level || "experimental"
471
- commit = st[:provenance].source_commit
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[:provenance].requires_chi}" if st[:hooks_requires_failure]
482
- @stdout.puts " not loaded: #{st[:hooks_requires_failure]}" if st[:hooks_requires_failure]
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[:plugin])
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[:needs].each { |need| @stdout.puts " #{Samagotchi::MemoryBundle::Status.need_line(need)}" }
491
- (st[:provenance].includes || []).each do |member|
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[:files].values.count { |v| v[:modified] || v[:missing] }
503
- hooks_count = st[:provenance].hooks.size
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[:plugin]
505
+ plugin = st.plugin
506
506
  mods += 1 if plugin && (plugin[:state] != "ok" || plugin[:requires_failure])
507
- mods += 1 if st[:hooks_requires_failure]
508
- mods += 1 if st[:scope_error]
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[:provenance].includes
510
+ includes = st.provenance.includes
511
511
  includes_info = includes ? " includes=#{includes.join(",")}" : ""
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}"
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[:scope_error] ? "#{st[:scope]} (unknown)" : st[:scope]
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
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:65532b75b33fa7a58b4a35f9ed32e2705f4e3d323c6f770e71448da5cc9314d0
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"