samagotchi 0.143.0 → 0.145.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +71 -1
  3. data/docs/bundle-authoring.md +1 -1
  4. data/docs/cli.md +21 -4
  5. data/docs/configuration.md +1 -1
  6. data/docs/guardrails.md +6 -3
  7. data/docs/hooks.md +5 -1
  8. data/docs/internals/bridge.md +2 -1
  9. data/docs/internals/bundles.md +1 -1
  10. data/docs/internals/engine.md +1 -1
  11. data/docs/internals/web-frontend.md +59 -2
  12. data/docs/plugins.md +4 -2
  13. data/docs/releasing.md +3 -2
  14. data/docs/testing.md +1 -1
  15. data/lib/samagotchi/bridge/sse_writer.rb +11 -1
  16. data/lib/samagotchi/bundle_command/check.rb +13 -2
  17. data/lib/samagotchi/bundle_settings_check.rb +34 -1
  18. data/lib/samagotchi/bundles/check-in/manifest.yml +4 -4
  19. data/lib/samagotchi/bundles/check-in/plugin.rb +13 -6
  20. data/lib/samagotchi/bundles/core/manifest.yml +2 -2
  21. data/lib/samagotchi/bundles/cost-cap/manifest.yml +4 -4
  22. data/lib/samagotchi/bundles/cost-cap/plugin.rb +4 -1
  23. data/lib/samagotchi/bundles/github-pr/manifest.yml +2 -2
  24. data/lib/samagotchi/bundles/github-pr/plugin.rb +4 -2
  25. data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +4 -2
  26. data/lib/samagotchi/bundles/source-links/manifest.yml +2 -2
  27. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  28. data/lib/samagotchi/bundles/work-cap/manifest.yml +4 -4
  29. data/lib/samagotchi/bundles/work-cap/plugin.rb +17 -8
  30. data/lib/samagotchi/context_status.rb +1 -1
  31. data/lib/samagotchi/engine.rb +17 -7
  32. data/lib/samagotchi/kernel_loop.rb +7 -0
  33. data/lib/samagotchi/live_versions.rb +28 -2
  34. data/lib/samagotchi/llm/chat_loop.rb +5 -0
  35. data/lib/samagotchi/llm/utf8_scrub.rb +5 -3
  36. data/lib/samagotchi/plugin/api.rb +2 -2
  37. data/lib/samagotchi/plugin/context.rb +3 -2
  38. data/lib/samagotchi/plugin/peer.rb +1 -1
  39. data/lib/samagotchi/plugin_session_state.rb +6 -3
  40. data/lib/samagotchi/self_report.rb +15 -13
  41. data/lib/samagotchi/session_access/local.rb +7 -10
  42. data/lib/samagotchi/session_commands.rb +2 -1
  43. data/lib/samagotchi/system_one/answer.rb +8 -5
  44. data/lib/samagotchi/system_one/client.rb +17 -0
  45. data/lib/samagotchi/system_one/http_backend.rb +23 -10
  46. data/lib/samagotchi/testing/boundary.rb +76 -0
  47. data/lib/samagotchi/testing/env_not_shown.rb +17 -0
  48. data/lib/samagotchi/testing.rb +1 -0
  49. data/lib/samagotchi/update_command.rb +8 -7
  50. data/lib/samagotchi/version.rb +1 -1
  51. data/lib/samagotchi/web/public/annotate_bar.js +1 -1
  52. data/lib/samagotchi/web/public/app.js +15 -3
  53. data/lib/samagotchi/web/public/chip_popover.js +14 -4
  54. data/lib/samagotchi/web/public/composer_fold.js +5 -2
  55. data/lib/samagotchi/web/public/composer_grip.js +45 -2
  56. data/lib/samagotchi/web/public/copy.js +1 -1
  57. data/lib/samagotchi/web/public/index.html +79 -40
  58. data/lib/samagotchi/web/public/info_bar_model.js +6 -4
  59. data/lib/samagotchi/web/public/more_sheet.js +212 -0
  60. data/lib/samagotchi/web/public/question_cards.js +26 -3
  61. data/lib/samagotchi/web/public/ref_scan.js +4 -2
  62. data/lib/samagotchi/web/public/stage_frame.js +58 -13
  63. data/lib/samagotchi/web/public/stage_model.js +2 -1
  64. data/lib/samagotchi/web/public/stage_view.js +52 -20
  65. data/lib/samagotchi/web/public/turn_block.js +43 -9
  66. data/lib/samagotchi/worker.rb +21 -4
  67. metadata +3 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0403d20c85725cdd309a9b0ff24cc997c3effd60f37e42c9258de568b5e89944
4
- data.tar.gz: aa1345c31e6d3ee3dbf578d6ed7298ec98f5c32caf67f304b75ac3caf3f2f886
3
+ metadata.gz: 6055a05533de1a37a9e9393effa720eb2ca93f0db2715d7f806f83cae3086a8d
4
+ data.tar.gz: ac0a46f034b62cf4c0b6968d6bb9b07fdf8bae8ab331f4745b4a597ef45dd8c9
5
5
  SHA512:
