samagotchi 0.139.0 → 0.139.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -1
- data/docs/cli.md +1 -1
- data/docs/configuration.md +3 -1
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/tool-guardrails.md +1 -1
- data/docs/sessions.md +3 -2
- data/lib/samagotchi/bundle_command.rb +21 -21
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/plugin.rb +8 -3
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/self_map.md +2 -2
- data/lib/samagotchi/cancellation_controller.rb +4 -4
- data/lib/samagotchi/context_fetch.rb +2 -2
- data/lib/samagotchi/cut.rb +21 -0
- data/lib/samagotchi/cut_policy.rb +6 -5
- data/lib/samagotchi/hook_routes.rb +3 -2
- data/lib/samagotchi/kernel_loop.rb +8 -6
- data/lib/samagotchi/launch_mode.rb +5 -2
- data/lib/samagotchi/llm/chat_loop.rb +3 -2
- data/lib/samagotchi/llm/openai_chat.rb +1 -1
- data/lib/samagotchi/memory_bundle/status.rb +13 -3
- data/lib/samagotchi/project_env.rb +16 -2
- data/lib/samagotchi/steer_cut.rb +2 -1
- data/lib/samagotchi/testing/boundary.rb +3 -1
- data/lib/samagotchi/turn_note.rb +6 -6
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/public/app.js +3 -1
- data/lib/samagotchi/web/public/composer.css +1 -1
- data/lib/samagotchi/web/public/index.html +10 -9
- data/lib/samagotchi/web/public/notifications.js +10 -1
- data/lib/samagotchi/web/public/revise_stack.js +6 -5
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1ef79d43ec1d5fc441ca8892ceb240a42fca4629295e71debc495df44b5efde8
|
|
4
|
+
data.tar.gz: 39b1d809ff216fcd1a1af146dc7858b45e80f94a8018e00841839573b753be0d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 228b7f233e3d6fd752ba0dbad22f4fed1e27910eb97a37190ec9172db94d1369dffe13f64389b3e1f77914bc845d2625c273c7569594fe76ffc4aa18edf749f6
|
|
7
|
+
data.tar.gz: f6439358124c6785a2ab54947e97928f725a817146bcd24e2263734e48bd2718b693e6573d4253fc64bbfe660a390125720f6a71662f31400bb7fbd39a4dd9da
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,29 @@ and commands may change between minor versions. How releases are made:
|
|
|
7
7
|
[docs/releasing.md](docs/releasing.md).
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.139.1] - 2026-10-31
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- A project's mise.toml can no longer set `TMPDIR` for the session's tools: the guardrails judge `$TMPDIR` paths
|
|
15
|
+
by chi's own, so a write there could have been taken for a tmp write while it landed in the project.
|
|
16
|
+
- MCP servers (mcp 0.7.4) and context sources start like `execute`'s commands: without chi's own Bundler setup
|
|
17
|
+
(`RUBYOPT=-rbundler/setup`, `BUNDLE_GEMFILE`) or the project's `RUBYOPT`/`RUBYLIB`/Bundler variables, so a
|
|
18
|
+
chi run from a checkout no longer hands them its Gemfile.
|
|
19
|
+
- A worker that never starts its Bridge (`chi --attach`, the web's wake) now points at `chi home doctor`, which names
|
|
20
|
+
a worker that failed to boot the chi home's chi.
|
|
21
|
+
- The system bundle's self-knowledge names `execute`'s early hand-over to a task (a message for the turn), not only
|
|
22
|
+
its time limit.
|
|
23
|
+
- Web: a Revise stack's version chip names the version you are looking at ("v1 of 3"), and an earlier version you
|
|
24
|
+
switched to has no Copy link (it linked the newest turn with the earlier version's quote).
|
|
25
|
+
- Web: the bell is one control in each place: a toggle in a browser tab, the notifications sheet's opener in the
|
|
26
|
+
Home Screen app.
|
|
27
|
+
- Web: a markdown report in the stage's one-line rows is exactly a plain row's height.
|
|
28
|
+
- Web: on a phone, Revise, the stack switcher, Copy, the composer's Revise ✕, a fold's size and the activity
|
|
29
|
+
summaries no longer stay tinted after a tap.
|
|
30
|
+
|
|
31
|
+
Update with `chi update` (system, mcp 0.7.4).
|
|
32
|
+
|
|
10
33
|
## [0.139.0] - 2026-10-30
|
|
11
34
|
|
|
12
35
|
### Added
|
|
@@ -5303,7 +5326,8 @@ and long-lived sessions.
|
|
|
5303
5326
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
5304
5327
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
5305
5328
|
|
|
5306
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.139.
|
|
5329
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.139.1...HEAD
|
|
5330
|
+
[0.139.1]: https://github.com/dm1try/samagotchi/compare/v0.139.0...v0.139.1
|
|
5307
5331
|
[0.139.0]: https://github.com/dm1try/samagotchi/compare/v0.138.0...v0.139.0
|
|
5308
5332
|
[0.138.0]: https://github.com/dm1try/samagotchi/compare/v0.137.0...v0.138.0
|
|
5309
5333
|
[0.137.0]: https://github.com/dm1try/samagotchi/compare/v0.136.1...v0.137.0
|
data/docs/cli.md
CHANGED
|
@@ -1061,7 +1061,7 @@ moves into the later stub. The newest version has a chip, "v3 · 2 earlier ▾",
|
|
|
1061
1061
|
listing every version by its prompt; pick one to jump to its stub. Before
|
|
1062
1062
|
that chip, a "‹ 3/3 ›" switcher reads an earlier version's answer in the
|
|
1063
1063
|
newest one's place (its left edge dashed while it shows; Copy copies the
|
|
1064
|
-
version on view); › back to the newest. Switching only changes what this
|
|
1064
|
+
version on view, and the chip names it, "v1 of 3"); › back to the newest. Switching only changes what this
|
|
1065
1065
|
page shows, and a reload shows the newest again. Revise
|
|
1066
1066
|
again on the newest version adds to the stack. Stacks are session data (the
|
|
1067
1067
|
stamp is on the Revise prompt, see [sessions.md](sessions.md), Revise
|
data/docs/configuration.md
CHANGED
|
@@ -1402,7 +1402,9 @@ launches it. It is recorded in `~/.config/samagotchi/home.json` (`$XDG_CONFIG_HO
|
|
|
1402
1402
|
`home_older_than_spawner`), but chi web's restart runs the newest chi it knows outside the home instead
|
|
1403
1403
|
(`restart_home_older`): its flags may be ones the older chi doesn't know.
|
|
1404
1404
|
- **A home that still fails at boot** (a broken install): the worker writes one `boot_failed` line to the debug log
|
|
1405
|
-
(`log.file`) and exits, rather than leave the spawner waiting; `chi home doctor` reports the newest one
|
|
1405
|
+
(`log.file`) and exits, rather than leave the spawner waiting; `chi home doctor` reports the newest one, and an
|
|
1406
|
+
attach whose worker never starts its Bridge says so ("… did not start its Bridge in time (chi home doctor shows a
|
|
1407
|
+
worker that failed to boot)").
|
|
1406
1408
|
- Each machine has its own home (a remote's is that machine's).
|
|
1407
1409
|
|
|
1408
1410
|
### Checking it: chi home doctor
|
|
@@ -45,7 +45,7 @@ Behavior:
|
|
|
45
45
|
- `execute` and `task_create` both spawn with chi's own Ruby/Bundler setup scrubbed (`TaskRuntime.sanitized_spawn_env`): `RUBYOPT` loses its `-r…bundler/setup` flags (others stay) and `RUBYLIB`, `BUNDLE_GEMFILE`, `BUNDLE_BIN_PATH`, `BUNDLER_VERSION` are unset, so a project's `ruby -e` or `bundle exec` uses its own Gemfile when chi runs under `bundle exec`. `task_create` refuses those keys in the model's `env`; `execute`'s `env` comes from chi's callers (`Builtins`, `!cmd`), not the model, and is set over the scrub. An `execute` handed over at its limit keeps the process it spawned, so the task runs with the same env.
|
|
46
46
|
- `execute` and `task_create` export chi's own markers into the command, set over the model's `env` (which can't set them: `Error: env key is reserved`): `SAMAGOTCHI_PARENT_SESSION` (the session's id, `chi` without one; a chi started there answers as a parent agent), `SAMAGOTCHI_SESSION_MODEL` (the resolved ref the session runs on, read at each call so it follows `/model`; unset when unknown) `SAMAGOTCHI_SESSION_THINKING` (the session's own thinking level; unset when it has none) and `SAMAGOTCHI_SESSION_REASONING_HISTORY` (its own reasoning_history level, the same way). `chi self` reads the last three for its model, thinking level and reasoning history. The four are `ChildEnv::SESSION_MARKERS` (`TaskRuntime::MARKER_ENV_KEYS`), which a spawned worker unsets (`ChildEnv.unset`, merged into its spawn env). A session's worker never inherits them from whoever started it (a `chi send` run by another session's `execute`): its other commands (a user's `!cmd`, an MCP server, a context command) have none.
|
|
47
47
|
- They also pass on a set `XDG_STATE_HOME` / `XDG_CONFIG_HOME` made absolute (`Builtins.xdg_absolute`, via `Paths`), so a chi the command runs in another `cwd` (`chi send`) reads this chi's state and config. These aren't reserved: an XDG dir the model sets in `task_create`'s `env` stays its own.
|
|
48
|
-
- Every runner starts from the session's project env, `ProjectEnv.tools_env`: the overlay a spawned worker computed once at boot (`SessionManager.apply_project_env` → `ProjectEnv.compute` → `ProjectEnv.install!`; its folder's `mise env`, and the chi shim's dir first on PATH so `chi` is the chi home's), less `ProjectEnv::TOOLS_DROP` (`RUBYOPT`, `RUBYLIB`, Bundler's: the scrub above sets those). Outside a worker (`ProjectEnv::NONE`) it is `{}`. The sites: `execute` and `task_create` (`Tools::Builtins#project_env`, under the markers and the model's `env`; a participant's turn gets `{}`), the user's `!cmd` (`SessionCommands#shell`) and a context source's command (`ContextFetch#env_for`)
|
|
48
|
+
- Every runner starts from the session's project env, `ProjectEnv.tools_env`: the overlay a spawned worker computed once at boot (`SessionManager.apply_project_env` → `ProjectEnv.compute` → `ProjectEnv.install!`; its folder's `mise env`, and the chi shim's dir first on PATH so `chi` is the chi home's), less `ProjectEnv::TOOLS_DROP` (`RUBYOPT`, `RUBYLIB`, Bundler's: the scrub above sets those). Outside a worker (`ProjectEnv::NONE`) it is `{}`. The sites: `execute` and `task_create` (`Tools::Builtins#project_env`, under the markers and the model's `env`; a participant's turn gets `{}`), the user's `!cmd` (`SessionCommands#shell`), and through `ProjectEnv.spawn_env` (the scrub above, then `tools_env`: what `execute` spawns with, for the runners that don't go through it) a context source's command (`ContextFetch#env_for`) and an MCP server, under the server's own keys (`bundles/mcp/plugin.rb` `server_env`, from mcp 0.7.4; no participant rule: the server is the session's). chi's own process env is never changed, and the project env never enters the prompt. User docs: [Project env](../sessions.md#project-env); the launch side: [How chi launches itself](../architecture.md#how-chi-launches-itself).
|
|
49
49
|
|
|
50
50
|
## Finish wake
|
|
51
51
|
|
|
@@ -50,7 +50,7 @@ Configuration (config.yml `execute.*` keys and CLI flags too, as for `read`):
|
|
|
50
50
|
- `SAMAGOTCHI_EXECUTE_TIMEOUT_SEC` (`execute.timeout_sec`, default `120`): what a command still running then gets is `execute.on_timeout`.
|
|
51
51
|
- `SAMAGOTCHI_EXECUTE_ON_TIMEOUT` (`execute.on_timeout`, default `task`): `task` hands the command over as a background task, not killed; the result starts `Still running at the 120s limit: moved to a background task, not killed.` (`Execute::HANDED_OVER`), then its `task_id:` and the tails of stdout and stderr so far, and `task_wait` waits for it. `kill` stops it and answers `Error: command timed out after …s` (`Execute::TIMEOUT_PREFIX`) with the hint to use `task_create`. The `!cmd` bang always kills. With `task`, a message from the user waiting for the turn hands a command over early, after 3 s, with the first line `Still running at the 4s mark: a message from the user is waiting, …` (same `Execute::HANDED_OVER` start; never under `kill`). See [Background tasks](background-tasks.md).
|
|
52
52
|
|
|
53
|
-
The env a command runs in (the session's project env, `ProjectEnv.tools_env`, then the Ruby/Bundler scrub and chi's markers) is the same for `execute`, `task_create`, `!cmd
|
|
53
|
+
The env a command runs in (the session's project env, `ProjectEnv.tools_env`, then the Ruby/Bundler scrub and chi's markers) is the same for `execute`, `task_create`, `!cmd`, context sources and MCP servers (`ProjectEnv.spawn_env`): see [Background tasks](background-tasks.md).
|
|
54
54
|
|
|
55
55
|
Implementation note:
|
|
56
56
|
|
data/docs/sessions.md
CHANGED
|
@@ -386,7 +386,8 @@ mise.toml pins Ruby 3.4 gets that `ruby`, `gem` and `bundle` in its tools while
|
|
|
386
386
|
belong to the starter's Ruby), unless mise sets its own; kept when the Ruby stays the same.
|
|
387
387
|
- **Never what a `chi` in a tool reads.** The tools' env never takes `HOME`, any `XDG_*`, any `SAMAGOTCHI_*`,
|
|
388
388
|
`CHI_REMOTE`, `RACK_ENV` or `CI` from the project (a `chi` run there would read another config or state dir, or
|
|
389
|
-
send its session commands to another machine)
|
|
389
|
+
send its session commands to another machine), nor `TMPDIR` (the guardrails judge a tool's `$TMPDIR/x` and its
|
|
390
|
+
tmp dirs by the worker's own `TMPDIR`, so the tools run on that one): they are dropped
|
|
390
391
|
and the debug log names them once (`project_env_reserved`). `mise env` runs without `MISE_YES` (it would trust an
|
|
391
392
|
untrusted mise.toml for you) and `CI`; your `MISE_TRUSTED_CONFIG_PATHS` stays.
|
|
392
393
|
- **Only this session.** A worker started from this one (a restart, a delegate child) starts on the worker's own
|
|
@@ -403,7 +404,7 @@ mise.toml pins Ruby 3.4 gets that `ruby`, `gem` and `bundle` in its tools while
|
|
|
403
404
|
included, asks you first; a read-only mention (`find . -name mise.toml`) doesn't. `.envrc` and mise's local files
|
|
404
405
|
(`mise.local.toml`, `mise.<env>.local.toml`, dotted or not, and `config.local.toml` / `config.<env>.local.toml` in
|
|
405
406
|
`.config/mise/`, `mise/`, `.mise/`) are secrets to them, as `.env` is.
|
|
406
|
-
- **What tools still drop**: every `execute`, task
|
|
407
|
+
- **What tools still drop**: every `execute`, task, `!cmd`, context source and MCP server takes no `RUBYOPT`, `RUBYLIB`, `BUNDLE_GEMFILE`,
|
|
407
408
|
`BUNDLE_BIN_PATH` or `BUNDLER_VERSION` from the project's mise env: it runs without `RUBYLIB` and Bundler's
|
|
408
409
|
variables, and with the worker's own `RUBYOPT`, Bundler's part removed (`TaskRuntime`).
|
|
409
410
|
- **`chi` in a tool** stays the home's: with a chi home the tools' PATH starts with `$XDG_STATE_HOME/samagotchi/bin`,
|
|
@@ -447,17 +447,17 @@ module Samagotchi
|
|
|
447
447
|
@stdout.puts "Bundle '#{name}' not installed."
|
|
448
448
|
return 0
|
|
449
449
|
end
|
|
450
|
-
@stdout.puts "Bundle: #{name} v#{st
|
|
451
|
-
if st
|
|
450
|
+
@stdout.puts "Bundle: #{name} v#{st.provenance.version}#{dev_label(st.provenance)} scope=#{status_scope(st)} installed=#{st.provenance.installed_at}"
|
|
451
|
+
if st.scope_error
|
|
452
452
|
@stdout.puts "Target: (unknown scope: this chi can't resolve it; upgrade chi or reinstall the bundle)"
|
|
453
453
|
else
|
|
454
|
-
@stdout.puts "Target: #{st
|
|
454
|
+
@stdout.puts "Target: #{st.target_dir}"
|
|
455
455
|
end
|
|
456
|
-
st
|
|
456
|
+
st.files.each do |k, info|
|
|
457
457
|
next @stdout.puts(" #{k}: unchecked") if info[:unchecked]
|
|
458
458
|
|
|
459
459
|
mods = []
|
|
460
|
-
mods << "conflict (kept your edits over v#{st
|
|
460
|
+
mods << "conflict (kept your edits over v#{st.provenance.version}: chi bundle diff #{name} #{k})" if info[:conflict]
|
|
461
461
|
mods << "modified" if info[:modified]
|
|
462
462
|
mods << "missing" if info[:missing]
|
|
463
463
|
mods << "no-index" unless info[:index_present] || info[:overlay]
|
|
@@ -466,9 +466,9 @@ module Samagotchi
|
|
|
466
466
|
@stdout.puts " #{k}: #{label}"
|
|
467
467
|
end
|
|
468
468
|
# Hooks
|
|
469
|
-
hooks = st
|
|
470
|
-
trust = st
|
|
471
|
-
commit = st
|
|
469
|
+
hooks = st.provenance.hooks
|
|
470
|
+
trust = st.provenance.trust_level || "experimental"
|
|
471
|
+
commit = st.provenance.source_commit
|
|
472
472
|
@stdout.puts " trust_level: #{trust}" if hooks.any? || trust
|
|
473
473
|
@stdout.puts " source_commit: #{commit}" if commit
|
|
474
474
|
if hooks.any?
|
|
@@ -478,17 +478,17 @@ module Samagotchi
|
|
|
478
478
|
exists = File.exist?(hook_path) ? "ok" : "missing"
|
|
479
479
|
@stdout.puts " #{hk}: #{hook.events_field} on_error=#{hook.on_error} priority=#{hook.priority} [#{exists}]"
|
|
480
480
|
end
|
|
481
|
-
@stdout.puts " requires_chi: #{st
|
|
482
|
-
@stdout.puts " not loaded: #{st
|
|
481
|
+
@stdout.puts " requires_chi: #{st.provenance.requires_chi}" if st.hooks_requires_failure
|
|
482
|
+
@stdout.puts " not loaded: #{st.hooks_requires_failure}" if st.hooks_requires_failure
|
|
483
483
|
end
|
|
484
|
-
if (plugin = st
|
|
484
|
+
if (plugin = st.plugin)
|
|
485
485
|
note = plugin[:state] == "modified" ? " (edited after install: not loaded; reinstall the bundle)" : ""
|
|
486
486
|
@stdout.puts " Plugin: #{plugin[:file]} [#{plugin[:state]}]#{note}"
|
|
487
487
|
@stdout.puts " requires_chi: #{plugin[:requires_chi]}" if plugin[:requires_chi]
|
|
488
488
|
@stdout.puts " not loaded: #{plugin[:requires_failure]}" if plugin[:requires_failure]
|
|
489
489
|
end
|
|
490
|
-
st
|
|
491
|
-
(st
|
|
490
|
+
st.needs.each { |need| @stdout.puts " #{Samagotchi::MemoryBundle::Status.need_line(need)}" }
|
|
491
|
+
(st.provenance.includes || []).each do |member|
|
|
492
492
|
state = Samagotchi::MemoryBundle::Provenance.new(name: member).installed? ? "installed" : "not installed: chi bundle install #{member}"
|
|
493
493
|
@stdout.puts " includes #{member} [#{state}]"
|
|
494
494
|
end
|
|
@@ -499,17 +499,17 @@ module Samagotchi
|
|
|
499
499
|
next @stdout.puts(" #{bname} (manifest.json unreadable)") if record.error?
|
|
500
500
|
|
|
501
501
|
st = Samagotchi::MemoryBundle::Status.bundle_status(bname)
|
|
502
|
-
mods = st
|
|
503
|
-
hooks_count = st
|
|
502
|
+
mods = st.files.values.count { |v| v[:modified] || v[:missing] }
|
|
503
|
+
hooks_count = st.provenance.hooks.size
|
|
504
504
|
hook_info = hooks_count > 0 ? " hooks=#{hooks_count}" : ""
|
|
505
|
-
plugin = st
|
|
505
|
+
plugin = st.plugin
|
|
506
506
|
mods += 1 if plugin && (plugin[:state] != "ok" || plugin[:requires_failure])
|
|
507
|
-
mods += 1 if st
|
|
508
|
-
mods += 1 if st
|
|
507
|
+
mods += 1 if st.hooks_requires_failure
|
|
508
|
+
mods += 1 if st.scope_error
|
|
509
509
|
plugin_info = plugin ? " plugin=#{plugin[:file]}" : ""
|
|
510
|
-
includes = st
|
|
510
|
+
includes = st.provenance.includes
|
|
511
511
|
includes_info = includes ? " includes=#{includes.join(",")}" : ""
|
|
512
|
-
@stdout.puts " #{bname} v#{st
|
|
512
|
+
@stdout.puts " #{bname} v#{st.provenance.version}#{dev_label(st.provenance)} scope=#{status_scope(st)} files=#{st.files.size}#{hook_info}#{plugin_info}#{includes_info} issues=#{mods}"
|
|
513
513
|
end
|
|
514
514
|
end
|
|
515
515
|
0
|
|
@@ -523,7 +523,7 @@ module Samagotchi
|
|
|
523
523
|
end
|
|
524
524
|
|
|
525
525
|
# "team (unknown)" for a scope this chi can't resolve (Status#bundle_status).
|
|
526
|
-
def status_scope(st) = st
|
|
526
|
+
def status_scope(st) = st.scope_error ? "#{st.scope} (unknown)" : st.scope
|
|
527
527
|
|
|
528
528
|
def diff(rest)
|
|
529
529
|
args = rest.reject { |a| a.start_with?("--") }
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mcp
|
|
3
|
-
version: 0.7.
|
|
3
|
+
version: 0.7.4
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Tools from MCP servers (stdio): each server in config.yml bundles: mcp: servers: runs for the session; their tools go into chi's tool index: the model searches them with find_tools and calls one with call_tool (<server>/<tool>); /mcp lists them"
|
|
6
6
|
trust_level: reviewed
|
|
7
7
|
files: {}
|
|
8
8
|
plugin:
|
|
9
9
|
file: plugin.rb
|
|
10
|
-
sha256: sha256:
|
|
10
|
+
sha256: sha256:b892de35b1fc16dd22152cd95a6c90447a2be45f4daff04a4648ab2653463a20
|
|
11
11
|
# 0.67.0 is the release with chi.tool_source (the core tool index the bundle publishes into).
|
|
12
12
|
requires_chi: ">= 0.121.0"
|
|
@@ -454,10 +454,15 @@ class Plugin
|
|
|
454
454
|
client
|
|
455
455
|
end
|
|
456
456
|
|
|
457
|
-
# A server's env: the session's project env
|
|
458
|
-
#
|
|
457
|
+
# A server's env: the session's project env as execute's commands get it
|
|
458
|
+
# (the worker's mise env without chi's own Ruby and Bundler setup, on a
|
|
459
|
+
# chi that has it: Samagotchi::ProjectEnv.spawn_env) under the server's
|
|
460
|
+
# own keys. An older chi has only the overlay, or none.
|
|
459
461
|
def server_env(server)
|
|
460
|
-
project = defined?(Samagotchi::ProjectEnv)
|
|
462
|
+
project = if !defined?(Samagotchi::ProjectEnv) then {}
|
|
463
|
+
elsif Samagotchi::ProjectEnv.respond_to?(:spawn_env) then Samagotchi::ProjectEnv.spawn_env
|
|
464
|
+
else Samagotchi::ProjectEnv.current.env
|
|
465
|
+
end
|
|
461
466
|
project.merge(server.env)
|
|
462
467
|
end
|
|
463
468
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: samagotchi-system
|
|
3
|
-
version: 0.139.
|
|
3
|
+
version: 0.139.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:
|
|
7
7
|
identity.md: sha256:29c2f7df9f6c5acbc5ec17000a4490741e55e426af15ca0d81c8706edd9bd027
|
|
8
|
-
self_map.md: sha256:
|
|
8
|
+
self_map.md: sha256:3253cb9ae7f09821d8fe1fed5154dd2b5e33fe0e9f096eee2af981d7d7d66f16
|
|
9
9
|
config_modification_protocol.md: sha256:ff4efccf2bdde453fc4e6741df8c18f78da78e353f6b1a5a64d0e7a631aea258
|
|
10
10
|
memory_guide.md: sha256:3b9ff779ee962f1307d56937c826d5e581923114f3f39455b779ca618b363cd7
|
|
11
11
|
delegated.md: sha256:a0d6ddcd4a8be06526d964898611775fd251cfd9d145a9d16afaccaa503597bd
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
asking for changes, checks failing, the PR closed), chi may start a turn for it with no user
|
|
32
32
|
message ("context <name> changed"): I tell my user what changed and act on nothing.
|
|
33
33
|
- `[TASK FINISHED from task <id8>, …]` is a background task I started (`task_create`, an `execute`
|
|
34
|
-
handed over at its limit) that finished while I wasn't
|
|
35
|
-
and log tail. When it opens a turn with no user message ("task <id8> finished"), I carry on with
|
|
34
|
+
handed over at its limit, or early when a message came for my turn) that finished while I wasn't
|
|
35
|
+
waiting on it: its exit code, duration and log tail. When it opens a turn with no user message ("task <id8> finished"), I carry on with
|
|
36
36
|
what I planned for its result, or tell my user in a sentence or two how it went. Without the
|
|
37
37
|
"chi started this turn" line it is only information for my next answer.
|
|
38
38
|
- `forget_outputs` (only when my model's `llm_context.strategy` has the experimental forget layer)
|
|
@@ -20,8 +20,8 @@ module Samagotchi
|
|
|
20
20
|
@generation = nil
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
# @param detail [Object, nil] what the canceller says about it (
|
|
24
|
-
# generation
|
|
23
|
+
# @param detail [Object, nil] what the canceller says about it (the Cut
|
|
24
|
+
# of a cut generation, or a turn stopped by a hook)
|
|
25
25
|
def cancel!(reason = :manual, detail = nil)
|
|
26
26
|
listeners = []
|
|
27
27
|
@mutex.synchronize do
|
|
@@ -54,11 +54,11 @@ module Samagotchi
|
|
|
54
54
|
@mutex.synchronize { @detail }
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
-
# Who stopped it when a hook did: the
|
|
57
|
+
# Who stopped it when a hook did: the Cut's +by+ (the bundle, or the
|
|
58
58
|
# hook's label); nil for any other cancel.
|
|
59
59
|
def stopped_by
|
|
60
60
|
reason, detail = @mutex.synchronize { [@reason, @detail] }
|
|
61
|
-
by = detail
|
|
61
|
+
by = detail.by.to_s if reason == :hook && detail.respond_to?(:by)
|
|
62
62
|
by unless by.nil? || by.empty?
|
|
63
63
|
end
|
|
64
64
|
|
|
@@ -109,8 +109,8 @@ module Samagotchi
|
|
|
109
109
|
|
|
110
110
|
def env_for(location, name)
|
|
111
111
|
previous = location.snapshot_path(name)
|
|
112
|
-
# The session's project env (ProjectEnv) under the source's own keys.
|
|
113
|
-
ProjectEnv.
|
|
112
|
+
# The session's project env (ProjectEnv.spawn_env) under the source's own keys.
|
|
113
|
+
ProjectEnv.spawn_env.merge("SAMAGOTCHI_CONTEXT_NAME" => name,
|
|
114
114
|
"SAMAGOTCHI_CONTEXT_PREVIOUS" => File.exist?(previous) ? previous : nil)
|
|
115
115
|
end
|
|
116
116
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
# Why a generation was cut: what CancellationController#cancel_generation!
|
|
5
|
+
# is given (a bundle's stop_generation, or Engine#cut_for_steer) and what
|
|
6
|
+
# the loops keep on a cut generation. +by+ is who stopped it (the hook's
|
|
7
|
+
# label, or "steer"), +reason+ what it said, +steer+ true when a queued user
|
|
8
|
+
# message cut it (no nudge, no retry attempt spent), +source+ that message,
|
|
9
|
+
# for the steer_cut row. The same value rides a whole-turn stop
|
|
10
|
+
# (stop_turn): the cancel note reads its +by+ and +reason+.
|
|
11
|
+
Cut = Data.define(:by, :reason, :steer, :source) do
|
|
12
|
+
def initialize(by: nil, reason: nil, steer: false, source: nil) = super
|
|
13
|
+
|
|
14
|
+
# A hook's cut (stop_generation, or stop_turn): the bundle +by+ and its
|
|
15
|
+
# +reason+.
|
|
16
|
+
def self.hook(by: nil, reason: nil) = new(by: by, reason: reason)
|
|
17
|
+
|
|
18
|
+
# Engine#cut_for_steer: a queued message cut the streaming generation.
|
|
19
|
+
def self.steer(source) = new(by: "steer", reason: "a new message", steer: true, source: source)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "cut"
|
|
3
4
|
require_relative "turn_note"
|
|
4
5
|
|
|
5
6
|
module Samagotchi
|
|
@@ -30,7 +31,7 @@ module Samagotchi
|
|
|
30
31
|
|
|
31
32
|
module_function
|
|
32
33
|
|
|
33
|
-
# +cut+: the
|
|
34
|
+
# +cut+: the Cut (what stop_generation passed). +emit+ takes an
|
|
34
35
|
# event Hash; +inject+ puts queued input in (true when there was any).
|
|
35
36
|
# +finish_reason+ and +thinking_chars+: the cut generation's, for the
|
|
36
37
|
# retry's row.
|
|
@@ -40,13 +41,13 @@ module Samagotchi
|
|
|
40
41
|
return STOPPED if cancel_controller.cancelled?
|
|
41
42
|
|
|
42
43
|
# The row under the cut step, above the message it was cut for.
|
|
43
|
-
emit.call({ type: :steer_cut, iteration: iteration, source: cut
|
|
44
|
-
return AGAIN if inject.call || cut
|
|
44
|
+
emit.call({ type: :steer_cut, iteration: iteration, source: cut.source.to_s }) if cut.steer
|
|
45
|
+
return AGAIN if inject.call || cut.steer
|
|
45
46
|
|
|
46
47
|
if empty_retry.left?
|
|
47
|
-
empty_retry.nudge!(conversation, TurnNote.cut_retry(cut
|
|
48
|
+
empty_retry.nudge!(conversation, TurnNote.cut_retry(cut.by, cut.reason),
|
|
48
49
|
emit: emit, iteration: iteration, finish_reason: finish_reason,
|
|
49
|
-
thinking_chars: thinking_chars, stopped_by: cut
|
|
50
|
+
thinking_chars: thinking_chars, stopped_by: cut.by)
|
|
50
51
|
return AGAIN
|
|
51
52
|
end
|
|
52
53
|
empty_retry.drop_nudge!(conversation)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "cut"
|
|
3
4
|
require_relative "event_routes"
|
|
4
5
|
require_relative "hooks"
|
|
5
6
|
require_relative "log"
|
|
@@ -104,7 +105,7 @@ module Samagotchi
|
|
|
104
105
|
return false unless ctrl && !ctrl.cancelled?
|
|
105
106
|
|
|
106
107
|
notify("stopped the turn: #{reason}", :warn, hook)
|
|
107
|
-
ctrl.cancel!(:hook,
|
|
108
|
+
ctrl.cancel!(:hook, Cut.hook(by: source(hook), reason: reason.to_s))
|
|
108
109
|
end
|
|
109
110
|
|
|
110
111
|
# Cut the streaming generation (reason :hook); the turn goes on and asks
|
|
@@ -115,7 +116,7 @@ module Samagotchi
|
|
|
115
116
|
ctrl = @cancel_controller.call
|
|
116
117
|
return false unless ctrl
|
|
117
118
|
|
|
118
|
-
ctrl.cancel_generation!(:hook,
|
|
119
|
+
ctrl.cancel_generation!(:hook, Cut.hook(by: source(hook), reason: reason.to_s))
|
|
119
120
|
end
|
|
120
121
|
|
|
121
122
|
private
|
|
@@ -13,6 +13,7 @@ require_relative "client"
|
|
|
13
13
|
require_relative "llm/errors"
|
|
14
14
|
require_relative "log"
|
|
15
15
|
require_relative "empty_answer_retry"
|
|
16
|
+
require_relative "cut"
|
|
16
17
|
require_relative "cut_policy"
|
|
17
18
|
require_relative "thinking"
|
|
18
19
|
require_relative "hooks"
|
|
@@ -327,8 +328,9 @@ module Samagotchi
|
|
|
327
328
|
# One request: the prompt and its images as sent, the images' token
|
|
328
329
|
# estimate, and the window it was measured against.
|
|
329
330
|
Request = Struct.new(:prompt, :images, :image_tokens, :window, :prefill, keyword_init: true)
|
|
330
|
-
# One generation: the response (nil when cut), the
|
|
331
|
-
#
|
|
331
|
+
# One generation: the response (nil when cut), the Cut that stopped it
|
|
332
|
+
# (an empty one when nothing said why), this generation's own server
|
|
333
|
+
# counts, the model the server named, the
|
|
332
334
|
# thinking it streamed, and why it stopped (llama.cpp's finish
|
|
333
335
|
# reason, Client.finish_reason_from; nil when not named),
|
|
334
336
|
# and its prompt-cache counts (the largest seen: they come cumulative),
|
|
@@ -472,7 +474,7 @@ module Samagotchi
|
|
|
472
474
|
rescue Client::RequestCancelled
|
|
473
475
|
raise unless cut?(generation_controller, turn.cancel_controller)
|
|
474
476
|
|
|
475
|
-
generation.cut = generation_controller.detail ||
|
|
477
|
+
generation.cut = generation_controller.detail || Cut.new
|
|
476
478
|
nil
|
|
477
479
|
end
|
|
478
480
|
generation.cut ? cut_generation(turn, generation, buffer_mark) : completed_generation(turn, request, generation)
|
|
@@ -634,8 +636,8 @@ module Samagotchi
|
|
|
634
636
|
turn.buffer.slice!(buffer_mark..)
|
|
635
637
|
emit(turn, type: :generation_completed, iteration: turn.iteration, content_length: 0,
|
|
636
638
|
thinking_chars: generation.streamed_thinking, served_model: generation.served_model,
|
|
637
|
-
requested_model: turn.model_name, finish_reason: "stopped", stopped_by: generation.cut
|
|
638
|
-
stop_reason: generation.cut
|
|
639
|
+
requested_model: turn.model_name, finish_reason: "stopped", stopped_by: generation.cut.by,
|
|
640
|
+
stop_reason: generation.cut.reason)
|
|
639
641
|
end
|
|
640
642
|
|
|
641
643
|
def completed_generation(turn, request, generation)
|
|
@@ -668,7 +670,7 @@ module Samagotchi
|
|
|
668
670
|
raise Client::RequestCancelled, turn.cancel_controller.reason if outcome.stopped?
|
|
669
671
|
return :next if outcome.again?
|
|
670
672
|
|
|
671
|
-
emit(turn, type: :generation_cancelled, iteration: turn.iteration, reason: :hook, stopped_by: cut
|
|
673
|
+
emit(turn, type: :generation_cancelled, iteration: turn.iteration, reason: :hook, stopped_by: cut.by)
|
|
672
674
|
cancelled_result(turn.conversation, tool_activity: turn.tool_activity, reason: :hook, partial_assistant_text: "")
|
|
673
675
|
end
|
|
674
676
|
|
|
@@ -17,8 +17,11 @@ module Samagotchi
|
|
|
17
17
|
# into a kept one.
|
|
18
18
|
SCRATCH_LEFTOVER = "that's a leftover scratch session; it is deleted at the next sweep (chi sessions clean)"
|
|
19
19
|
# A worker spawned or resumed for an attach whose Bridge never came (%s:
|
|
20
|
-
# the session id).
|
|
21
|
-
|
|
20
|
+
# the session id). A worker whose chi home couldn't load chi logs
|
|
21
|
+
# SessionManager::HOME_BOOT_FAILED and exits: HomeDoctor's boot row says
|
|
22
|
+
# what and what to run, so the text names chi home doctor.
|
|
23
|
+
BRIDGE_LATE = "the worker for session %s did not start its Bridge in time " \
|
|
24
|
+
"(chi home doctor shows a worker that failed to boot)"
|
|
22
25
|
# An attach (or a resume) that found the session's worker leaving, its
|
|
23
26
|
# Bridge closed and its recap on leave still being written: it waits for
|
|
24
27
|
# the worker to let go (OwnerLock.await_release), then starts a new one.
|
|
@@ -14,6 +14,7 @@ require_relative "stream_drops"
|
|
|
14
14
|
require_relative "../kernel_loop"
|
|
15
15
|
require_relative "../answer_display"
|
|
16
16
|
require_relative "../answer_refs"
|
|
17
|
+
require_relative "../cut"
|
|
17
18
|
require_relative "../cut_policy"
|
|
18
19
|
require_relative "../context_status"
|
|
19
20
|
require_relative "../context_window"
|
|
@@ -763,7 +764,7 @@ module Samagotchi
|
|
|
763
764
|
rescue RequestCancelled
|
|
764
765
|
raise unless generation_controller&.cancelled? && !@cancel_controller.cancelled?
|
|
765
766
|
|
|
766
|
-
return cut_response(iteration, thought, generation_controller.detail ||
|
|
767
|
+
return cut_response(iteration, thought, generation_controller.detail || Cut.new)
|
|
767
768
|
end
|
|
768
769
|
dropped = settle_thinking_drop(iteration, response.thinking_drop)
|
|
769
770
|
record_context_status(response.usage, window)
|
|
@@ -820,7 +821,7 @@ module Samagotchi
|
|
|
820
821
|
# stream never sent its last chunk).
|
|
821
822
|
def cut_response(iteration, thought, cut)
|
|
822
823
|
emit(type: :generation_completed, iteration: iteration, content_length: 0, thinking_chars: thought.length,
|
|
823
|
-
requested_model: @model_name, finish_reason: "stopped", stopped_by: cut
|
|
824
|
+
requested_model: @model_name, finish_reason: "stopped", stopped_by: cut.by, stop_reason: cut.reason)
|
|
824
825
|
response = ChatResponse.new(text: "", reasoning: thought, tool_calls: [], usage: nil, finish_reason: "stopped",
|
|
825
826
|
cut: cut)
|
|
826
827
|
[response, nil]
|
|
@@ -26,7 +26,7 @@ module Samagotchi
|
|
|
26
26
|
# +provider+ is the provider that served it (the body's `provider`,
|
|
27
27
|
# OpenRouter's routing name); nil when the server says nothing.
|
|
28
28
|
# +cut+ is set on the empty response the chat loop makes for a
|
|
29
|
-
# generation a plugin cut (stop_generation):
|
|
29
|
+
# generation a plugin cut (stop_generation): the Cut.
|
|
30
30
|
# +top_logprobs+: the first answer token's likeliest alternatives
|
|
31
31
|
# (TokenLogprob) when the request asked for logprobs and the server
|
|
32
32
|
# sent them (a non-streamed answer only); [] otherwise.
|
|
@@ -12,6 +12,16 @@ require_relative "../model_overlay"
|
|
|
12
12
|
module Samagotchi
|
|
13
13
|
module MemoryBundle
|
|
14
14
|
module Status
|
|
15
|
+
# A bundle as this chi reads it now (what chi bundle status prints):
|
|
16
|
+
# its stored record, the scope it resolves to (scope_error when it
|
|
17
|
+
# can't), the folder its files belong in, what each file looks like on
|
|
18
|
+
# disk, its installed plugin, why none of its hooks load, and its needs
|
|
19
|
+
# with what this process's PATH found. The +files+ entries and +plugin+
|
|
20
|
+
# stay Hashes: their fields differ per case (an unchecked file has no
|
|
21
|
+
# paths at all).
|
|
22
|
+
Report = Data.define(:provenance, :scope, :scope_error, :target_dir, :files, :plugin,
|
|
23
|
+
:hooks_requires_failure, :needs)
|
|
24
|
+
|
|
15
25
|
# A scope this chi can't resolve (a newer chi's, a hand edit) isn't
|
|
16
26
|
# guessed: scope_error says so, target_dir is nil and each file is
|
|
17
27
|
# unchecked (no disk reads, nothing counted as missing).
|
|
@@ -50,9 +60,9 @@ module Samagotchi
|
|
|
50
60
|
base_path: base_path
|
|
51
61
|
}
|
|
52
62
|
end
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
63
|
+
Report.new(provenance: bundle, scope: scope, scope_error: target_dir ? nil : "unknown scope: #{scope}",
|
|
64
|
+
target_dir: target_dir, files: details, plugin: plugin_status(provenance, bundle),
|
|
65
|
+
hooks_requires_failure: hooks_requires_failure(bundle), needs: needs_status(bundle))
|
|
56
66
|
end
|
|
57
67
|
|
|
58
68
|
# Why none of the bundle's hooks load (this chi doesn't meet its
|
|
@@ -50,8 +50,10 @@ module Samagotchi
|
|
|
50
50
|
# Keys the tools' overlay never sets: a `chi` a tool runs (the shim's)
|
|
51
51
|
# reads its config, state and log dirs, its settings and the machine its
|
|
52
52
|
# session commands go to (CHI_REMOTE) from them, and the test-run flags.
|
|
53
|
-
#
|
|
54
|
-
|
|
53
|
+
# And TMPDIR: the guardrails judge a tool's `$TMPDIR/x` and its tmp dirs
|
|
54
|
+
# (Guardrails::PathResolve.tmp_roots) by the worker's own TMPDIR, so the
|
|
55
|
+
# tools must run on that one too. Dropped, with one warning.
|
|
56
|
+
RESERVED_KEYS = %w[HOME TMPDIR RACK_ENV CI CHI_REMOTE].freeze
|
|
55
57
|
RESERVED_PREFIXES = %w[XDG_ SAMAGOTCHI_].freeze
|
|
56
58
|
# Never set from an ORIG_ENV record (a tool can write it), only unset:
|
|
57
59
|
# loader and Bundler variables.
|
|
@@ -112,6 +114,18 @@ module Samagotchi
|
|
|
112
114
|
# @return [Hash{String=>String, nil}]
|
|
113
115
|
def tools_env = current.env.except(*TOOLS_DROP)
|
|
114
116
|
|
|
117
|
+
# The whole env change for a command chi starts for the session outside
|
|
118
|
+
# execute (MCP servers, context sources): what execute spawns over
|
|
119
|
+
# (Tools::TaskRuntime.sanitized_spawn_env: chi's own RUBYOPT without
|
|
120
|
+
# bundler/setup, no RUBYLIB or Bundler variables), then #tools_env. One
|
|
121
|
+
# env for every command, so a worker run from a checkout under `bundle
|
|
122
|
+
# exec` hands none of them its Gemfile.
|
|
123
|
+
# @return [Hash{String=>String, nil}]
|
|
124
|
+
def spawn_env
|
|
125
|
+
require_relative "tools/task_runtime" # lazily: bin/chi loads this file for #take_orig!
|
|
126
|
+
Tools::TaskRuntime.sanitized_spawn_env.merge(tools_env)
|
|
127
|
+
end
|
|
128
|
+
|
|
115
129
|
# Specs: back to NONE.
|
|
116
130
|
def reset! = @current = NONE
|
|
117
131
|
|
data/lib/samagotchi/steer_cut.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "config"
|
|
4
|
+
require_relative "cut"
|
|
4
5
|
require_relative "log"
|
|
5
6
|
require_relative "steer"
|
|
6
7
|
require_relative "generation_phase"
|
|
@@ -131,7 +132,7 @@ module Samagotchi
|
|
|
131
132
|
|
|
132
133
|
def cut!(ctrl, source, waited: false)
|
|
133
134
|
age = @phase.age
|
|
134
|
-
cut = ctrl.cancel_generation!(:steer,
|
|
135
|
+
cut = ctrl.cancel_generation!(:steer, Cut.steer(source))
|
|
135
136
|
if cut
|
|
136
137
|
@waiting.clear!
|
|
137
138
|
fields = { source: source, age: age&.round(1) }
|
|
@@ -103,7 +103,9 @@ module Samagotchi
|
|
|
103
103
|
"Samagotchi::TokenUsage::CHARS_PER_TOKEN" => "estimates a tool's tokens: could be a stable constant",
|
|
104
104
|
'require "samagotchi/tool_declarations"' => "loaded for Api.tool_spec's schemas",
|
|
105
105
|
"Samagotchi::ProjectEnv" => "checks the worker has a project env (an older chi doesn't)",
|
|
106
|
-
"Samagotchi::ProjectEnv.current" => "starts a server in the session's project env: needs a ctx tools-env API"
|
|
106
|
+
"Samagotchi::ProjectEnv.current" => "starts a server in the session's project env: needs a ctx tools-env API",
|
|
107
|
+
"Samagotchi::ProjectEnv.respond_to?" => "checks the worker has ProjectEnv.spawn_env (an older chi doesn't)",
|
|
108
|
+
"Samagotchi::ProjectEnv.spawn_env" => "starts a server in the session's project env: needs a ctx tools-env API"
|
|
107
109
|
},
|
|
108
110
|
"slow-commands" => {
|
|
109
111
|
'require "samagotchi/guardrails/shell_lex"' => "for ShellLex",
|
data/lib/samagotchi/turn_note.rb
CHANGED
|
@@ -98,8 +98,8 @@ module Samagotchi
|
|
|
98
98
|
# before a tool call, so the note says "reply"
|
|
99
99
|
# @param running_tasks [Array<Hash>] {id:, command:} of this session's
|
|
100
100
|
# background tasks still running: a Stop doesn't end them
|
|
101
|
-
# @param stopped_by [
|
|
102
|
-
#
|
|
101
|
+
# @param stopped_by [Cut, nil] the Cut of a hook that stopped it
|
|
102
|
+
# (stop_turn, or a cut with no retry left)
|
|
103
103
|
def cancelled(reason, seconds: nil, shown: false, running_tasks: [], stopped_by: nil)
|
|
104
104
|
why = reason.to_s.empty? ? "" : " (#{reason.to_s.tr("_", "-")}#{stopper(stopped_by)})"
|
|
105
105
|
after = seconds ? " after #{seconds.round}s" : ""
|
|
@@ -107,12 +107,12 @@ module Samagotchi
|
|
|
107
107
|
message("the previous turn was cancelled#{why}#{after}; #{what}#{still_running(running_tasks)}")
|
|
108
108
|
end
|
|
109
109
|
|
|
110
|
-
# " <by>: <reason>" of
|
|
110
|
+
# " <by>: <reason>" of the hook whose Cut stopped the turn, as far as known.
|
|
111
111
|
def stopper(stopped_by)
|
|
112
|
-
return ""
|
|
112
|
+
return "" if stopped_by.nil?
|
|
113
113
|
|
|
114
|
-
by = one_line(stopped_by
|
|
115
|
-
reason = one_line(stopped_by
|
|
114
|
+
by = one_line(stopped_by.by)
|
|
115
|
+
reason = one_line(stopped_by.reason)
|
|
116
116
|
[by.empty? ? nil : " #{by}", reason.empty? ? nil : ": #{reason}"].compact.join
|
|
117
117
|
end
|
|
118
118
|
|
data/lib/samagotchi/version.rb
CHANGED
|
@@ -822,7 +822,9 @@ function claimOpts() {
|
|
|
822
822
|
// active (it opens in the browser their review link was opened in: one
|
|
823
823
|
// link per browser, the cookie), else this page's own.
|
|
824
824
|
function claimLinkFor(source, range) {
|
|
825
|
-
|
|
825
|
+
// Not in a Revise stack's switched copy (revise_switch.js: view only, no claim key; Verify isn't offered there either).
|
|
826
|
+
if (!store.selected || !["answer", "step", "user"].includes(source.kind) || !historyEl.contains(source.root)
|
|
827
|
+
|| source.root.closest("[data-stack-view]")) return null;
|
|
826
828
|
const turnId = turnIdOf(historyEl, source.root);
|
|
827
829
|
if (!turnId) return null;
|
|
828
830
|
const claim = { turnId, ...selectionClaim(source.root, range, claimOpts()) };
|
|
@@ -31,7 +31,7 @@ body.has-session #composer{flex-wrap:wrap;row-gap:6px}
|
|
|
31
31
|
.revise-chip.refused{border-color:var(--warn-border);background:var(--warn-bg);color:var(--warn-text)}
|
|
32
32
|
.revise-chip-text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
33
33
|
.revise-chip-clear{flex:none;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;min-height:0;padding:0;border:0;border-radius:999px;background:transparent;color:var(--text-muted);font-size:11px;cursor:pointer}
|
|
34
|
-
.revise-chip-clear:hover{background:var(--bg-raised);color:var(--text-bright)
|
|
34
|
+
.revise-chip-clear:hover{filter:none}@media(hover:hover){.revise-chip-clear:hover{background:var(--bg-raised);color:var(--text-bright)}}
|
|
35
35
|
.chip{display:inline-flex;align-items:center;gap:6px;background:var(--accent-bg);border:1px solid var(--border-strong);border-radius:8px;padding:3px 4px 3px 3px;font-size:12px;max-width:220px}
|
|
36
36
|
.chip img{width:32px;height:32px;object-fit:cover;border-radius:5px}
|
|
37
37
|
.chip .chip-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
@@ -61,7 +61,7 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
|
|
|
61
61
|
/* Folded: the bubble hidden, an answer or a delegate report (#history's: outranks .bubble.user's display). */
|
|
62
62
|
#history>.bubble.folded{display:none}
|
|
63
63
|
.answer-fold{display:flex;align-items:baseline;gap:0;align-self:stretch;max-width:100%;min-width:0;margin:0;padding:12px 14px;font:inherit;font-size:13px;font-weight:600;line-height:1.5;color:var(--text-muted);text-align:left;cursor:pointer;user-select:none;background:var(--bg-panel);border:1px solid var(--border);border-left:3px solid var(--accent-strong);border-radius:12px}
|
|
64
|
-
.answer-fold:hover .answer-fold-size{color:var(--text-2)}
|
|
64
|
+
@media(hover:hover){.answer-fold:hover .answer-fold-size{color:var(--text-2)}}
|
|
65
65
|
.answer-fold:focus-visible{outline:2px solid var(--accent-ring);outline-offset:1px}
|
|
66
66
|
.answer-fold::before{content:"▾\00a0";color:var(--accent);flex:none}
|
|
67
67
|
.answer-fold.folded::before{content:"▸\00a0"}
|
|
@@ -92,7 +92,7 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
|
|
|
92
92
|
.revise-row.stacked{height:auto;margin-top:-6px;justify-content:flex-start}
|
|
93
93
|
.revise-btn{min-height:0;height:22px;padding:0 8px;border-radius:6px;font-size:11px;line-height:20px;background:var(--bg-raised);border:1px solid var(--border-strong);color:var(--text-muted);cursor:pointer;opacity:0;transition:opacity .12s ease,color .12s ease}
|
|
94
94
|
.revise-row:not(.stacked) .revise-btn{position:absolute;right:14px;top:-10px;z-index:1;height:20px;line-height:18px}
|
|
95
|
-
.revise-btn:hover:not(:disabled){color:var(--text-bright)
|
|
95
|
+
.revise-btn:hover:not(:disabled){filter:none}@media(hover:hover){.revise-btn:hover:not(:disabled){color:var(--text-bright)}}
|
|
96
96
|
.revise-btn:disabled{cursor:default}
|
|
97
97
|
.revise-btn.armed,.revise-btn.armed:hover:not(:disabled){opacity:1;color:var(--on-solid);background:var(--accent-strong);border-color:var(--accent-strong)}
|
|
98
98
|
.bubble.output:hover+.revise-row .revise-btn,.revise-row:hover .revise-btn,.revise-row.stacked .revise-btn,.revise-btn:focus-visible{opacity:1}
|
|
@@ -110,7 +110,7 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
|
|
|
110
110
|
/* The ‹ N/M › switcher (revise_switch.js): an earlier version's answer read in the top one's place; its copy dashed, the top hidden. */
|
|
111
111
|
.stack-switch{display:inline-flex;align-items:center;gap:2px;font-size:12px;color:var(--text-2);font-variant-numeric:tabular-nums}
|
|
112
112
|
.stack-switch-btn{min-height:0;height:22px;min-width:24px;padding:0 6px;border-radius:6px;border:1px solid var(--border-strong);background:var(--bg-raised);color:var(--text-2);font:inherit;font-size:14px;line-height:20px;cursor:pointer}
|
|
113
|
-
.stack-switch-btn:hover:not([aria-disabled="true"]){color:var(--text-bright)
|
|
113
|
+
.stack-switch-btn:hover:not([aria-disabled="true"]){filter:none}@media(hover:hover){.stack-switch-btn:hover:not([aria-disabled="true"]){color:var(--text-bright)}}
|
|
114
114
|
.stack-switch-btn:focus-visible{outline:2px solid var(--accent-ring);outline-offset:1px}
|
|
115
115
|
.stack-switch-btn[aria-disabled="true"]{opacity:.4;cursor:default}
|
|
116
116
|
.stack-switch-n{min-width:2.6em;text-align:center}
|
|
@@ -277,12 +277,12 @@ button.system-one-wrong,.system-one-wrong.marked{flex:none;padding:1px 6px;borde
|
|
|
277
277
|
/* A reloaded row's ✂ mark (an LLM context edit on its output), one line above the output. */
|
|
278
278
|
.activity-edit{flex:1 1 100%;min-width:0;margin:2px 0 0 var(--row-gutter);font-size:12px;color:var(--text-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:help}
|
|
279
279
|
.activity-diff>summary{cursor:pointer;font-size:12px;color:var(--text-muted);width:max-content;font-variant-numeric:tabular-nums}
|
|
280
|
-
.activity-diff>summary:hover{color:var(--text-2)}
|
|
280
|
+
@media(hover:hover){.activity-diff>summary:hover{color:var(--text-2)}}
|
|
281
281
|
.activity-diff[open]>summary{margin-bottom:4px}
|
|
282
282
|
/* A read of the file as-is: its numbered lines, closed under the preview (read_view.js); open, they stand for it. */
|
|
283
283
|
.activity-read{flex:1 1 100%;min-width:0;margin:2px 0 2px var(--row-gutter)}
|
|
284
284
|
.activity-read>summary{cursor:pointer;font-size:12px;color:var(--text-muted);width:max-content;font-variant-numeric:tabular-nums}
|
|
285
|
-
.activity-read>summary:hover{color:var(--text-2)}
|
|
285
|
+
@media(hover:hover){.activity-read>summary:hover{color:var(--text-2)}}
|
|
286
286
|
.activity-read[open]>summary{margin-bottom:4px}
|
|
287
287
|
.activity-row:has(>.activity-read[open])>.activity-output{display:none}
|
|
288
288
|
.read-lines{margin:0;padding:4px 0;background:var(--bg-inset);border:1px solid var(--border);border-radius:6px;font-size:12px;line-height:1.45;overflow:auto;max-height:420px}
|
|
@@ -305,7 +305,7 @@ button.system-one-wrong,.system-one-wrong.marked{flex:none;padding:1px 6px;borde
|
|
|
305
305
|
.has-steps .activity-command-cwd{flex:1 1 auto;min-width:0;margin:0;overflow-wrap:anywhere}
|
|
306
306
|
.activity-command-cwd>.activity-command-sep{font-family:inherit;color:var(--text-faint)}
|
|
307
307
|
.activity-command-raw{flex:none;display:inline-flex;align-items:center;height:18px;padding:0 7px;border:1px solid var(--border-strong);border-radius:9px;font-size:11px;color:var(--text-muted);cursor:pointer;user-select:none;-webkit-user-select:none}
|
|
308
|
-
.activity-command-raw:hover{color:var(--text-2)}
|
|
308
|
+
@media(hover:hover){.activity-command-raw:hover{color:var(--text-2)}}
|
|
309
309
|
.activity-command-raw input{position:absolute;opacity:0;width:0;height:0;margin:0}
|
|
310
310
|
.activity-command-raw:has(input:focus-visible){outline:2px solid var(--accent);outline-offset:1px}
|
|
311
311
|
.activity-command-raw:has(input:checked){color:var(--accent);border-color:var(--accent-border);background:var(--accent-tint)}
|
|
@@ -618,7 +618,7 @@ button.system-one-wrong,.system-one-wrong.marked{flex:none;padding:1px 6px;borde
|
|
|
618
618
|
.bubble.output,.bubble.user{position:relative}
|
|
619
619
|
.copy-btn{position:absolute;top:6px;right:6px;z-index:1;display:inline-flex;align-items:center;justify-content:center;height:24px;min-width:24px;padding:0 5px;border-radius:6px;background:var(--bg-raised);border:1px solid var(--border-strong);color:var(--text-muted);font-size:11px;line-height:1;white-space:nowrap;user-select:none;-webkit-user-select:none;opacity:0;transition:opacity .12s ease,color .12s ease}
|
|
620
620
|
.copy-btn svg{display:block}
|
|
621
|
-
.copy-btn:hover:not(:disabled){filter:none
|
|
621
|
+
.copy-btn:hover:not(:disabled){filter:none}@media(hover:hover){.copy-btn:hover:not(:disabled){color:var(--text-bright)}}
|
|
622
622
|
.bubble:hover>.copy-btn,.code-wrap:hover>.copy-btn,.copy-btn:focus-visible,.copy-btn[data-copied]{opacity:1}
|
|
623
623
|
.copy-btn[data-copied],.copy-btn[data-copied]:hover:not(:disabled){color:var(--ok-text)}
|
|
624
624
|
.copy-btn[data-copied] svg{display:none}
|
|
@@ -847,8 +847,9 @@ body.stage-view .turn-stage{position:absolute;left:0;right:0;bottom:100%;margin-
|
|
|
847
847
|
.ts-extras>.bubble.user>.copy-btn{display:none}
|
|
848
848
|
.ts-extras>.bubble.user .user-message{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;overflow:hidden;white-space:normal}
|
|
849
849
|
/* A line's markdown (a delegate report's) reads as a plain row in the clamped rows: its blocks run inline at the
|
|
850
|
-
row's own size, a
|
|
851
|
-
|
|
850
|
+
row's own size, font and baseline (a pre's monospace or a cell's middle alignment would make the line box taller), a
|
|
851
|
+
space between them, rules and copy buttons gone. The prompt line shown whole keeps it. */
|
|
852
|
+
:is(.ts-extras>.bubble.user,.ts-prompt:not(.full)>.bubble.user)>.user-message.markdown :is(p,h1,h2,h3,h4,h5,h6,ul,ol,li,blockquote,pre,table,thead,tbody,tr,th,td,.code-wrap){display:inline;margin:0;padding:0;border:0;border-radius:0;background:transparent;font-family:inherit;font-size:inherit;font-weight:inherit;line-height:inherit;color:inherit;white-space:normal;vertical-align:baseline}
|
|
852
853
|
:is(.ts-extras>.bubble.user,.ts-prompt:not(.full)>.bubble.user)>.user-message.markdown :is(p,h1,h2,h3,h4,h5,h6,li,blockquote,pre,td,th)::after{content:" "}
|
|
853
854
|
:is(.ts-extras>.bubble.user,.ts-prompt:not(.full)>.bubble.user)>.user-message.markdown :is(hr,img,.copy-btn){display:none}
|
|
854
855
|
/* Inside the dock's own blur a second backdrop-filter sees nothing: the cards are opaque here (as #commandList is). */
|
|
@@ -200,7 +200,16 @@ export function createNotifications({
|
|
|
200
200
|
button.hidden = state === "unsupported";
|
|
201
201
|
button.dataset.state = state;
|
|
202
202
|
button.title = (push ? PUSH_TITLES : NOTIFY_TITLES)[state] || "";
|
|
203
|
-
|
|
203
|
+
// In a tab the bell is a toggle (aria-pressed); in the app it opens the
|
|
204
|
+
// notifications sheet, whose shell (chip_popover.js) keeps its
|
|
205
|
+
// aria-expanded, as every popover opener's: one state, never both.
|
|
206
|
+
if (push) {
|
|
207
|
+
button.removeAttribute("aria-pressed");
|
|
208
|
+
button.setAttribute("aria-haspopup", "dialog");
|
|
209
|
+
if (!button.hasAttribute("aria-expanded")) button.setAttribute("aria-expanded", "false");
|
|
210
|
+
} else {
|
|
211
|
+
button.setAttribute("aria-pressed", state === "on" ? "true" : "false");
|
|
212
|
+
}
|
|
204
213
|
}
|
|
205
214
|
|
|
206
215
|
async function toggleNotify() {
|
|
@@ -94,9 +94,10 @@ export function stubText({ version, label = "", lines = 0, top }) {
|
|
|
94
94
|
return `v${version}${said}${size} — superseded by v${top}`;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
// The version chip's words
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
// The version chip's words for version +at+ (0-based) on view of +count+:
|
|
98
|
+
// the newest, "v3 · 2 earlier"; an earlier one the switcher shows, "v1 of 3".
|
|
99
|
+
export function versionChipText(count, at = count - 1) {
|
|
100
|
+
return at >= count - 1 ? `v${count} · ${count - 1} earlier` : `v${at + 1} of ${count}`;
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
// The composer's chip: "Revising: ‹label›", or the refusal (+unavailable+:
|
|
@@ -236,12 +237,12 @@ export function installReviseStacks({
|
|
|
236
237
|
function drawRow(row, { id, answer, chain, openings }) {
|
|
237
238
|
const off = unavailable();
|
|
238
239
|
const isArmed = armed() === id;
|
|
240
|
+
const at = chain ? (shown.get(id) ?? chain.length - 1) : 0;
|
|
239
241
|
const versions = chain
|
|
240
|
-
? html`<details class="stack-versions"><summary>${versionChipText(chain.length)}</summary><div class="stack-versions-list" role="list">${chain.map((vid, k) => html`<button type="button" role="listitem" class=${vid === id ? "stack-version current" : "stack-version"} data-stack-pick=${vid} ?disabled=${vid === id}><span class="stack-version-n">v${k + 1}</span> ${promptLabel(openings.get(vid))}</button>`)}</div></details>`
|
|
242
|
+
? html`<details class="stack-versions"><summary>${versionChipText(chain.length, at)}</summary><div class="stack-versions-list" role="list">${chain.map((vid, k) => html`<button type="button" role="listitem" class=${vid === id ? "stack-version current" : "stack-version"} data-stack-pick=${vid} ?disabled=${vid === id}><span class="stack-version-n">v${k + 1}</span> ${promptLabel(openings.get(vid))}</button>`)}</div></details>`
|
|
241
243
|
: nothing;
|
|
242
244
|
const button = html`<button type="button" class=${isArmed ? "revise-btn armed" : "revise-btn"} data-revise=${id} ?disabled=${off} title=${off ? REVISE_UNAVAILABLE : "Rewrite this answer: your next message revises it, and the earlier version folds away"}>${isArmed ? "Revising…" : "Revise"}</button>`;
|
|
243
245
|
row.classList.toggle("stacked", !!chain);
|
|
244
|
-
const at = chain ? (shown.get(id) ?? chain.length - 1) : 0;
|
|
245
246
|
render(html`${chain ? switcherTemplate(id, at, chain.length) : nothing}${versions}${button}`, row);
|
|
246
247
|
rowAnswer.set(row, answer);
|
|
247
248
|
}
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: samagotchi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.139.
|
|
4
|
+
version: 0.139.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -309,6 +309,7 @@ files:
|
|
|
309
309
|
- lib/samagotchi/context_usage.rb
|
|
310
310
|
- lib/samagotchi/context_window.rb
|
|
311
311
|
- lib/samagotchi/continue_offer.rb
|
|
312
|
+
- lib/samagotchi/cut.rb
|
|
312
313
|
- lib/samagotchi/cut_policy.rb
|
|
313
314
|
- lib/samagotchi/debug_log.rb
|
|
314
315
|
- lib/samagotchi/delivery.rb
|