samagotchi 0.99.0 → 0.100.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0185e889922200db9f6882d899b3ff17c74d296aa8c7823a7040e45e308edc47'
4
- data.tar.gz: addddd537eac6fc703d0e217c7a968446ca44bb53f8ee501e61de86b1163085c
3
+ metadata.gz: 47dbac4f107af7d9c7d9d571b907a1e9e777326732614ed34986f2dce954168a
4
+ data.tar.gz: 4335f033596dc75f3db80b754a347e36862d0fd07f1d72bafcc7a5c50ed539ff
5
5
  SHA512:
6
- metadata.gz: 62a2aaff51d6bbc45cfcc59e3d199444448cd838c229fbe4bda717658b6ba83fc7f273326ea0d8e8cb3e34839bd3c78ba472a5792589dbfe87a5dd1a8df2c5a7
7
- data.tar.gz: 8b68d54a05c89461dba2428496111897cf85ca1cc70ac70f2b1621edcce7d70908243e181644b7086557d347fc80c51305a7b3df446c9299a09c2ec8e1cd113b
6
+ metadata.gz: a2454b93c9271b14ba0349c2991f6b0312dfdee76fd6a90005feb38726025dad71b6b4f1701bcbb2622c3db786d86fe2e58636668aa2f7d43d0ac289cae36e5a
7
+ data.tar.gz: 8fef8d270b58719f286b57c840f1a5fc3a53de10fec01fdcc08ea3de1315de17d267a834759b05ae09c05ca80b8f27009329eb38ff0aaf9a7b6ab59585b3e1d3
data/CHANGELOG.md CHANGED
@@ -8,6 +8,39 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.100.1] - 2026-10-22
12
+
13
+ ### Fixed
14
+
15
+ - The web's `continue →` no longer takes a draft left over from another session (or the start page) as the new
16
+ link's first message: the opener goes in, and the toast's **Restore draft** brings the draft back. A draft written
17
+ in the continued session still stays.
18
+ - work-cap 0.1.1: a changed `bundles: work-cap:` setting reaches a running session (the caps are resolved again when the
19
+ settings change; the state file still wins per meter), and an empty key (`tool_calls:` with no value) falls through to
20
+ `child:` / `default:` instead of reading as "off"; `off`, `false` and `0` stay an explicit no cap.
21
+ - Web: a closed step's text in the history renders as markdown (with `web.markdown` on) and keeps its source refs,
22
+ live and after a reload, as the answer does.
23
+ - A hook entry in config.yml's `hooks:` that isn't a mapping, or has no `path:`, warns naming it
24
+ (`hooks.before_turn[0]`) instead of vanishing; the event's other entries still load.
25
+
26
+ ## [0.100.0] - 2026-10-22
27
+
28
+ ### Added
29
+
30
+ - Plugins can read the session's work so far with `ctx.usage`: turns, tool calls (by tool), steps and active time
31
+ (generations plus tool calls, never a wait on a question), across workers, the same numbers `/stats` shows. The
32
+ bundle test kit sets it with `bundle.set(usage: {...})`. See [docs/plugins.md](docs/plugins.md).
33
+ - The `work-cap` bundle (in the `core` profile; `chi update` installs it): an optional per-session cap on tool calls
34
+ and active minutes across turns, the bound a session whose step-limit Continue keeps being answered otherwise
35
+ lacks. At a cap the turn pauses on Continue / Stop, the question naming the session's totals; `/workcap` shows or
36
+ sets it, `chi send --new --set work-cap.tool_calls=400` caps a child. Inert until a cap is set. See
37
+ [docs/plugins.md](docs/plugins.md#the-work-cap-bundle).
38
+
39
+ ### Fixed
40
+
41
+ - An empty `hooks:` key in config.yml no longer crashes chi, and a malformed `hooks:` section (or one event's
42
+ entry) warns instead of being dropped without a word.
43
+
11
44
  ## [0.99.0] - 2026-10-22
12
45
 
13
46
  ### Added
@@ -3774,7 +3807,9 @@ and long-lived sessions.
3774
3807
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
3775
3808
  hotkey panel that send selected text or the clipboard to your sessions.
3776
3809
 
3777
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.99.0...HEAD
3810
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.100.1...HEAD
3811
+ [0.100.1]: https://github.com/dm1try/samagotchi/compare/v0.100.0...v0.100.1
3812
+ [0.100.0]: https://github.com/dm1try/samagotchi/compare/v0.99.0...v0.100.0
3778
3813
  [0.99.0]: https://github.com/dm1try/samagotchi/compare/v0.98.0...v0.99.0
3779
3814
  [0.98.0]: https://github.com/dm1try/samagotchi/compare/v0.97.0...v0.98.0
3780
3815
  [0.97.0]: https://github.com/dm1try/samagotchi/compare/v0.96.0...v0.97.0
data/README.md CHANGED
@@ -78,8 +78,8 @@ Messages API, picks the
78
78
  model (or asks, when there are several), sends one test request and writes
79
79
  the config. With a config already there, it adds a `hosts:` entry and keeps
80
80
  the rest of the file. Then it installs the `core` bundles (loop-guard,
81
- check-in, guardrails, slow-commands, agents-md, cost-cap: the safety set for a
82
- small local model, the cost cap inert until you set one) and, on a terminal, offers `dev` (known-names, mcp, btw, skills,
81
+ check-in, guardrails, slow-commands, agents-md, cost-cap, work-cap: the safety set for a
82
+ small local model, the cost and work caps inert until you set one) and, on a terminal, offers `dev` (known-names, mcp, btw, skills,
83
83
  source-links, github-pr, coordinator, claude-peer, codex-peer, ruby, model-notes; see
84
84
  [Bundle profiles](docs/memory.md#bundle-profiles-core-and-dev)). Edit the
85
85
  file later as in [Configure](#configure); `chi bootstrap --help` has the
data/docs/architecture.md CHANGED
@@ -411,7 +411,10 @@ stay the turns' only. `SessionMetrics#persist` is serialized (its own mutex, tak
411
411
  threads' saves land in snapshot order.
412
412
  A bundle's `ctx.cost` reads the same sums (`SessionMetrics#cost`, a frozen `SessionMetrics::Cost`): the `tokens`
413
413
  block's `cost_sum` and `cost_estimate_sum` with the running turn's finished generations folded in, plus the `side`
414
- block's, so it counts what earlier workers spent too ([plugins.md](plugins.md)).
414
+ block's, so it counts what earlier workers spent too ([plugins.md](plugins.md)). Its `ctx.usage` reads the work
415
+ (`SessionMetrics#usage`, a frozen `SessionMetrics::Usage`): the snapshot's `turns`, `tool_calls_total` /
416
+ `tool_calls_by_tool` and `iterations_total`, and `active_ms`, the generations' `gen_ms` plus the finished tool
417
+ records' `duration_ms` (tallied per tool as they are counted; `ask_user_question`'s, a wait on the user, left out).
415
418
 
416
419
  The page's frequent reads of `GET /api/sessions/:id` stay light too:
417
420
  - `?tail=1&recent=1` (each turn's end, a cancel, an answer display): `{tail, session: {id, status,
@@ -428,7 +431,10 @@ The page's frequent reads of `GET /api/sessions/:id` stay light too:
428
431
  `drawAsking(text, refs)`) pairs them on its raw draw and again after its markdown swap; the card pairs its
429
432
  question text and its kept reply (`question_cards.js`); the step's closed `.gen-text` takes them
430
433
  (`turn_view.js`; on a reload its `data-answer-at`, the message's `display_refs`) and `linkTextRefs` pairs it at
431
- the turn's end and after a reload. A file ref outside the
434
+ the turn's end and after a reload. A closed step's text is markdown too with `web.markdown` on: a reload's
435
+ from its message's `html` (`turn_view.js` `textTemplate`), a live turn's from `POST /api/render` on its step's
436
+ first open after the turn's end (`markdown_place.js` `renderStepTexts`, which pairs its refs again after the
437
+ swap); the text as written stays on it as `data-copy-source` (`stepText`, a reloaded card's kept reply). A file ref outside the
432
438
  session's folder is dropped, as a reload drops it: the folder `/tail` names, else (an older worker) the hub's
433
439
  session list entry's, else (refs to check, no hub entry) the session file's; with no folder known at all, every
434
440
  file ref is dropped rather than shown unchecked.
@@ -609,7 +615,7 @@ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/
609
615
  | Attached context | `context_sources.rb` (`ContextSources`), `context_poller.rb`, `context_absorber.rb`, `peer_sources.rb` (`PeerSources`: the sources peer bundles declare) | External sources a session keeps fresh, and the peer loop's wakes ([context.md](context.md)). |
610
616
  | LLM context strategies | `llm_context_strategy.rb`, `llm_context_view.rb`, `llm_context_stale.rb`, `llm_context_forget.rb` | What a request sends of the history (none, stale, forget) ([internals/llm-context-forget.md](internals/llm-context-forget.md)). |
611
617
  | Session chains | `session_continue.rb` (`SessionContinue`), `session_chain.rb` (`SessionChain`) | A session that continues an earlier one. `SessionChain.opener_for` gives the text web `continue →` prefills in the new link: the previous link's own first typed message when that link continues one itself, else nil (the 201's `opener`; not saved) ([sessions.md](sessions.md)). |
612
- | 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/side_totals.rb` (`SessionMetrics::SideTotals`) and `side_usage.rb` (`SideUsage`): the side requests' sums, apart from the turns'. |
618
+ | 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'. |
613
619
  | Images / vision | `image_store.rb` (`ImageStore`), `vision_support.rb`, `vision_context.rb` | Stored prompt images and whether a model takes them. |
614
620
  | Toolkits | `toolkit.rb` (`Toolkit`), `toolkit_binding.rb` (`ToolkitBinding`, the Engine's seam), `toolkit_command.rb` (`/toolkit`), `tools/registry.rb` (`Registry#toolkit`, `#left_out_names`), `web/public/toolkit_chip.js` | Which registered tools a session's prompts declare: saved in the session, resolved at a turn's start (`Engine#apply_toolkit!`), named in the prompt's not-loaded line, a kit's prompt text after the appended files; `chi.toolkit` and `ctx.use_toolkit` for plugins ([configuration.md](configuration.md#toolkits)). |
615
621
  | Tool index | `tools/tool_index.rb` (`Tools::ToolIndex`), `tools/deferred_tools.rb` | `find_tools` and `call_tool`: the tools a session knows but doesn't declare one by one (an mcp bundle's servers through `chi.tool_source`, the built-ins its toolkit leaves out), searched and called by name ([plugins.md](plugins.md)). On `api: anthropic` they also go out deferred, a hit loads its matches and the model calls them by name ([internals/anthropic.md](internals/anthropic.md)). |
@@ -129,7 +129,7 @@ class, a hand-built `ctx`), that is a gap in the kit: say so to the user rather
129
129
  of a checksum mismatch, and a line with no sha installs unchecked. `chi bundle check --fix` rewrites them; run it
130
130
  after the last edit.
131
131
  - **requires_chi.** Use the chi the user runs (`chi --version`). `check` warns when the plugin calls something
132
- newer than the floor (`ctx.http` needs 0.80.0, `ctx.cost` 0.83.0, `ctx.sessions.children` 0.34.0). A keyword added
132
+ newer than the floor (`ctx.http` needs 0.80.0, `ctx.cost` 0.83.0, `ctx.usage` 0.100.0, `ctx.sessions.children` 0.34.0). A keyword added
133
133
  later it can't see: `ctx.ask_model(model:)` needs 0.80.0, `ctx.sessions.send(delivery:)` 0.64.0.
134
134
  - **trust_level.** Leave it out. `reviewed` / `trusted` are for someone who reviewed the bundle, not its author.
135
135
  - **Restart.** A session loads bundles when it starts: after an install or a settings change, the user starts a
data/docs/cli.md CHANGED
@@ -84,7 +84,7 @@ chi bootstrap # try localhost 8080, 11434, 1234, 8000
84
84
  or with anchors gets the lines printed to paste instead. `--dry-run` shows
85
85
  what it would write.
86
86
  - **The bundles.** Unless writing the config failed, it installs the system
87
- bundle and the `core` profile (loop-guard, check-in, guardrails, slow-commands, agents-md, cost-cap), one line
87
+ bundle and the `core` profile (loop-guard, check-in, guardrails, slow-commands, agents-md, cost-cap, work-cap), one line
88
88
  each; on a terminal it then asks `Also install dev (known-names, mcp, btw,
89
89
  skills, source-links, github-pr, coordinator, claude-peer, codex-peer, ruby, model-notes)? [y/N]`. Run again, it installs nothing new: a bundle
90
90
  you uninstalled stays out ([Bundle profiles](memory.md#bundle-profiles-core-and-dev)).
data/docs/guardrails.md CHANGED
@@ -125,7 +125,7 @@ answers it. Every question's options carry an **effect**, set by whoever asks:
125
125
  - `deny`: Deny on an approval, Stop on the step-limit question or a plugin's
126
126
  stop option;
127
127
  - `spend`: more time and tokens, no new permission (Continue on the step-limit
128
- question, cost-cap's Continue);
128
+ question, cost-cap's and work-cap's Continue);
129
129
  - `permit`: one tool call (an approval's "Allow once");
130
130
  - `permit_wide`: more than one call (this session, this repo or directory,
131
131
  the rule).
@@ -140,7 +140,7 @@ guardrails:
140
140
  - `hard`: `deny` and `answer` only. A parent can deny an approval
141
141
  (`--option Deny --text "why"`, `--text` alone or `--dismiss`), Stop a
142
142
  step-limit question and answer plain questions.
143
- - `standard`: also `spend`: a parent may Continue a turn (or cost-cap's cap).
143
+ - `standard`: also `spend`: a parent may Continue a turn (or cost-cap's or work-cap's cap).
144
144
  - `soft` (the default): also `permit`: `chi answer --option "Allow once"`
145
145
  goes through, picked by the option's scope, not its label.
146
146
 
data/docs/hooks.md CHANGED
@@ -26,6 +26,11 @@ hooks:
26
26
  on_error: skip
27
27
  ```
28
28
 
29
+ Each event key holds a list of entries. A `hooks:` that isn't a mapping, or an event key that isn't a list
30
+ (`before_turn: guard.rb`), warns at each start and loads nothing for it; an empty key loads nothing, silently.
31
+ An entry that isn't a mapping (`- guard.rb`) or has no `path:` warns the same way, naming it
32
+ (`hooks.before_turn[0]`, counted from 0), and is skipped alone: the key's other entries load.
33
+
29
34
  ## Plugin Format
30
35
 
31
36
  Each plugin is a `.rb` file in the hooks directory. The class name must match
@@ -672,7 +677,7 @@ Notes:
672
677
  - Ordering: bundle hooks fire by `(priority, bundle_name, hook_name)` (lower priority first), then plain `config.yml` hooks in registration order.
673
678
  - Settings: a hook class with `initialize(settings = {})` gets the bundle's section of `config.yml` `bundles:` (see [Settings](#settings)).
674
679
  - A bundle can also ship a `plugin.rb` whose `chi.on(event)` blocks are bundle hooks too, next to commands and tools; see [Plugins](plugins.md). There `ctx.notify`, `ctx.steer` and the other `ctx` helpers act as this page's `event[:notify]`… do for the event the block runs for.
675
- - Shipped bundles (each in a profile: `core` or `dev`, see [Bundle profiles](memory.md#bundle-profiles-core-and-dev)): `chi bundle install guardrails` (rules, see [Guardrails](guardrails.md#the-guardrails-bundle)) and `chi bundle install known-names` (a hook, see [Guardrails](guardrails.md#the-known-names-bundle)), `chi bundle install source-links` (a hook: announces source refs, see [The source-links bundle](#the-source-links-bundle)), `chi bundle install btw` (a plugin: `/btw`, see [Plugins](plugins.md#the-btw-bundle)), `chi bundle install mcp` (a plugin: tools from MCP servers, see [Plugins](plugins.md#the-mcp-bundle)) `chi bundle install loop-guard` (a plugin: breaks tool-call loops, see [Plugins](plugins.md#the-loop-guard-bundle)), `chi bundle install check-in` (a plugin: checks on a long turn, see [Plugins](plugins.md#the-check-in-bundle)), `chi bundle install slow-commands` (a plugin: commands that ran past the execute limit are run as tasks next time, see [Plugins](plugins.md#the-slow-commands-bundle)), `chi bundle install agents-md` (a plugin: a nested AGENTS.md is steered into the conversation on the first touch under its directory, see [Plugins](plugins.md#the-agents-md-bundle)), `chi bundle install cost-cap` (a plugin: a per-session $ cap, `/cap`, see [Plugins](plugins.md#the-cost-cap-bundle)), `chi bundle install ruby` (a plugin: an RSpec run with no examples at all, a hint, see [Plugins](plugins.md#the-ruby-bundle)) `chi bundle install skills` (a plugin: `/skill`, versions of skills, see [Plugins](plugins.md#the-skills-bundle)), `chi bundle install github-pr` (attached context: the branch's open PR, see [Plugins](plugins.md#the-github-pr-bundle)), `chi bundle install coordinator` (a plugin: parallel work in worktrees and child sessions, see [Plugins](plugins.md#the-coordinator-bundle)), `chi bundle install claude-peer` / `codex-peer` (a plugin: talk to a Claude Code or Codex session, see [Plugins](plugins.md#the-claude-peer-bundle)) and `chi bundle install model-notes` (memories: reviewed model notes, see [Plugins](plugins.md#the-model-notes-bundle)).
680
+ - Shipped bundles (each in a profile: `core` or `dev`, see [Bundle profiles](memory.md#bundle-profiles-core-and-dev)): `chi bundle install guardrails` (rules, see [Guardrails](guardrails.md#the-guardrails-bundle)) and `chi bundle install known-names` (a hook, see [Guardrails](guardrails.md#the-known-names-bundle)), `chi bundle install source-links` (a hook: announces source refs, see [The source-links bundle](#the-source-links-bundle)), `chi bundle install btw` (a plugin: `/btw`, see [Plugins](plugins.md#the-btw-bundle)), `chi bundle install mcp` (a plugin: tools from MCP servers, see [Plugins](plugins.md#the-mcp-bundle)) `chi bundle install loop-guard` (a plugin: breaks tool-call loops, see [Plugins](plugins.md#the-loop-guard-bundle)), `chi bundle install check-in` (a plugin: checks on a long turn, see [Plugins](plugins.md#the-check-in-bundle)), `chi bundle install slow-commands` (a plugin: commands that ran past the execute limit are run as tasks next time, see [Plugins](plugins.md#the-slow-commands-bundle)), `chi bundle install agents-md` (a plugin: a nested AGENTS.md is steered into the conversation on the first touch under its directory, see [Plugins](plugins.md#the-agents-md-bundle)), `chi bundle install cost-cap` (a plugin: a per-session $ cap, `/cap`, see [Plugins](plugins.md#the-cost-cap-bundle)), `chi bundle install work-cap` (a plugin: a per-session cap on tool calls and active minutes, `/workcap`, see [Plugins](plugins.md#the-work-cap-bundle)), `chi bundle install ruby` (a plugin: an RSpec run with no examples at all, a hint, see [Plugins](plugins.md#the-ruby-bundle)) `chi bundle install skills` (a plugin: `/skill`, versions of skills, see [Plugins](plugins.md#the-skills-bundle)), `chi bundle install github-pr` (attached context: the branch's open PR, see [Plugins](plugins.md#the-github-pr-bundle)), `chi bundle install coordinator` (a plugin: parallel work in worktrees and child sessions, see [Plugins](plugins.md#the-coordinator-bundle)), `chi bundle install claude-peer` / `codex-peer` (a plugin: talk to a Claude Code or Codex session, see [Plugins](plugins.md#the-claude-peer-bundle)) and `chi bundle install model-notes` (memories: reviewed model notes, see [Plugins](plugins.md#the-model-notes-bundle)).
676
681
  - Installing a bundle executes its hook code at `Engine` startup. Only install bundles you trust, as you would a gem. Hooks are **not** executed at install time (copy-only); they are `module_eval`'d at `Engine.new` inside per-bundle `Samagotchi::Bundles::<name>` namespaces (no top-level `require` collisions). Keep hook files side-effect-free at load time; do work in `#call` — top-level side effects (require, IO, `at_exit`, global assignment) run once per `Engine.new` (class redefinition is idempotent).
677
682
 
678
683
  Lifecycle:
data/docs/memory.md CHANGED
@@ -387,7 +387,7 @@ profiles, meta bundles that install a set of them:
387
387
 
388
388
  | profile | bundles |
389
389
  |---|---|
390
- | `core` | loop-guard, check-in, guardrails, slow-commands, agents-md, cost-cap: the recommended safety set |
390
+ | `core` | loop-guard, check-in, guardrails, slow-commands, agents-md, cost-cap, work-cap: the recommended safety set |
391
391
  | `dev` | known-names, mcp, btw, skills, source-links, github-pr, coordinator, claude-peer, codex-peer, ruby, model-notes |
392
392
 
393
393
  `chi bootstrap` installs `core` and, on a terminal, offers `dev`. An existing
data/docs/plugins.md CHANGED
@@ -751,6 +751,7 @@ full label, `plugin.rb (bundle my-bundle)`.
751
751
  | `ctx.model` | the model the session runs on now: its resolved ref (`host:id`), right after `/model` too |
752
752
  | `ctx.model_key` | that model's memory overlay key (`<name>.<key>.md`, what `memory_write current_model_only` writes) |
753
753
  | `ctx.cost` | the session's spend so far, read now, as a frozen value: `total` (USD, reported + estimated, side requests included; 0.0 before any), `reported` (by providers, `usage.cost`), `estimated` (from `hosts.<name>.models` prices; a generation is one or the other, never both), `side` (recap, `/btw`, `ask_model`: their share of `total`) and `known?` (false while generations or side requests ran but none was reported or priced, so a limit on it can't be enforced; a session with some priced generations reads known, the unpriced ones counting 0). It includes the running turn's finished generations and what earlier workers of the session spent. Use its methods, not its class. Needs chi 0.83.0 |
754
+ | `ctx.usage` | the session's work so far, read now, as a frozen value: `turns` (finished ones and the running one), `tool_calls` and `tool_calls_by_tool` (every call, running ones included; canonical names: an alias counts under its tool, a `call_tool` call under the inner tool), `steps` (the loop rounds that dispatched tools, `/stats`' iterations) and `active_ms` (`active_minutes` too): work time, the generations' time plus the finished tool calls' time, never the waits between them (a question, an approval, the user's answer to `ask_user_question`), with `tool_ms_by_tool` its tool part by tool, so a waiting tool's time (`task_wait`) can be left out. Its counts are the ones `/stats` and `chi sessions stats` show, earlier workers' included. Use its methods, not its class. Needs chi 0.100.0 (guard it with `ctx.respond_to?(:usage)`) |
754
755
  | `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`) |
755
756
  | `ctx.data_dir` | `$XDG_STATE_HOME/samagotchi/plugins/<bundle>/`, created on first use. A session's own state goes in `sessions/<id>.json` or `sessions/<id>/` there: chi removes it when the session is deleted, discarded or pruned |
756
757
  | `ctx.log` | `ctx.log.info(:event, key: value)`: debug-log records tagged `plugins`, with `bundle=<bundle>` |
@@ -1120,7 +1121,7 @@ CHANGELOG line and bumps the shipped bundles that use it. Anything else under
1120
1121
 
1121
1122
  **Stable**
1122
1123
 
1123
- 1. `Samagotchi::Plugin::Api` / `chi.*` and `ctx` as documented above (`ctx.cost`'s value by its methods), including
1124
+ 1. `Samagotchi::Plugin::Api` / `chi.*` and `ctx` as documented above (`ctx.cost`'s and `ctx.usage`'s values by their methods), including
1124
1125
  `Samagotchi::Plugin::ToolResult`, `Samagotchi::Plugin::ModelError` (and its
1125
1126
  subclass `Samagotchi::Plugin::ModelCancelled`),
1126
1127
  `Samagotchi::Plugin::Service::Stopped`, `Samagotchi::Plugin::Sessions::Error`
@@ -1187,6 +1188,7 @@ bundle.sessions.on(:children) { |all:| [] } # ctx.sessions, scripte
1187
1188
  bundle.http.stub(:post, "http://127.0.0.1:9000/classify", json: { label: "safe" }) # ctx.http, never the network
1188
1189
  bundle.set(running: false) # the session's values: cwd:, messages:, cancelled: …
1189
1190
  bundle.set(cost: 2.5) # ctx.cost: reported USD, or { reported:, estimated:, side:, known: }
1191
+ bundle.set(usage: { tool_calls: 40, active_ms: 600_000 }) # ctx.usage: { turns:, tool_calls:, tool_calls_by_tool:, steps:, active_ms:, tool_ms_by_tool: }
1190
1192
  bundle.set(session_settings: { "cap" => 1 }) # the session's --set values (Bundle.load takes session_settings: too)
1191
1193
  bundle.tool_loads.last.names # ctx.load_tools calls; set(load_tools_staged: [...]) limits what they stage
1192
1194
  bundle.shutdown
@@ -1222,6 +1224,10 @@ bundle.shutdown
1222
1224
  - **`ctx.cost`** is zero and known unless the test sets it: `bundle.set(cost: 2.5)` (reported USD), a Hash of the
1223
1225
  parts (`reported:`, `estimated:`, `side:`, `known:`; `total` is reported + estimated) or a callable giving either,
1224
1226
  called at each read (a counting lambda shows whether the bundle read it).
1227
+ - **`ctx.usage`** is zero unless the test sets it: `bundle.set(usage: {...})`, a Hash of the parts (`turns:`,
1228
+ `tool_calls:`, `tool_calls_by_tool:`, `steps:`, `active_ms:`, `tool_ms_by_tool:`; zeros for the rest, an unknown
1229
+ part raises; `tool_calls` defaults to the sum of `tool_calls_by_tool`, `active_ms` to the sum of
1230
+ `tool_ms_by_tool`) or a callable giving one, called at each read.
1225
1231
  - **`session_settings:`** (on `Bundle.load` and `bundle.set`) are the session's own `--set` values for the bundle,
1226
1232
  keys its manifest lists in `session_settings:` (another key, or a value of another type than the manifest gives
1227
1233
  it, raises `ArgumentError`, as chi refuses it). `ctx.settings`
@@ -1680,6 +1686,64 @@ its own card and doesn't wake its parent ([sessions.md](sessions.md#delegating))
1680
1686
  The state is one file per session, `<data_dir>/sessions/<session id>.json` (`{"cap": 3.0, "step": 3.0}`), removed
1681
1687
  with the session.
1682
1688
 
1689
+ Its sibling for work instead of money, tool calls and active minutes across the session's turns, is
1690
+ [the work-cap bundle](#the-work-cap-bundle): the cap a local model (no cost) can have.
1691
+
1692
+ ## The work-cap bundle
1693
+
1694
+ `chi bundle install work-cap` installs the bundle shipped with chi (`chi bootstrap` installs it with the `core`
1695
+ profile; an existing install gets it with `chi update`). It caps the work one session does across its turns: tool
1696
+ calls and active minutes. The per-turn meters reset every turn (the step limit, check-in, loop-guard), and a
1697
+ Continue on the step-limit question starts the next 300 steps; a parent agent may answer that Continue itself
1698
+ (`chi answer`), so a child it keeps continuing has no overall bound but cost-cap's `$`, and on a local model none.
1699
+ work-cap is that bound, with cost-cap's shape: the harness watches, the model never sees any of it. It is inert
1700
+ without a cap, and there is none by default.
1701
+
1702
+ ```yaml
1703
+ # config.yml
1704
+ bundles:
1705
+ work-cap:
1706
+ child: {tool_calls: 500, minutes: 90} # delegate children with no own value
1707
+ default: {} # every other session (and a delegate without child:); empty: no cap
1708
+ warn_at: 0.8 # tell the user (only) once at this share of a cap; 0/off: no warning
1709
+ ignore_tools: [task_wait, task_get, delegate, delegate_result] # not counted: waiting
1710
+ ```
1711
+
1712
+ - **What counts** (`ctx.usage`): **tool calls**, the session's own across its turns and workers, less
1713
+ `ignore_tools` (the waiting tools by default, `delegate` among them since it blocks on its child, so a session
1714
+ waiting on a long task or a child isn't capped for waiting); and **active minutes**, work time: the generations' time plus the finished tool calls' time (less
1715
+ `ignore_tools`' time), never a wait (a question, its own included, an approval, the user's answer to
1716
+ `ask_user_question`), so idle time and a Continue answered hours later don't count.
1717
+ - **When it checks:** before each generation, like cost-cap: the overshoot is at most one generation's tool calls.
1718
+ When the step limit and a work cap fall in the same turn, the work cap asks first, and its Stop ends the turn.
1719
+ - **At `warn_at` of a cap** you get one notice per meter, `work-cap> 320 of 400 tool calls (80%)`; a raised cap
1720
+ warns again.
1721
+ - **At a cap** the turn pauses on a question (header "Work cap") that names the session's totals, so a parent
1722
+ answering it decides on the whole picture: `Work cap reached: 412 tool calls of 400 (6 turns, 47 min active,
1723
+ $1.20). Continue raises it to 800.` **Continue** raises the cap by another cap's worth (as many steps as it
1724
+ takes to pass the work, saved for the session), **Stop** stops the turn (`canceled` with `stopped_by: work-cap`)
1725
+ with a card showing the totals. Both meters at once: one question naming both, Continue raises each. With no one
1726
+ to ask (`--non-interactive`) it stops. Continue is tagged `:spend`, as cost-cap's: a parent agent may Continue
1727
+ under `guardrails.parent_answers: soft` or `standard`, only Stop under `hard` (see
1728
+ [Guardrails](guardrails.md#answers-from-a-parent-agent)).
1729
+ - **The caps of a session**, each meter on its own: the one `/workcap` or a Continue set, else its own value (`chi
1730
+ --set work-cap.tool_calls=400`, `work-cap.minutes=60`, `chi send --new --set …`; `off`, `0` or a negative number
1731
+ for none), else `child:`'s for a delegate child, else `default:`'s. Minutes in config.yml read `90`, `90m` or
1732
+ `1.5h`. A value that doesn't read (`tool_calls: many`, a `default:` that isn't a mapping) gets one notice,
1733
+ `work-cap: tool_calls: "many" isn't a number or off; using default.tool_calls: (400)`, and the next value is used,
1734
+ never a silent no cap.
1735
+
1736
+ **`/workcap`** (works while a turn runs; the next generation reads it):
1737
+
1738
+ - `/workcap`: `work-cap: 320 of 400 tool calls (6 turns, 47 min active, $1.20)`, or `work-cap: no cap (…)` with the
1739
+ totals.
1740
+ - `/workcap 400`: the tool-call cap (and what Continue adds) becomes 400 for this session; `/workcap 90m` or
1741
+ `/workcap 2h`: the active-minutes cap.
1742
+ - `/workcap off` (or `/workcap 0`): no work cap for this session, over the config.
1743
+
1744
+ The state is one file per session, `<data_dir>/sessions/<session id>.json` (`{"tool_calls": {"cap": 800, "step":
1745
+ 400}}`, a meter's `"cap": null` for off).
1746
+
1683
1747
  ## The check-in bundle
1684
1748
 
1685
1749
  `chi bundle install check-in` installs the bundle shipped with chi (`chi
@@ -1712,7 +1776,8 @@ what it has found. check-in counts the turn's tool calls, and when there are
1712
1776
  - **`mode: notify`**: a notice line only.
1713
1777
 
1714
1778
  The count is per turn: a new turn (a prompt, a continue, a reminder) starts
1715
- from zero; steering merged into the running turn doesn't reset it. The polling
1779
+ from zero; steering merged into the running turn doesn't reset it (a cap
1780
+ across the session's turns is [the work-cap bundle](#the-work-cap-bundle)). The polling
1716
1781
  tools are not counted. A nudge that arrives after the model's final answer is
1717
1782
  dropped (logged), never restarting a turn that is done; the card's "Nudged the
1718
1783
  model at N tool calls" then becomes "The answer came first; nudge not sent."
data/docs/releasing.md CHANGED
@@ -13,7 +13,7 @@ can run the whole release; the user approves the notes before the tag and the
13
13
  and `rake release:check` enforce it). The system bundle upgrades itself when
14
14
  chi starts.
15
15
  - **Every other shipped bundle** (btw, check-in, guardrails, known-names,
16
- loop-guard, mcp, skills, slow-commands, agents-md, cost-cap, source-links, github-pr, coordinator, claude-peer, codex-peer, ruby, model-notes, and the profiles core and dev) has its own semver in its
16
+ loop-guard, mcp, skills, slow-commands, agents-md, cost-cap, work-cap, source-links, github-pr, coordinator, claude-peer, codex-peer, ruby, model-notes, and the profiles core and dev) has its own semver in its
17
17
  `manifest.yml` and, when it needs a newer chi, a `requires_chi:` line. They never upgrade by themselves: users run
18
18
  `chi update` (all of them, with the gem) or `chi bundle upgrade NAME`. So:
19
19
  - a change to a bundle's files bumps that bundle's `version:`
data/docs/sessions.md CHANGED
@@ -237,7 +237,7 @@ A session can **continue** an earlier one: "the next run of the same routine" (a
237
237
  - **If the continue fails part-way:** a new link that fails to start gives its moved delegates back before it is deleted (each gets a note saying so; its worker is stopped first, if one started, and a worker that won't stop is reported, the link left), with any report one of them rang it with meanwhile; the previous link, back in the lists, is woken for reports waiting. A continue that dies in the middle leaves a `move.json` in the new link's folder: the new link's next worker finishes that move (only the delegates it names, and without a second note to one already told) as it starts. Until the new link's own worker holds it, a ring from a moved delegate doesn't start another (a `starting` marker in its folder, which that worker removes; a stale one, over 60 s old or from a process that is gone, is ignored). An open delegate below an archived delegate refuses the continue (`under archived …`): unarchive that one, or archive this one.
238
238
  - **Refused too:** a link that is continued already (naming the link that continues it; continue that one, or `last:<id>`), a previous link whose folder is gone, and one whose model's host can't run it when no default model is configured to fall back to. A refusal starts nothing and archives nothing; a new link that fails to start puts back what was archived. Every refusal that can be known up front (the archive's ones included) comes before the recap wait. One continue of a session runs at a time (a lock in its folder; other chains don't wait), so a double click finds the first one's link and is refused, while a `last:<id>` continue that waited follows the chain to its new end.
239
239
  - **Web:** `POST /api/sessions` takes `continues: "<id, prefix or last:id>"`, with `idle: true` (a new link waiting for its first message, previewed by the previous link's, or `preview`) or a `prompt`, and `model` (or `model_name`) for the new link's model, but not `dir`, `llm_context` or `thinking` (400 `invalid_continues`: the previous link's are taken). 201 is the new session's summary, `continues` set, with `moved`: the ids of the delegates that moved to it (`[]` for none), and `model_warning`: the default-model fallback's warning, then any about the model id, joined by `; ` (`null` for none), and `opener`: the previous link's first typed message, in full, when that link continues one itself (`null` otherwise: a chain's first link's first message is its task); 404 `not_found`; 400 `ambiguous_id` for a prefix of several sessions, `invalid_model` for a given model whose host config.yml doesn't have (or a fallback with no default model); 409 `continued` with `next_id` (the link that continues it already, which the page opens instead), `open_children` with `ids` (open delegates that can't move), `folder_gone`, `busy`, `scratch` or `owned_by_tui`. Every session summary carries `continues`.
240
- - **The page:** the info bar's `continue →` continues the open session (`continues: <id>, idle: true`; it reads `continuing…` through the recap wait) and opens the new link with an opener in the composer, not sent (a draft already there stays instead): the previous link's own first typed message when that link continues one (the 201's `opener`; a `!shell` line isn't taken), else `Continue where we left off.`. So whatever you sent on the last link comes back on the next one, and editing it once changes it for every later link; the previous link is archived, a toast says so (`Continued 3fa2c1d0 in 4b7e9a01; 3fa2c1d0 is archived; 1 open delegate moved`), and a moved delegate's card folds under the new link. When the previous link's model's host is gone the link starts on the default model and the toast carries the server's `model_warning` too, and then stays until closed. A fresh link (one with no turn yet, however it was made: `continue →`, `chi send --new --continues`, a reload) shows the model picker in its composer with the link's model, until its first turn starts (a failed or canceled first turn ends it too); a pick runs `/model <name>` in the link (a command bubble; the info bar follows) and changes neither the start page's remembered model nor its Recent list, and a Send right after it waits for the `/model` to be taken. A link a chi REPL owns, or one continued already, shows none. A link continued already has no `continue →`: its `→` link goes to the next link (a stale page's continue answered 409 `continued` opens `next_id`). A continue refused for open delegates that can't move shows a toast that names each one (its preview and what the server says to do: `older chi worker 0.46.1; restart it`, `open in a chi REPL; close it there`), each a link to its page; any other refusal shows the server's words. The info bar of a link carries `← 2026-10-08` (the previous link's day) and, on an earlier link, `2026-10-10 →`, each a link with its id, its full start (`2026-10-10 14:32`) and its preview in the tooltip; a neighbour that started on the open link's own day shows its time instead (`← 09:12`, `14:32 →`). A link made by `continue →` inherits the previous link's preview (and its title, if it has one) when it is created, so its card keeps its chain's title whatever opener it is sent, and its top bar and browser tab show its card's title. A link whose title is the opener and nothing else (a `chi send --new --continues` link whose first message is `Continue where we left off.`) shows its chain's first link's title instead, on its card, in the popover and in its info bar's title line, display only (its messages and data keep the opener; its own title when the first link is gone). The lists fold a chain into the card of its latest listed link, whatever the earlier links' archived state (typing in an archived link brings it back, folded there still), their delegates with them: the card's `↩ #N` chip (`#N of M` when later links aren't listed; its tooltip `Session chain: link N of M`) opens a popover of the other links, newest first (`#N`, its date, with its time (`2026-10-17 14:32`) when another link of the chain started the same day, its own title when it differs from the card's (a link inherits its chain's, so mostly it doesn't), else its recap's first sentence, else its preview, `archived`), a click opens one; the chip shows a live dot or `waiting` for a link that has a worker or waits on the user, and a waiting link puts its chain's card first. All sessions folds nothing while a search is typed or "include archived" is on: every link is a card of its own then, its chip `#N of M`, with its own delegates folded in, so a search finds an earlier link and its delegates.
240
+ - **The page:** the info bar's `continue →` continues the open session (`continues: <id>, idle: true`; it reads `continuing…` through the recap wait) and opens the new link with an opener in the composer, not sent (a draft written in this session stays instead; one carried over from another session or the start page gives way to the opener, and the toast's **Restore draft** brings it back): the previous link's own first typed message when that link continues one (the 201's `opener`; a `!shell` line isn't taken), else `Continue where we left off.`. So whatever you sent on the last link comes back on the next one, and editing it once changes it for every later link; the previous link is archived, a toast says so (`Continued 3fa2c1d0 in 4b7e9a01; 3fa2c1d0 is archived; 1 open delegate moved`), and a moved delegate's card folds under the new link. When the previous link's model's host is gone the link starts on the default model and the toast carries the server's `model_warning` too, and then stays until closed. A fresh link (one with no turn yet, however it was made: `continue →`, `chi send --new --continues`, a reload) shows the model picker in its composer with the link's model, until its first turn starts (a failed or canceled first turn ends it too); a pick runs `/model <name>` in the link (a command bubble; the info bar follows) and changes neither the start page's remembered model nor its Recent list, and a Send right after it waits for the `/model` to be taken. A link a chi REPL owns, or one continued already, shows none. A link continued already has no `continue →`: its `→` link goes to the next link (a stale page's continue answered 409 `continued` opens `next_id`). A continue refused for open delegates that can't move shows a toast that names each one (its preview and what the server says to do: `older chi worker 0.46.1; restart it`, `open in a chi REPL; close it there`), each a link to its page; any other refusal shows the server's words. The info bar of a link carries `← 2026-10-08` (the previous link's day) and, on an earlier link, `2026-10-10 →`, each a link with its id, its full start (`2026-10-10 14:32`) and its preview in the tooltip; a neighbour that started on the open link's own day shows its time instead (`← 09:12`, `14:32 →`). A link made by `continue →` inherits the previous link's preview (and its title, if it has one) when it is created, so its card keeps its chain's title whatever opener it is sent, and its top bar and browser tab show its card's title. A link whose title is the opener and nothing else (a `chi send --new --continues` link whose first message is `Continue where we left off.`) shows its chain's first link's title instead, on its card, in the popover and in its info bar's title line, display only (its messages and data keep the opener; its own title when the first link is gone). The lists fold a chain into the card of its latest listed link, whatever the earlier links' archived state (typing in an archived link brings it back, folded there still), their delegates with them: the card's `↩ #N` chip (`#N of M` when later links aren't listed; its tooltip `Session chain: link N of M`) opens a popover of the other links, newest first (`#N`, its date, with its time (`2026-10-17 14:32`) when another link of the chain started the same day, its own title when it differs from the card's (a link inherits its chain's, so mostly it doesn't), else its recap's first sentence, else its preview, `archived`), a click opens one; the chip shows a live dot or `waiting` for a link that has a worker or waits on the user, and a waiting link puts its chain's card first. All sessions folds nothing while a search is typed or "include archived" is on: every link is a card of its own then, its chip `#N of M`, with its own delegates folded in, so a search finds an earlier link and its delegates.
241
241
  - **CLI:** `chi send --new --continues (ID|PREFIX|last:ID) [--model M] [-m TEXT]` (see *Starting a session*).
242
242
  - **Which link:** an id, a unique prefix, or `last:<id or prefix>`, the latest link of the chain that session is in. It walks the links that still exist: a deleted link in the middle (`chi sessions delete`, the retention sweep of an unarchived one) ends the walk before it, so `last:` of an earlier link then names that earlier link, and continuing it starts a second line beside the links after the gap; name a link after the gap instead. A trigger names the chain this way: `chi send` to an archived link would bring it back (a human's input), not continue the chain.
243
243
 
data/docs/sub-agent.md CHANGED
@@ -47,7 +47,9 @@ chi answer ID --question QID --dismiss [--timeout S] [--format json]
47
47
 
48
48
  A child with a bundle setting of its own, such as a $ cap (the [cost-cap bundle](plugins.md#the-cost-cap-bundle)):
49
49
  `chi send --new --wait --format json --set cost-cap.cap=3 -m "task"`. At the cap the wait exits 3 with the cost-cap
50
- question; `chi answer ID --question QID --option Continue` (or `Stop`) answers it.
50
+ question; `chi answer ID --question QID --option Continue` (or `Stop`) answers it. A cap on the child's work across
51
+ its turns (the [work-cap bundle](plugins.md#the-work-cap-bundle)), the bound a parent that keeps answering the step
52
+ limit's Continue doesn't otherwise have: `--set work-cap.tool_calls=400` (or `work-cap.minutes=60`, active minutes).
51
53
 
52
54
  `chi answer` waits after answering, the same way `chi send --wait` does, so the
53
55
  next question comes back as exit 3 again. What it waits for is the answered
@@ -122,7 +124,10 @@ relayed to the parent's user the way approvals are.
122
124
 
123
125
  A session at its cost cap (the [cost-cap bundle](plugins.md#the-cost-cap-bundle)) waits the same way, as a
124
126
  question of kind `hook` with header "Cost cap" and options `Continue` and `Stop`: `--option Continue` raises the cap
125
- by another cap's worth and the turn goes on, `--option Stop` ends it `canceled` with `stopped_by: cost-cap`.
127
+ by another cap's worth and the turn goes on, `--option Stop` ends it `canceled` with `stopped_by: cost-cap`. A session at its work cap (the
128
+ [work-cap bundle](plugins.md#the-work-cap-bundle)) asks the same, header "Work cap", the question naming the
129
+ session's totals (`Work cap reached: 412 tool calls of 400 (6 turns, 47 min active, $1.20). Continue raises it to
130
+ 800.`); `stopped_by: work-cap` on Stop.
126
131
 
127
132
  ## Approvals
128
133
 
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: core
3
- version: 0.3.4
3
+ version: 0.4.0
4
4
  scope: system
5
5
  description: "Recommended safety set"
6
6
  trust_level: reviewed
7
- # Its members' highest floor (cost-cap's)
8
- requires_chi: ">= 0.92.0"
7
+ # Its members' highest floor (work-cap's: ctx.usage, raised at the release)
8
+ requires_chi: ">= 0.100.0"
9
9
  includes:
10
10
  - loop-guard
11
11
  - check-in
@@ -13,3 +13,4 @@ includes:
13
13
  - slow-commands
14
14
  - agents-md
15
15
  - cost-cap
16
+ - work-cap
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: samagotchi-system
3
- version: 0.99.0
3
+ version: 0.100.1
4
4
  scope: system
5
5
  description: Default system memories — identity, self map, config modification protocol, memory guide and the delegated-session rules
6
6
  files:
@@ -0,0 +1,13 @@
1
+ ---
2
+ name: work-cap
3
+ version: 0.1.1
4
+ scope: system
5
+ description: "A per-session work cap (tool calls, active minutes) across turns: at a cap the turn pauses on Continue (another cap's worth) / Stop with the session's totals, a warning at 80% goes to the user only; /workcap shows or sets it"
6
+ trust_level: reviewed
7
+ plugin:
8
+ file: plugin.rb
9
+ sha256: sha256:f4ce7ea7f5801490a29e232c41c7bca2007c140d21f1a78bbfb60e6b6fd4f64b
10
+ # ask_user's effects: are in chi 0.92.0; ctx.usage is new in the next chi
11
+ requires_chi: ">= 0.100.0"
12
+ # chi --set work-cap.tool_calls=400 / work-cap.minutes=60: the session's own caps, a number or off
13
+ session_settings: {tool_calls: number, minutes: number}