6
- metadata.gz: 528d2c989d71eb722a9e66ede6c2ddd4a57387e74842034bf019a671abb2f3b7d50cb759c8628f6d86d611434a6772a9a3fc75f5886d7f42eea98f3b54bcef1c
7
- data.tar.gz: d5078148478d31d778cb973d8ae7dac52696e5cfa9909f96b5ea33185d3c920f067aeea64417b7f64de1785047fc74f9c24bca3f4d4bfda75b792342c712ae22
6
+ metadata.gz: 7e0ab9f589c50aa6c3fa7bdb37b64e17d9963191fa1878e624dd33d68f296d31bd107a09eb8feb5ae7bd85b9c7f2e6bbf1ff1c296ae1fd938a828160c7c5a488
7
+ data.tar.gz: 34688115cd7554ed4f660ab131dfcccb4dd7d15e24f72c7e32f6802d1a2c422d80cfea3cef47f18afc689eb653fc36102b5726d45129202f157d91f1d1db09b7
data/CHANGELOG.md CHANGED
@@ -7,6 +7,74 @@ 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.145.0] - 2026-11-02
11
+
12
+ ### Changed
13
+
14
+ - On a phone, a web approval uses more of the screen, and a short one shows its question, command and Submit without
15
+ scrolling. A tall card can use the stage's raised height, as a long question already could.
16
+ - The stage shrinks back once at the next step after you answer a question or approval, so no empty room stays under the
17
+ answered card. Otherwise it still only grows during a turn.
18
+ - The web's approval card has no Deny radio. The Deny button denies in one tap, and a reason typed in the box now goes
19
+ with it. Terminals, `chi answer` and relayed approvals still answer Deny by its label.
20
+ - On a phone or touch screen, the composer folds while a card's text field has focus and comes back afterwards. Your
21
+ saved fold and draft are untouched.
22
+ - The session bar says "waiting" instead of "running" while a card blocks the turn.
23
+ - A live answer's new line appears at once instead of fading in, which takes about a quarter off the CPU a streaming
24
+ answer uses on a Mac Studio.
25
+
26
+ ### Fixed
27
+
28
+ - An approval card's folder and reason wrap at words instead of splitting them.
29
+ - The text fields on web cards ask iOS Safari not to offer its AutoFill Contact bar.
30
+
31
+ Update with `chi update`.
32
+
33
+ ## [0.144.0] - 2026-11-02
34
+
35
+ ### Changed
36
+
37
+ - `chi update`'s re-check of the bundles chi doesn't ship warns about a reference to chi's internals only when this chi
38
+ no longer has it: the constant doesn't resolve, or the method the name gives isn't there. The row reads
39
+ `warn boundary plugin.rb: Samagotchi::OldInternal::Thing is gone from this chi, so this bundle may fail`; a name that
40
+ still works (`Samagotchi::Log`) gets no row, so the table stops repeating warnings about bundles that load fine.
41
+ `chi bundle check` (the author's own run) stays strict.
42
+ - The web's session bar on a phone no longer swipes: id, status, model and ⚙ stay on the row, and ⋯ opens a bottom sheet with the
43
+ rest (toolkit, working set, participants, chain links, delegates, the telemetry, Continue, Archive, Stop, Delete). A dot on ⋯ says
44
+ when something there needs a look. See [docs/cli.md](docs/cli.md#the-session-bar-on-a-phone).
45
+ - chi web uses much less CPU during a turn: about a fifth of a core less on a Mac Studio while a question waits or the model
46
+ is silent, and less while text streams. The stage's dot and running tick are now a slow opacity blink instead of a pulsing
47
+ ring, the spinners turn in small steps, and while a question or an approval blocks the turn they stand still, so movement
48
+ means chi is working. An in-turn card like check-in's keeps its dot blinking, as that turn goes on.
49
+
50
+ ### Fixed
51
+
52
+ - An event with bytes JSON can't take (a plugin's card, say) no longer ends every stream that reaches it: it goes out scrubbed.
53
+ - A waiting approval on the web page opens at its question and command, on a phone and on a desktop, when it arrives and when the page
54
+ loads on it. A card too tall for the stage showed its options with what it asks scrolled out of sight; the options are now a scroll
55
+ down the stage's one scroll.
56
+ - `chi update` tells you a `chi web` started with `--port` runs old code; it only looked on `web.port`.
57
+ - `chi self` finds a `chi web` started with `--port`; it said `not running on port N` for `web.port`.
58
+ - `/guardrails` in chi web or an attached terminal shows all of its listing: a command's reply was cut at 4 KB, so with the core
59
+ bundle's rules System 1's section never showed and the text stopped mid-word. A reply now goes up to 64 KB, and a longer one ends
60
+ with a line saying it was cut.
61
+ - A `!` command that prints bytes that aren't valid UTF-8 no longer breaks the session's page and attached terminals: its output
62
+ stopped every stream that reached it, on each reconnect. The bad bytes show as `?`, as they do in the saved conversation.
63
+ - `/guardrails` no longer shows `System 1 (on):` when the settings mean nothing can be judged: it reads
64
+ `System 1 (on, but nothing is judged: system_one.host is not set):`, and likewise for a host that isn't in `hosts:`,
65
+ isn't `api: openai` or names no model.
66
+ - A `bundles:` entry in config.yml for a bundle that isn't installed (or a key it doesn't take) warns once, not in every new session,
67
+ respawn and delegate; a changed finding warns again, as does one fixed and then back.
68
+ - An answer with a line that opens and closes backticks itself (```` ```foo``` first ````) no longer loses every link after it: the source-links
69
+ and github-pr bundles and the web's file refs read it as inline code, not as a code block left open.
70
+ - A session whose cost-cap, work-cap or check-in saved nothing no longer leaves an empty folder under `plugins/<bundle>/sessions/`.
71
+ `ctx.session_dir(create: false)` is the path with no folder made, for a bundle that only reads. `chi update` gets them
72
+ (cost-cap 0.2.5, work-cap 0.1.5, check-in 0.3.3).
73
+ - A Stop that arrives on an empty reply ends the turn cancelled. It used to end
74
+ as "no answer: the model returned nothing".
75
+
76
+ Update with `chi update` (the core profile 0.4.3): check-in 0.3.3, cost-cap 0.2.5 and work-cap 0.1.5 (these three need this chi), github-pr 0.6.2 and source-links 0.6.2.
77
+
10
78
  ## [0.143.0] - 2026-11-01
11
79
 
12
80
  ### Added
@@ -5481,7 +5549,9 @@ and long-lived sessions.
5481
5549
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
5482
5550
  hotkey panel that send selected text or the clipboard to your sessions.
5483
5551
 
5484
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.143.0...HEAD
5552
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.145.0...HEAD
5553
+ [0.145.0]: https://github.com/dm1try/samagotchi/compare/v0.144.0...v0.145.0
5554
+ [0.144.0]: https://github.com/dm1try/samagotchi/compare/v0.143.0...v0.144.0
5485
5555
  [0.143.0]: https://github.com/dm1try/samagotchi/compare/v0.142.0...v0.143.0
5486
5556
  [0.142.0]: https://github.com/dm1try/samagotchi/compare/v0.141.0...v0.142.0
5487
5557
  [0.141.0]: https://github.com/dm1try/samagotchi/compare/v0.140.0...v0.141.0
@@ -121,7 +121,7 @@ end
121
121
  `bundle.http.stub` stubs `ctx.http`; `bundle.question_asked(text, question:)` fires `:question_asked` and returns
122
122
  the refs; `sandbox.config`, `sandbox.fake_executable` and `bundle.install` cover setup.
123
123
  The full list is in [plugins.md, Testing a bundle](plugins.md#testing-a-bundle-experimental). The kit is
124
- experimental (it may change before chi 1.0). If a test needs a fake of chi's own (a stub of a `Samagotchi::`
124
+ experimental (it may change before chi 1.0). A failing matcher that meets `ENV` prints `#<the process ENV, contents not shown>`, not your keys. If a test needs a fake of chi's own (a stub of a `Samagotchi::`
125
125
  class, a hand-built `ctx`), that is a gap in the kit: say so to the user rather than reaching into chi.
126
126
 
127
127
  ## Pitfalls
data/docs/cli.md CHANGED
@@ -156,7 +156,8 @@ The loop for a bundle you're writing (the whole path: [bundle-authoring.md](bund
156
156
  never fires), `settings` (warn: a `--set` the manifest's `settings:` rules out, as config.yml's `bundles:` is told), `api` (warn: a `ctx.*` / `chi.*` call or a keyword this chi's API lacks, read from the source with
157
157
  Prism against the API's own classes, on a path that never runs too; written on a local `ctx` / `chi` itself, so a
158
158
  `s = ctx.sessions` variable isn't followed; a name the plugin guards with `respond_to?` is left), `boundary` (warn: a `Samagotchi::` name outside "Stable for bundles"; a shipped bundle's listed
159
- exceptions aside) and `tests` (info: how to run them, or "no tests"). `--set KEY=VALUE` (repeatable, typed as
159
+ exceptions aside. On an installed bundle (`chi update`) only a name this chi no longer has is a row: the constant
160
+ doesn't resolve, or the method named isn't there) and `tests` (info: how to run them, or "no tests"). `--set KEY=VALUE` (repeatable, typed as
160
161
  `chi --set`) goes to `bundles: <name>:` in the sandbox's config.yml.
161
162
 
162
163
  ### Trying a bundle
@@ -314,7 +315,9 @@ done
314
315
  and each warning or error is a row under the bundle's name: `requires_chi`
315
316
  ruling this chi out, a plugin or hook that no longer loads, a hook on an event
316
317
  chi no longer fires, a `ctx`/`chi` call the API lacks, a reference to chi's
317
- internals, a name chi now ships, or a hook or plugin edited since it was installed (it
318
+ internals that this chi no longer has (`Samagotchi::OldInternal::Thing is gone from this chi, so this bundle may
319
+ fail` — a name that still resolves gets no row, so a bundle reaching `Samagotchi::Log` isn't warned about on every
320
+ update), a name chi now ships, or a hook or plugin edited since it was installed (it
318
321
  doesn't load). What only an author can act on (shas, unlisted files, tests, a
319
322
  missing `requires_chi`, its floor, `trust_level`) is left out, and so are
320
323
  profiles. A clean bundle gets no row beyond its `skipped (not from chi)`.
@@ -350,8 +353,8 @@ done
350
353
  restarts: `chi sessions stop ID`, and the next prompt starts it on the new
351
354
  chi); it exits on its own only when nothing is attached (no web tab, no
352
355
  terminal, no reminder) and `session.idle_exit_minutes` pass (never with 0).
353
- A new or woken worker always starts on the newest chi installed. A `chi web`
354
- on `web.port` running an older chi needs a restart (Ctrl-C, then `chi web`):
356
+ A new or woken worker always starts on the newest chi installed. A running
357
+ `chi web` (on the port it was started with, else `web.port`) on an older chi needs a restart (Ctrl-C, then `chi web`):
355
358
  its page says so too. One from before workers started on the newest chi
356
359
  also starts its new sessions on its own version until then (the row says so).
357
360
 
@@ -1397,6 +1400,20 @@ access token", paste the token there (the part of the link after
1397
1400
  phone. The same sheet lists the other devices (a long-turns-done switch each, Remove), and `chi web push ls|rm|test`
1398
1401
  does it from the Mac (see [Phone notifications](configuration.md#phone-notifications-web-push)).
1399
1402
 
1403
+ #### The session bar on a phone
1404
+
1405
+ At a phone's width (600 px or less) the session bar is one row with no swiping: the id (a tap copies it), the status, the model
1406
+ (cut short first), ⚙ and ⋯. ⋯ opens a bottom sheet with the rest:
1407
+
1408
+ - a row for what the row has no room for, each opening what its chip opens on a wider screen: the model and the 🧰 toolkit (a tap
1409
+ shows the full text a tooltip gives elsewhere), the working set (or the memories with `web.working_set` off), and, when the
1410
+ session has them, participants, chain links, the parent, delegates, model notes and an older worker's Restart;
1411
+ - one line with ctx, speed, the clock and the folder;
1412
+ - Continue, Archive (or Unarchive), Stop and Delete, under the same rules and confirmations as the wider screen's buttons.
1413
+
1414
+ A dot on ⋯ says something behind it needs a look: the accent when the working set changed or a participant is here, the warning
1415
+ colour when a delegate waits or the worker runs an older chi. The attached context's chips keep their own second line under the row.
1416
+
1400
1417
  ### Remote sessions
1401
1418
 
1402
1419
  The session commands also reach another machine's sessions, through its chi
@@ -127,7 +127,7 @@ own subsystems: `hosts:` (below), `models:` (see "Prompt profile" and
127
127
  `guardrails:` (see [Guardrails](guardrails.md)), `bundles:` (a bundle's
128
128
  settings for its hooks, see [Hooks: Settings](hooks.md#settings); a session's own
129
129
  `--set BUNDLE.KEY=VALUE`, for a key the bundle's manifest lists in `session_settings:`, lays over them, see
130
- [Per-session settings](plugins.md#per-session-settings); a name no installed bundle has, or a key its manifest's `settings:` rules out, warns at a session's start; `--set KEY=VALUE` does the same for a few core keys,
130
+ [Per-session settings](plugins.md#per-session-settings); a name no installed bundle has, or a key its manifest's `settings:` rules out, warns at the start of the first session to find it and again only when the findings change or come back (a digest of the lines shown is stamped in `$XDG_STATE_HOME/samagotchi/bundles_warned`; the log has them from every session); `--set KEY=VALUE` does the same for a few core keys,
131
131
  see "Per-session settings" below),
132
132
  `memories:` and `append_system_prompt:` (see "Appended prompt files"). Their entry names (host names, model ids, aliases) are yours to
133
133
  choose. The `memories:` list is the persistent baseline for preloaded memory entries —
data/docs/guardrails.md CHANGED
@@ -107,7 +107,9 @@ A file that doesn't parse is moved aside to `approvals.json.corrupt-<UTC time>`
107
107
  with one warning, and chi starts with no stored approvals (more asks, nothing lost).
108
108
  `/guardrails` lists the rules and approvals; `/guardrails revoke N` removes
109
109
  approval N. When System 1 is enabled or has judgments, it also shows this
110
- session's recent auto-allows and each rule's record summary. In System 1 `on`
110
+ session's recent auto-allows and each rule's record summary, and its heading says
111
+ when the settings mean nothing can be judged (`System 1 (on, but nothing is judged:
112
+ system_one.host is not set):`; no request is made to check). In System 1 `on`
111
113
  mode, `/guardrails wrong [N]` flags the Nth newest auto-allowed call in this
112
114
  session as one System 1 should have asked about (default 1). The flag is
113
115
  appended to the judgments log; the rule asks again until its record is rebuilt.
@@ -1163,8 +1165,9 @@ written, and the call is denied without a card; a turn that stopped as the
1163
1165
  judgment came back opens no card either.
1164
1166
 
1165
1167
  `ruby script/system-one-probe HOST [MODEL]` asks three fixed approval-like
1166
- questions through the client and prints each probability and the latency of
1167
- the call — how a host is checked before any of this is turned on.
1168
+ questions, then a choice and a score question, through the client and prints
1169
+ each answer and the latency of the call — how a host is checked before any of
1170
+ this is turned on.
1168
1171
 
1169
1172
  ### Answering for the user: `on` mode
1170
1173
 
data/docs/hooks.md CHANGED
@@ -489,10 +489,14 @@ notice is only added on top. A config hook (config.yml's own files) keeps its
489
489
  behaviour: no notice, just the log. A UI that joins later reads the same lines
490
490
  from `Engine#plugin_warning`.
491
491
 
492
+ In the [bundle test kit](plugins.md#testing-a-bundle-experimental) a hook that raises raises
493
+ `Testing::HookFailed` out of the call that fired it, under every `on_error`.
494
+
492
495
  ## Runtime Hook Registration
493
496
 
494
497
  You can also register hooks programmatically during a turn (they are cleared
495
- automatically after each `run_turn`):
498
+ automatically after each `run_turn`; one that raises is logged, tells you once
499
+ per event in a worker, as a bundle's does, and the next hook runs):
496
500
 
497
501
  ```ruby
498
502
  engine = Samagotchi::Engine.new
@@ -212,7 +212,8 @@ The Bridge exposes:
212
212
  `404` for an unknown id. A POST with a body because routes match exact paths.
213
213
  - `POST /session/:id/command` — a session command (`/model`, `/models`, `!rollback`, `!cmd`,
214
214
  `/continue`) for the worker loop; `202` with a `command_id` its `:command_ran` names, `400` when
215
- the line isn't one. A `:command_ran` carries `thinking`, `reasoning_history` and `llm_context`
215
+ the line isn't one. A `:command_ran`'s `output` is the command's, up to `Worker::COMMAND_OUTPUT_LIMIT` (64 KiB of chars; past it
216
+ the text ends with a `[cut: N of M chars: …]` line, which the UIs print as they print the rest). It carries `thinking`, `reasoning_history` and `llm_context`
216
217
  (the summaries the next turn runs under) when the command changed them or the model, and
217
218
  `confirm: {line, text}` when a `/thinking` or `/reasoning-history` change from a `web:` or `tui:`
218
219
  client asks first (`CacheConfirm`; nothing changed, `line` applies it).
@@ -42,7 +42,7 @@ user-facing side (what a bundle owns, profiles, needs) is in [memory.md](../memo
42
42
  | `Builder` | `chi bundle build`: memories (+ the installed bundle's hooks, rules, plugin, scripts) → zip/tar/dir |
43
43
  | `Profile` | meta bundles (`includes:`): install the members, record which; `InstallResult#kept_own` names a new member's memory files the user already had, with other bytes (`kept_own_line`, printed by `chi bundle`, `chi update` and `chi bootstrap`); a dry run runs each member's Installer as a dry run too, so it names them as well |
44
44
  | `SystemBundle`, `ShippedUpdate` | the built-in system bundle (`ensure!` at Engine start, `sync` in `chi update`) and `chi update`'s upgrade of shipped bundles; both drive the Installer with `upgrade: true`. `ShippedUpdate.external` is the rest: what `chi update` only checks |
45
- | `InstalledCopy`, `BundleCommand::InstalledCheck` | `chi update`'s re-check of the bundles chi didn't ship ([docs/cli.md](../cli.md#updating)). `InstalledCopy` lays a record and its installed files out as a source folder (manifest.yml from `Manifest.write`, the files' own shas, memories from `bases/`); `InstalledCheck` forks per bundle (a plugin's `exit` or hang is that bundle's row; stdio to /dev/null, so `--json` stays parseable; `TIMEOUT`, then KILL of its group) and runs `Check.new(dir, installed: true)` there, which leaves out what only suits a source tree. The fork is the point: `Sandbox` swaps ENV and the plugin's code is arbitrary. `UpdateCommand#external_rows` makes the rows (`level`, never `failed`) and holds the check back while a newer gem is yet to take over (`@not_checked`), since a fork is this chi's code |
45
+ | `InstalledCopy`, `BundleCommand::InstalledCheck` | `chi update`'s re-check of the bundles chi didn't ship ([docs/cli.md](../cli.md#updating)). `InstalledCopy` lays a record and its installed files out as a source folder (manifest.yml from `Manifest.write`, the files' own shas, memories from `bases/`); `InstalledCheck` forks per bundle (a plugin's `exit` or hang is that bundle's row; stdio to /dev/null, so `--json` stays parseable; `TIMEOUT`, then KILL of its group) and runs `Check.new(dir, installed: true)` there, which leaves out what only suits a source tree. Its `boundary` finding is narrower there than in a source tree: `Testing::Boundary.gone?` calls a reference gone only when its constant doesn't resolve or the object has no method the name names, so a bundle reaching `Samagotchi::Log` gets no row. lib loads lazily, so when a name doesn't resolve `gone?` requires every file of this chi's library in that same child and asks again (about 0.2 s, only for a bundle that reaches into chi at all); anything it can't read — a `const_get`, a require of a file that is there — is taken as still there. The fork is the point: `Sandbox` swaps ENV and the plugin's code is arbitrary. `UpdateCommand#external_rows` makes the rows (`level`, never `failed`) and holds the check back while a newer gem is yet to take over (`@not_checked`), since a fork is this chi's code |
46
46
  | `Uninstaller`, `Trash`, `Status`, `Listing` | remove, keep removed files, `chi bundle status`, `chi bundle list` |
47
47
  | `IndexUpdater`, `IndexSync` | the managed `index.md` lines (Installer; and the write/edit tools for memories) |
48
48
  | `EntrySize` | one memory's size in tokens: `memory.entry_warn_tokens`' crossing note after a memory write, `chi self`'s `big memories` row |
@@ -811,7 +811,7 @@ Both loops:
811
811
  again, up to `retry.empty_answer` times (at most 3), with a hidden nudge (`TurnNote.empty_retry`) on the tail.
812
812
  The retry request runs at temperature 0.6 when the configured sampling sets no temperature; a configured one
813
813
  is kept. A length stop with the context at least 90% full is not retried. When the retries run out, the
814
- Engine keeps a `TurnNote.empty` note as the turn's end.
814
+ Engine keeps a `TurnNote.empty` note as the turn's end. A Stop that lands on an empty reply ends the turn cancelled, in both loops.
815
815
  - **A refusal** (chat loop only): a finish reason `refusal` (Messages), or `content_filter` (OpenAI) with no text, is
816
816
  an empty answer never retried; text streamed before a refusal is not kept, and tool calls with it are not run. A
817
817
  `content_filter` with text keeps it, marked `ChatLoop::FILTERED_MARK`, also never retried. `ModelResult#refusal` is `{category:}` (from the response's
@@ -23,7 +23,7 @@ page. A factory dep a new version stops passing is the same hazard the other way
23
23
  out of `send_turn.js`, `session_actions.js` and `lists_view.js`); a page loaded before keeps all its old modules
24
24
  until a reload, so the risk is a mixed cache, not an open tab. `answer_fold.js`, `details_motion.js`,
25
25
  `turn_range.js`, `history_reveal.js`, `latest.js`, `session_payload.js`, `method_api.js`, `session_store.js`,
26
- `session_fields.js`, `feature_gates.js`, `revise_stack.js`, `revise_switch.js`, `shell_command.js`, `stale_params.js` and `popover_modal.js` are new modules for that reason.
26
+ `session_fields.js`, `feature_gates.js`, `revise_stack.js`, `revise_switch.js`, `shell_command.js`, `stale_params.js`, `popover_modal.js` and `more_sheet.js` are new modules for that reason.
27
27
 
28
28
  ## Two pages, two models
29
29
 
@@ -210,6 +210,63 @@ createFamilyPop`) still place their own. Every popover behaves the same (`popove
210
210
  none behind. `model_picker.js`, `command_list.js` (listboxes: the focus stays in the input), `ref_menu.js` (a menu)
211
211
  and `update_sheet.js` (a native modal `<dialog>`) aren't popovers in that sense.
212
212
 
213
+ **A phone's session bar** (`more_sheet.js`): CSS alone folds the chips and the bar's buttons away at 600 px or less
214
+ (`display:none`, so the same `.meta` draws both layouts and `info_bar.js` knows nothing of it); they stay in the DOM.
215
+ ⋯'s sheet (`#infoMoreBtn`) draws its rows from the bar's model, and a row or an action clicks the real chip or button, so
216
+ each keeps one set of handlers, confirmations and in-flight rules. A chip the sheet opens has no box to be placed at, so
217
+ `chip_popover.js` places it, marks `aria-expanded` and gives focus back at the page's `[data-anchor-stand-in]` (⋯); that
218
+ matters at exactly 600 px, where the CSS is a phone's but the shell (`SHEET_MAX`, `<`) still draws a popover, not a sheet.
219
+ A new chip on the bar needs its place in that CSS's hidden list and a row in `moreRows`, and its attention state in the row, or
220
+ it is lost on a phone. `app.js`'s `updateInfoBar` feeds the sheet (`update`) on every redraw; its rows and actions draw into
221
+ fixed slots, so a row that joins keeps the focus where it was. The `narrowScreen` listener closes the sheet when the window grows.
222
+
223
+
224
+ ## The stage's height
225
+
226
+ `stage_frame.js` is the one writer. The cap is a ceiling: 60% of the page, 80% with the cloud open, 85% while a card has
227
+ the room, and never above the session panel. The frame is grow-only while a turn runs, so a change inside the stage
228
+ never moves its top edge, with one exception: **the next step after a blocking card's answer lets the frame go, once**
229
+ (`stage_view.js releaseHeldWait`, from `generationStarted`). The `held-wait` class says it is due: set with `waiting`,
230
+ cleared there, so a later step, which calls it again, does nothing. What goes together: the live slots' waiting height, a
231
+ raised cap, the tallest content seen (`forgetContent`). At the tap nothing moves: the card collapses to its line inside
232
+ the held frame and the stage's scroll stays, unless the line would be above the view (a reader who scrolled down a card
233
+ taller than the stage), when it comes back to the view's top (`settleAnsweredCard`).
234
+
235
+ Two cards raise the cap: a model's own question whose reply overflows its slot (`drawRoom`, the question room, read from
236
+ the reply's start with a pill to the card) and a blocking approval too tall for the stage's view, a delegate's relayed
237
+ one too (`raiseForApproval`, `stage_frame.js fitsCap`). An approval gets the cap only: no room class and no pill, as it
238
+ opens at its question and command (`revealTop`).
239
+
240
+ ## Frames
241
+
242
+ During a turn the cost is frames, not code: JS, layout and style are each a few percent of a core, while an animation
243
+ that updates at 60 fps keeps the compositor and GPU busy (25-50% of a core with nothing to show). Moving a pulse to
244
+ `transform`/`opacity` on the compositor still costs most of that, because the composites go on; the update rate is what
245
+ counts. So:
246
+
247
+ - **An infinite animation is stepped, moves only `opacity` or `transform`, and stops when the turn waits.**
248
+ `spec/web/public/frames.test.js` reads every page and stylesheet for the first two (`.card.skeleton` is the allowed
249
+ exception: loading only), and for its off switch under `prefers-reduced-motion`.
250
+ - The dot, a running tick and a live step's dot share `live-blink 2.4s steps(16)`. `steps()` applies per segment and the
251
+ keyframes have two, so that is 13 updates/s: a third keyframe would change the rate. The spinners are
252
+ `init-spin .8s steps(16)`, 20 updates/s.
253
+ - A blocking card (`.turn-stage.waiting`: a question, an approval, a delegate's relayed one, whose parent's turn blocks on
254
+ it) stops them all, so movement means chi is working; `stage.spec.js`'s question scenario checks it, also after a
255
+ reload. An asking card (check-in) has no `.waiting`, as its turn goes on: its dot keeps blinking. `.init-spin` is
256
+ outside the stage (plugin setup only).
257
+ - A one-shot fade on something that arrives many times a second is a recurring cost too. A live answer's new line
258
+ appears at once: its fade alone was about a quarter of the streaming answer's total on the Studio (24-25% of a core to
259
+ 17-19%). The headline's `ts-fade` stays: turning it off saved nothing measurable.
260
+
261
+ A running turn's text reaches the page at most every 100 ms (`turn_block.js FLUSH_MS`): a chunk only marks its step dirty,
262
+ and `flush` writes the dirty steps, follows and runs `onChange`, which refreshes the stage on the next frame
263
+ (`stage_view.js scheduleRefresh`, unthrottled: a collapse or a card stays immediate). The first chunk after a quiet
264
+ interval flushes on the next frame, so a first token isn't late; one inside the interval sets a timer for the rest, which
265
+ then asks for a frame, so a hidden tab's flush still pauses. A chunk that opens a step applies at once. What must not wait
266
+ writes for itself: `generationCompleted` and a step's close write its text, a card's refresh reads the model.
267
+ `reset`, `turnEnded` and `flush` itself cancel the timer. A frame already requested can't be cancelled that way, so it may
268
+ run `flush` with nothing to write, which doesn't count as a flush for the interval.
269
+
213
270
  ## Stale answers
214
271
 
215
272
  The page switches sessions, reopens a popover or asks again while a fetch is out. An answer for the old ask must be
@@ -258,7 +315,7 @@ reach another device or the server.
258
315
  | Key | Module | What |
259
316
  |---|---|---|
260
317
  | `chi_model`, `chi_model_recent` | `model_pick.js`, `model_picker.js`, `start_page.js` | the start page's model and its recent list |
261
- | `chi_composer_collapsed` | `composer_fold.js` | the composer folded |
318
+ | `chi_composer_collapsed` | `composer_fold.js` | the composer folded (a card's text field folds it for the time it has the focus on a narrow or touch screen, `composer_grip.js watchCardField`, on top of this and of the visit's state: neither is written) |
262
319
  | `chi_composer_floor` | `composer_grip.js`, `composer_mount.js` | the composer's dragged height |
263
320
  | `chi_strip_hidden` (`chi_zen`: the old name, read once) | app.js | the session strip hidden |
264
321
  | `chi_stage_collapsed` | `stage_view.js` | the stage collapsed |
data/docs/plugins.md CHANGED
@@ -845,7 +845,7 @@ full label, `plugin.rb (bundle my-bundle)`.
845
845
  | `ctx.turn_author` | who wrote the running turn when it isn't the session's owner: a participant's `"p:<id>"` (`chi share`); nil for the owner's turns and between turns. Every hook event of such a turn carries the same as `author:`. The handler runs with the owner's power whatever the author ([hooks](hooks.md#participants-turns)). Needs chi 0.105.0 (guard it with `ctx.respond_to?(:turn_author)`) |
846
846
  | `ctx.settings` | the bundle's settings, frozen: config.yml's `bundles: <bundle>:` with the session's own `--set` values laid over it ([Per-session settings](#per-session-settings)); the config alone until the session is bound (in `register` and `chi.ctx`) |
847
847
  | `ctx.data_dir` | `$XDG_STATE_HOME/samagotchi/plugins/<bundle>/`, created on first use: what the bundle keeps across sessions |
848
- | `ctx.session_dir` | this session's own folder there, created on use, read now; nil with no session yet. chi removes it when the session is deleted, discarded or pruned, so don't build the path yourself. A `sessions/<id>.json` an older version of your bundle kept moves in as `state.json` (chi still removes one left there). Needs chi newer than 0.142.0 |
848
+ | `ctx.session_dir` | this session's own folder there, created on use, read now; nil with no session yet. `ctx.session_dir(create: false)` is the same path with no folder made, for a read, so a session that saved nothing leaves none (a `File.read` of what isn't there raises `Errno::ENOENT`); needs chi newer than 0.143.0. chi removes it when the session is deleted, discarded or pruned, so don't build the path yourself. A `sessions/<id>.json` an older version of your bundle kept moves in as `state.json` (chi still removes one left there). Needs chi newer than 0.142.0 |
849
849
  | `ctx.log` | `ctx.log.info(:event, key: value)`: debug-log records tagged `plugins`, with `bundle=<bundle>` |
850
850
  | `ctx.messages` | the conversation, as a frozen copy, without the system prompt. While a turn runs, a session worker's (attached, web) adds that turn so far: its prompt, the model's text and the lines merged into it (no tool calls or thinking); the REPL's is the conversation before that turn. An entry may carry `at`, when it happened (docs/sessions.md, Message times) |
851
851
  | `ctx.messages_partial?` | whether `ctx.messages` leaves out a running turn (the REPL mid-turn), so a plugin can say what its answer is about |
@@ -1384,7 +1384,9 @@ that calls it, or for a real external bundle that does. No method on the chance
1384
1384
  and `Boundary::EXCEPTIONS` the per-bundle list of what each shipped bundle
1385
1385
  (and its specs) reaches beyond it, each with why: the APIs to design before
1386
1386
  that bundle could leave chi's repo. `spec/samagotchi/testing/boundary_spec.rb`
1387
- fails on a reference in neither (docs/testing.md).
1387
+ fails on a reference in neither (docs/testing.md). In `chi update`'s re-check of
1388
+ an installed bundle the same scan warns only for a name this chi no longer has
1389
+ (`Boundary.gone?`): a reference that still resolves gets no row.
1388
1390
 
1389
1391
  **Used by shipped bundles today, but internal** (may change without notice; a
1390
1392
  bundle that needs one should ask for an API instead):
data/docs/releasing.md CHANGED
@@ -28,7 +28,8 @@ can run the whole release; the user approves the notes before the tag and the
28
28
  one-line string). `release:bump` touches no other `requires_chi` but a
29
29
  profile's (next point);
30
30
  - a new `ctx.*` / `chi.*` call a plugin may make gets its release's version in `Plugin::Api::SINCE` once
31
- released (`chi bundle check`'s `requires-chi-floor` reads it); an unreleased one waits there as a comment;
31
+ released (`chi bundle check`'s `requires-chi-floor` reads it); an unreleased one waits there as an `# Unreleased` comment, and
32
+ `release:check` fails while that comment is left: move each call up with the new version;
32
33
  - a new shipped bundle joins a profile (`includes:` in `core` or `dev`; a
33
34
  spec checks every bundle is in exactly one), and that profile's version
34
35
  goes up: `chi update` then installs only the new bundle for those who
@@ -71,7 +72,7 @@ as the GitHub release body.
71
72
  | `rake gem:validate`, `gem:build`, `gem:install`, `gem:clean` | Check the gemspec; build the gem into `pkg/`; build and install it locally; remove the built gems. |
72
73
  | `rake release:draft_changelog` | A draft Unreleased section from the commit subjects since the last tag, grouped. Stdout only. |
73
74
  | `rake release:bump[X.Y.Z]` | VERSION, the system manifest, Gemfile.lock, CHANGELOG, and `requires_chi: ">= X.Y.Z"` for each bundle marked `requires_chi_next` (the marker dropped), then a profile left below a member's `requires_chi` raised to it, with a patch bump of its version (none when it moved since the last tag) and named in the CHANGELOG's "Update with `chi update`" line. No commit. |
74
- | `rake release:check` | Clean tracked tree, VERSION == system bundle, a CHANGELOG section, no `requires_chi_next` left, `bundles:check`, `rake lint` (RuboCop) + `npm run lint` (the web frontend's ESLint), rspec (both roots: `spec` and the bundles' own `spec/`) + `rake bundles:specs` + npm test, `gem build`, then a clean install into a temp GEM_HOME that runs `chi --version`, `chi self` and `chi bundle list` with a temp HOME. Needs the network (gem dependencies). |
75
+ | `rake release:check` | Clean tracked tree, VERSION == system bundle, a CHANGELOG section, no `requires_chi_next` left, no `# Unreleased` comment left in `Plugin::Api::SINCE`, `bundles:check`, `rake lint` (RuboCop) + `npm run lint` (the web frontend's ESLint), rspec (both roots: `spec` and the bundles' own `spec/`) + `rake bundles:specs` + npm test, `gem build`, then a clean install into a temp GEM_HOME that runs `chi --version`, `chi self` and `chi bundle list` with a temp HOME. Needs the network (gem dependencies). |
75
76
  | `rake release:notes[X.Y.Z]` | Prints the CHANGELOG section (default: the current VERSION). |
76
77
 
77
78
  ## Runbook
data/docs/testing.md CHANGED
@@ -294,7 +294,7 @@ well formed (unique ids, known keys and labels, a rule that exists in its set).
294
294
 
295
295
  ```sh
296
296
  script/affected-specs # the specs for this branch's change, best first, at most 16
297
- bundle exec rspec $(script/affected-specs)
297
+ script/affected-specs --run # runs them; with no Ruby picks it says so and runs nothing
298
298
  script/affected-specs --why # with the reason for each pick; --all: no cap; --run: runs them
299
299
  script/affected-specs A..B # another range (A...B: from their merge-base); a single REV: vs the working tree
300
300
  ```
@@ -4,6 +4,7 @@ require "json"
4
4
 
5
5
  require_relative "client_event"
6
6
  require_relative "event_id"
7
+ require_relative "../llm/utf8_scrub"
7
8
 
8
9
  module Samagotchi
9
10
  class Bridge
@@ -231,11 +232,20 @@ module Samagotchi
231
232
  lines << "id: #{EventId.format(seq, @epoch)}"
232
233
  event_type = data.is_a?(Hash) && (data[:type] || data["type"])
233
234
  lines << "event: #{event_type}" if event_type && !event_type.to_s.empty?
234
- JSON.generate(data).each_line { |line| lines << "data: #{line.chomp}" }
235
+ frame_json(data).each_line { |line| lines << "data: #{line.chomp}" }
235
236
  io.write(lines.join("\r\n") + "\r\n\r\n")
236
237
  io.flush
237
238
  end
238
239
 
240
+ # An event holding bytes JSON can't take (a plugin's card, a tool's text)
241
+ # goes out scrubbed: raising here would end this stream, and every stream
242
+ # after it, for as long as the ring holds the event.
243
+ def frame_json(data)
244
+ JSON.generate(data)
245
+ rescue JSON::GeneratorError
246
+ JSON.generate(LLM::Utf8Scrub.call(data))
247
+ end
248
+
239
249
  def write_heartbeat(io)
240
250
  io.write(": ping\r\n\r\n")
241
251
  io.flush
@@ -403,8 +403,19 @@ module Samagotchi
403
403
  require_relative "../testing/boundary"
404
404
  known = shipped? ? Testing::Boundary::EXCEPTIONS.fetch(manifest.name, {}) : {}
405
405
  Testing::Boundary.scan(@dir).reject { |f| known.key?(f.reference) }.each do |finding|
406
- add(:warn, :boundary, finding.file, "#{finding.reference} is internal to chi and may break on a chi upgrade",
407
- fix: "use chi.* / ctx, or the names docs/plugins.md lists as stable")
406
+ if @installed
407
+ # `chi update` asks one question of a bundle it didn't write: what
408
+ # does this chi break of it. A name that still resolves is not a
409
+ # warning, so the table stays clear of rows that repeat while
410
+ # nothing is wrong.
411
+ next unless Testing::Boundary.gone?(finding.reference)
412
+
413
+ add(:warn, :boundary, finding.file, "#{finding.reference} is gone from this chi, so this bundle may fail",
414
+ fix: "use chi.* / ctx, or the names docs/plugins.md lists as stable")
415
+ else
416
+ add(:warn, :boundary, finding.file, "#{finding.reference} is internal to chi and may break on a chi upgrade",
417
+ fix: "use chi.* / ctx, or the names docs/plugins.md lists as stable")
418
+ end
408
419
  end
409
420
  end
410
421
 
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "digest"
4
+ require "fileutils"
5
+ require_relative "atomic_file"
3
6
  require_relative "config"
7
+ require_relative "paths"
4
8
  require_relative "session_config"
5
9
  require_relative "memory_bundle/provenance"
6
10
 
@@ -10,8 +14,11 @@ module Samagotchi
10
14
  # value's type a manifest's settings: rules out. A config file only warns:
11
15
  # the rest of it still runs, and `chi --set` is what refuses
12
16
  # (SessionConfig.check!, in the same words). A manifest with no settings:
13
- # (a record from before them too) has its keys left alone.
17
+ # (a record from before them too) has its keys left alone. A digest of the
18
+ # lines last shown is stamped in $XDG_STATE_HOME/samagotchi/bundles_warned.
14
19
  module BundleSettingsCheck
20
+ STAMP_FILE = "bundles_warned"
21
+
15
22
  module_function
16
23
 
17
24
  # @param section [Hash{String => Hash}] ConfigFile.bundle_settings
@@ -29,6 +36,32 @@ module Samagotchi
29
36
  end
30
37
  end
31
38
 
39
+ # @return [String] the stamp of the lines last shown
40
+ def stamp_path(env: ENV) = File.join(Paths.state_dir(env: env), STAMP_FILE)
41
+
42
+ # The lines to show now: each worker checks the config, and all of them
43
+ # finding what the last one showed would repeat it in every session. None
44
+ # while +lines+ are the stamped ones (in any order); no lines clear the
45
+ # stamp, so the same typo made again is new. A stamp it can't read or
46
+ # write costs a repeated warning, never a session start.
47
+ # @param lines [Array<String>] #warnings
48
+ # @return [Array<String>] +lines+, or none
49
+ def unshown(lines)
50
+ stamp = stamp_path
51
+ if lines.empty?
52
+ FileUtils.rm_f(stamp)
53
+ return lines
54
+ end
55
+ digest = Digest::SHA256.hexdigest(lines.sort.join("\n"))
56
+ return [] if File.exist?(stamp) && File.read(stamp).strip == digest
57
+
58
+ FileUtils.mkdir_p(File.dirname(stamp))
59
+ AtomicFile.write(stamp, "#{digest}\n")
60
+ lines
61
+ rescue StandardError
62
+ lines
63
+ end
64
+
32
65
  # One bundle's +values+ against its record (or a manifest as one); none
33
66
  # while it declares no settings:.
34
67
  # @param bundle [MemoryBundle::InstalledBundle]
@@ -1,14 +1,14 @@
1
1
  ---
2
2
  name: check-in
3
- version: 0.3.1
3
+ version: 0.3.3
4
4
  scope: system
5
5
  description: "Checks on a long turn: after N tool calls with no answer, a card asks the user (Nudge / Keep going / Stop), or the model is nudged to say what it has found"
6
6
  trust_level: reviewed
7
7
  plugin:
8
8
  file: plugin.rb
9
- sha256: sha256:732f144471d146832d2f01a5e999463be789a35af476fbc5d97349091c07aea2
9
+ sha256: sha256:6580d6d19ac9beda871d88d3955abc5a319dd8b5aea225a06dee90b6daeeddce
10
10
  # check-in.progress asks System 1 (chi.judgment, new in 0.142.0);
11
- # ctx.session_dir is new in the next chi
12
- requires_chi: ">= 0.143.0"
11
+ # ctx.session_dir is new in 0.143.0
12
+ requires_chi: ">= 0.144.0"
13
13
  # config.yml bundles: check-in: (another key warns; an older chi ignores this)
14
14
  settings: [after, every, mode, message, ignore_tools]
@@ -236,15 +236,19 @@ class Plugin
236
236
  end
237
237
 
238
238
  def call_args(call)
239
- args = call.is_a?(Hash) ? (call[:arguments] || call["arguments"] || call) : call
240
- return args if args.is_a?(String)
239
+ return call if call.is_a?(String)
240
+ return JSON.generate(call) unless call.is_a?(Hash)
241
+
242
+ # A tool that isn't built in has its arguments on args: and, as JSON, in
243
+ # content: (or there alone, when none could be read): say them once.
244
+ args = call[:args]
245
+ return args.empty? ? call[:content].to_s : JSON.generate(args) if args.is_a?(Hash)
241
246
 
242
247
  # The tool's name is already in the entry and nil fields say nothing:
243
248
  # the cut should keep what the call asked for.
244
- args = args.compact.except(:name, "name") if args.is_a?(Hash)
245
- JSON.generate(args)
249
+ JSON.generate(call.compact.except(:name, "name"))
246
250
  rescue StandardError
247
- args.to_s
251
+ call.to_s
248
252
  end
249
253
 
250
254
  def cut(text, limit)
@@ -389,7 +393,7 @@ class Plugin
389
393
  def read_state(ctx, id)
390
394
  return {} unless id
391
395
 
392
- data = JSON.parse(File.read(state_file(ctx)))
396
+ data = JSON.parse(File.read(saved_file(ctx)))
393
397
  data.is_a?(Hash) ? data : {}
394
398
  rescue SystemCallError, JSON::ParserError
395
399
  {}
@@ -410,6 +414,9 @@ class Plugin
410
414
 
411
415
  def state_file(ctx) = File.join(ctx.session_dir, "state.json")
412
416
 
417
+ # For a read: a session with nothing saved keeps no folder.
418
+ def saved_file(ctx) = File.join(ctx.session_dir(create: false), "state.json")
419
+
413
420
  # --- helpers ---------------------------------------------------------------
414
421
 
415
422
  def calls = @mutex.synchronize { @count }
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: core
3
- version: 0.4.2
3
+ version: 0.4.3
4
4
  scope: system
5
5
  description: "Recommended safety set"
6
6
  trust_level: reviewed
7
7
  # Its members' highest floor (work-cap's: ctx.usage, raised at the release)
8
- requires_chi: ">= 0.143.0"
8
+ requires_chi: ">= 0.144.0"
9
9
  includes:
10
10
  - loop-guard
11
11
  - check-in
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  name: cost-cap
3
- version: 0.2.4
3
+ version: 0.2.5
4
4
  scope: system
5
5
  description: "A per-session $ cap: at the cap the turn pauses on Continue (another cap's worth) / Stop, a warning at 80% goes to the user only; /cap shows or sets it"
6
6
  trust_level: reviewed
7
7
  plugin:
8
8
  file: plugin.rb
9
- sha256: sha256:6101e5f4b2b454224ccc780637a84e429e90f179d8f37ba8681a1c5bf5d0c840
9
+ sha256: sha256:2d1f739ed0246d3d3040e0534cced2a1f621bdc5881fdde8b463df8a0501e477
10
10
  # ctx.cost is new in chi 0.83.0; ask_user's effects: in the next chi
11
- # ctx.session_dir is new in the next chi
12
- requires_chi: ">= 0.143.0"
11
+ # ctx.session_dir is new in 0.143.0
12
+ requires_chi: ">= 0.144.0"
13
13
  # session_settings: as a mapping of keys to types is new
14
14
  # chi --set cost-cap.cap=3: a session's own cap (ctx.settings["cap"]), a number or off
15
15
  session_settings: {cap: number}
@@ -247,7 +247,7 @@ class Plugin
247
247
  def state(ctx, session)
248
248
  return [nil, nil] unless session
249
249
 
250
- path = state_file(ctx)
250
+ path = saved_file(ctx)
251
251
  saved = JSON.parse(File.read(path))
252
252
  raise JSON::ParserError, "not a JSON object" unless saved.is_a?(Hash)
253
253
  return [nil, nil] unless saved.key?("cap")
@@ -309,6 +309,9 @@ class Plugin
309
309
 
310
310
  def state_file(ctx) = File.join(ctx.session_dir, "state.json")
311
311
 
312
+ # For a read: a session with nothing saved keeps no folder.
313
+ def saved_file(ctx) = File.join(ctx.session_dir(create: false), "state.json")
314
+
312
315
  def money(value) = self.class.money(value)
313
316
 
314
317
  # Whether +key+ is new for the once-only notice kept in +ivar+ (and keep it).