samagotchi 0.130.0 → 0.131.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +62 -1
- data/docs/architecture.md +1 -1
- data/docs/cli.md +15 -4
- data/docs/configuration.md +4 -1
- data/docs/desktop.md +28 -9
- data/docs/internals/background-tasks.md +1 -0
- data/docs/internals/participants.md +36 -9
- data/docs/internals/prompt-caching.md +5 -2
- data/docs/sessions.md +4 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/cache_confirm.rb +29 -9
- data/lib/samagotchi/desktop/macos.rb +99 -19
- data/lib/samagotchi/desktop_command.rb +7 -2
- data/lib/samagotchi/engine.rb +31 -2
- data/lib/samagotchi/guardrails/rules.rb +2 -2
- data/lib/samagotchi/guardrails/targets.rb +13 -0
- data/lib/samagotchi/level_setting_command.rb +3 -2
- data/lib/samagotchi/session.rb +49 -4
- data/lib/samagotchi/session_commands.rb +4 -4
- data/lib/samagotchi/toolkit_command.rb +2 -2
- data/lib/samagotchi/tools/execute.rb +5 -2
- data/lib/samagotchi/tools/task_runtime.rb +4 -2
- data/lib/samagotchi/update_command.rb +9 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +47 -8
- data/lib/samagotchi/web/participant_review.rb +9 -2
- data/lib/samagotchi/web/participant_view.rb +22 -6
- data/lib/samagotchi/web/proxy_gate.rb +6 -0
- data/lib/samagotchi/web/public/annotate_bar.js +6 -1
- data/lib/samagotchi/web/public/app.js +14 -5
- data/lib/samagotchi/web/public/participant.js +24 -6
- data/lib/samagotchi/web/public/verdict_card.js +48 -4
- data/lib/samagotchi/web/public/verdict_fresh.js +8 -4
- data/lib/samagotchi/web/public/verify_target.js +73 -0
- data/lib/samagotchi/web/token.rb +20 -1
- data/lib/samagotchi/worker.rb +7 -0
- metadata +2 -1
|
@@ -41,9 +41,11 @@ module Samagotchi
|
|
|
41
41
|
BUILD_NAME = ".chi-helper-build"
|
|
42
42
|
OLD_NAME = ".chi-helper-old"
|
|
43
43
|
# Apps started by launchd get a bare env (no shell rc files), so the
|
|
44
|
-
# launch file carries what chi needs to find its gems and
|
|
45
|
-
# these, and only when set: no tokens, no runtime knobs.
|
|
44
|
+
# launch file carries what chi needs to find its gems, state and tools.
|
|
45
|
+
# Only these, and only when set: no tokens, no runtime knobs.
|
|
46
46
|
ENV_ALLOWLIST = %w[XDG_CONFIG_HOME XDG_STATE_HOME GEM_HOME GEM_PATH RUBYLIB].freeze
|
|
47
|
+
# PATH is separately filtered to absolute dirs and frozen when written,
|
|
48
|
+
# so the helper's chi, workers and execute can find the shell's tools.
|
|
47
49
|
# Without it Ruby reads stdin as US-ASCII and non-ASCII notes break.
|
|
48
50
|
LANG = "en_US.UTF-8"
|
|
49
51
|
# chi broadcast's default triage deadline, and the helper's seconds on
|
|
@@ -54,6 +56,17 @@ module Samagotchi
|
|
|
54
56
|
# one launch-file section each, in this order.
|
|
55
57
|
TERMINALS = %w[kitty tmux].freeze
|
|
56
58
|
|
|
59
|
+
# PATH dirs the helper can use from launchd's / working directory:
|
|
60
|
+
# keep absolute entries only, in order, once each.
|
|
61
|
+
def self.path_dirs(env)
|
|
62
|
+
env["PATH"].to_s.split(File::PATH_SEPARATOR).select { _1.start_with?("/") }.uniq
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.baked_path(env)
|
|
66
|
+
dirs = path_dirs(env)
|
|
67
|
+
dirs.empty? ? nil : dirs.join(File::PATH_SEPARATOR)
|
|
68
|
+
end
|
|
69
|
+
|
|
57
70
|
# Runs a command without a shell; [output, success].
|
|
58
71
|
class Runner
|
|
59
72
|
def run(argv)
|
|
@@ -76,7 +89,8 @@ module Samagotchi
|
|
|
76
89
|
|
|
77
90
|
# The tmux: settings, or nil while tmux.enabled is off. The binary is
|
|
78
91
|
# tmux.binary, else the tmux on +env+'s PATH ("" when there is none:
|
|
79
|
-
# see #warnings); the socket
|
|
92
|
+
# only absolute dirs are usable from launchd; see #warnings); the socket
|
|
93
|
+
# dir is where tmux puts `-L` sockets
|
|
80
94
|
# ($TMUX_TMPDIR when it is a directory, else /tmp, then tmux-<uid>).
|
|
81
95
|
# Both are frozen at bake time, like the rest of the launch file.
|
|
82
96
|
def self.tmux_settings(get = Config.method(:get), env: ENV)
|
|
@@ -91,8 +105,7 @@ module Samagotchi
|
|
|
91
105
|
end
|
|
92
106
|
|
|
93
107
|
def self.path_tmux(env)
|
|
94
|
-
env
|
|
95
|
-
.find { File.file?(_1) && File.executable?(_1) }.to_s
|
|
108
|
+
path_dirs(env).map { File.join(_1, "tmux") }.find { File.file?(_1) && File.executable?(_1) }.to_s
|
|
96
109
|
end
|
|
97
110
|
|
|
98
111
|
# Each switched-on terminal's settings by name ("kitty" => …).
|
|
@@ -148,10 +161,14 @@ module Samagotchi
|
|
|
148
161
|
# `gem cleanup`), else this checkout's bin/chi. A section per terminal
|
|
149
162
|
# switched on (TERMINALS order); the broadcast timeout when given.
|
|
150
163
|
# The XDG dirs go in absolute (Paths): launchd starts the app in /,
|
|
151
|
-
# where a relative one would mean /<dir>.
|
|
164
|
+
# where a relative one would mean /<dir>. PATH is filtered to absolute,
|
|
165
|
+
# deduplicated dirs so chi, its workers and execute find shell tools;
|
|
166
|
+
# it is frozen at bake time.
|
|
152
167
|
def launch_config
|
|
153
168
|
env = { "LANG" => LANG }
|
|
154
169
|
ENV_ALLOWLIST.each { |name| env[name] = @env[name] unless @env[name].to_s.empty? }
|
|
170
|
+
path = self.class.baked_path(@env)
|
|
171
|
+
env["PATH"] = path if path
|
|
155
172
|
env.merge!(Paths.absolute_xdg(env: env, set: :key))
|
|
156
173
|
config = { "version" => @version, "argv" => [@ruby, @chi_path], "env" => env, "sources_sha" => sources_sha }
|
|
157
174
|
TERMINALS.each { |name| config[name] = @terminals[name] if @terminals[name] }
|
|
@@ -178,22 +195,55 @@ module Samagotchi
|
|
|
178
195
|
launch["sources_sha"] != sources_sha || !launch_ok?(launch)
|
|
179
196
|
end
|
|
180
197
|
|
|
181
|
-
# The launch file names
|
|
182
|
-
# other terminal settings or
|
|
183
|
-
# fixes that without a rebuild.
|
|
198
|
+
# The launch file names an older chi version than this one (or none), or
|
|
199
|
+
# other terminal settings, broadcast timeout or a missing bakeable PATH:
|
|
200
|
+
# refresh_launch_file fixes that without a rebuild. A different argv or
|
|
201
|
+
# baked PATH, or a newer version, is intentional: the ruby, chi and PATH
|
|
202
|
+
# the helper runs change only with chi desktop install --force or upgrade.
|
|
184
203
|
def launch_outdated?
|
|
185
204
|
launch = read_launch
|
|
186
|
-
launch["version"]
|
|
205
|
+
older_version?(launch["version"]) ||
|
|
187
206
|
TERMINALS.map { launch[_1] } != TERMINALS.map { @terminals[_1] } ||
|
|
188
|
-
launch["broadcast_timeout"] != @broadcast_timeout
|
|
207
|
+
launch["broadcast_timeout"] != @broadcast_timeout || path_missing?(launch)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Why chi update can't keep the launch file's ruby or chi: one of them
|
|
211
|
+
# is gone. nil when it runs, or has no usable argv (a refresh writes
|
|
212
|
+
# this chi's then).
|
|
213
|
+
def launch_problem
|
|
214
|
+
argv = usable_argv(read_launch)
|
|
215
|
+
missing = Array(argv).reject { |path| File.exist?(path) }
|
|
216
|
+
return nil if missing.empty?
|
|
217
|
+
|
|
218
|
+
"chi not found at #{missing.join(", ")}: run chi desktop install --force from the shell whose chi it should run"
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# The chi version the launch file names, nil without one.
|
|
222
|
+
def launch_version = read_launch["version"]
|
|
223
|
+
|
|
224
|
+
# The launch file names a newer chi than this one: an older chi's
|
|
225
|
+
# chi update keeps the helper as it is (no rebuild from older sources).
|
|
226
|
+
def launch_newer?
|
|
227
|
+
Gem::Version.new(launch_version.to_s) > Gem::Version.new(@version)
|
|
228
|
+
rescue ArgumentError
|
|
229
|
+
false
|
|
189
230
|
end
|
|
190
231
|
|
|
191
232
|
# Rewrite the launch file for this chi and its settings, keeping
|
|
192
|
-
# the env it was installed with
|
|
233
|
+
# its argv and the env it was installed with; a PATH only when it has
|
|
234
|
+
# none (changing them takes chi desktop install --force or upgrade). A
|
|
235
|
+
# newer version stays. The app reads it at each send.
|
|
193
236
|
def refresh_launch_file
|
|
194
237
|
config = launch_config
|
|
195
|
-
|
|
196
|
-
config["
|
|
238
|
+
old = read_launch
|
|
239
|
+
config["argv"] = usable_argv(old) || config["argv"]
|
|
240
|
+
config["version"] = old["version"] unless older_version?(old["version"])
|
|
241
|
+
old_env = old["env"]
|
|
242
|
+
if old_env.is_a?(Hash)
|
|
243
|
+
config["env"] = old_env
|
|
244
|
+
path = self.class.baked_path(@env)
|
|
245
|
+
config["env"]["PATH"] = path if config["env"]["PATH"].to_s.empty? && path
|
|
246
|
+
end
|
|
197
247
|
write_launch_file(config)
|
|
198
248
|
end
|
|
199
249
|
|
|
@@ -242,7 +292,9 @@ module Samagotchi
|
|
|
242
292
|
|
|
243
293
|
# @yield [String] progress lines
|
|
244
294
|
# @param login [Boolean] also start the app at login
|
|
245
|
-
|
|
295
|
+
# @param keep_launch [Boolean] keep the launch file's ruby, chi and PATH
|
|
296
|
+
# (chi update's rebuild) instead of baking this shell's
|
|
297
|
+
def install(force: false, login: false, keep_launch: false, &progress)
|
|
246
298
|
raise Error, "already installed (#{app_version}); use chi desktop upgrade" if installed? && !force
|
|
247
299
|
|
|
248
300
|
toolchain!
|
|
@@ -250,7 +302,7 @@ module Samagotchi
|
|
|
250
302
|
progress&.call("building #{APP_NAME} #{@version} (can take a few minutes)…")
|
|
251
303
|
built = build_app
|
|
252
304
|
swap_in(built)
|
|
253
|
-
write_launch_file
|
|
305
|
+
write_launch_file(keep_launch: keep_launch)
|
|
254
306
|
register_app
|
|
255
307
|
self.login = true if login
|
|
256
308
|
progress&.call("installed #{app_path}")
|
|
@@ -258,9 +310,9 @@ module Samagotchi
|
|
|
258
310
|
end
|
|
259
311
|
|
|
260
312
|
# Keeps the login item as it was.
|
|
261
|
-
def upgrade(&)
|
|
313
|
+
def upgrade(keep_launch: false, &)
|
|
262
314
|
was_login = installed? && login == "enabled"
|
|
263
|
-
install(force: true, login: was_login, &)
|
|
315
|
+
install(force: true, login: was_login, keep_launch: keep_launch, &)
|
|
264
316
|
end
|
|
265
317
|
|
|
266
318
|
# SMAppService status as the app reports it ("enabled",
|
|
@@ -313,6 +365,7 @@ module Samagotchi
|
|
|
313
365
|
launch_ok: launch_ok?(launch),
|
|
314
366
|
stale: stale?,
|
|
315
367
|
baked_dirs: env.select { |name, _| name.start_with?("XDG_") },
|
|
368
|
+
baked_path: env["PATH"],
|
|
316
369
|
service: dump.include?(BUNDLE_ID),
|
|
317
370
|
running: running,
|
|
318
371
|
host_keys: host_keys
|
|
@@ -393,12 +446,39 @@ module Samagotchi
|
|
|
393
446
|
{}
|
|
394
447
|
end
|
|
395
448
|
|
|
449
|
+
# This chi is newer than the version the launch file names; a missing or
|
|
450
|
+
# unreadable version counts as older.
|
|
451
|
+
def older_version?(version)
|
|
452
|
+
Gem::Version.new(@version) > Gem::Version.new(version.to_s)
|
|
453
|
+
rescue ArgumentError
|
|
454
|
+
true
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def usable_argv(launch)
|
|
458
|
+
argv = launch["argv"]
|
|
459
|
+
argv if argv.is_a?(Array) && !argv.empty? && argv.all? { |path| path.is_a?(String) && !path.empty? }
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def path_missing?(launch)
|
|
463
|
+
return false unless self.class.baked_path(@env)
|
|
464
|
+
|
|
465
|
+
env = launch["env"]
|
|
466
|
+
!env.is_a?(Hash) || env["PATH"].to_s.empty?
|
|
467
|
+
end
|
|
468
|
+
|
|
396
469
|
def launch_ok?(launch)
|
|
397
470
|
argv = Array(launch["argv"])
|
|
398
471
|
!argv.empty? && argv.all? { |path| File.exist?(path) }
|
|
399
472
|
end
|
|
400
473
|
|
|
401
|
-
def write_launch_file(config = launch_config)
|
|
474
|
+
def write_launch_file(config = launch_config, keep_launch: false)
|
|
475
|
+
if keep_launch
|
|
476
|
+
old = read_launch
|
|
477
|
+
config["argv"] = usable_argv(old) || config["argv"]
|
|
478
|
+
old_env = old["env"]
|
|
479
|
+
old_path = old_env["PATH"] if old_env.is_a?(Hash)
|
|
480
|
+
config["env"]["PATH"] = old_path unless old_path.to_s.empty?
|
|
481
|
+
end
|
|
402
482
|
FileUtils.mkdir_p(@support_dir)
|
|
403
483
|
AtomicFile.write(launch_path, JSON.pretty_generate(config) + "\n")
|
|
404
484
|
end
|
|
@@ -118,11 +118,12 @@ module Samagotchi
|
|
|
118
118
|
"#{s[:app_version] || "?"} (up to date: chi #{s[:chi_version]} changed nothing in it)"
|
|
119
119
|
end
|
|
120
120
|
launch = s[:launch_argv].empty? ? "(none)" : s[:launch_argv].join(" ")
|
|
121
|
-
launch += s[:launch_ok] ? " (ok)" : " (missing: chi
|
|
121
|
+
launch += s[:launch_ok] ? " (ok)" : " (missing: chi desktop install --force)"
|
|
122
122
|
dirs = s[:baked_dirs].empty? ? "(defaults)" : s[:baked_dirs].map { |name, value| "#{name}=#{value}" }.join(" ")
|
|
123
|
+
path = path_row(s[:baked_path])
|
|
123
124
|
service = s[:service] ? "registered" : "not registered (try chi desktop upgrade, or log out and back in)"
|
|
124
125
|
running = s[:running] ? "running" : "not running (open -g \"#{s[:app_path]}\")"
|
|
125
|
-
rows = [["Chi Helper", version], ["app", s[:app_path]], ["runs chi", launch], ["state dirs", dirs],
|
|
126
|
+
rows = [["Chi Helper", version], ["app", s[:app_path]], ["runs chi", launch], ["state dirs", dirs], ["PATH", path],
|
|
126
127
|
["service", service], ["hotkey", hotkey(s[:hotkey])], ["process", running], ["at login", login(s[:login])]]
|
|
127
128
|
rows << ["host keys", host_keys(s[:host_keys])] unless Array(s[:host_keys]).empty?
|
|
128
129
|
@stdout.puts(format_rows(rows))
|
|
@@ -153,6 +154,10 @@ module Samagotchi
|
|
|
153
154
|
end
|
|
154
155
|
end
|
|
155
156
|
|
|
157
|
+
def path_row(path)
|
|
158
|
+
path.to_s.empty? ? "(none: macOS's default; chi update fills it, chi desktop install --force changes it)" : path
|
|
159
|
+
end
|
|
160
|
+
|
|
156
161
|
def format_rows(rows)
|
|
157
162
|
width = rows.map { |label, _| label.length }.max
|
|
158
163
|
rows.map { |label, value| "#{label.ljust(width)} #{value}" }.join("\n")
|
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -16,6 +16,7 @@ require_relative "turn_note"
|
|
|
16
16
|
require_relative "turn_endings"
|
|
17
17
|
require_relative "turn_origin"
|
|
18
18
|
require_relative "verify_anchor"
|
|
19
|
+
require_relative "web/participant_view"
|
|
19
20
|
require_relative "verdict"
|
|
20
21
|
require_relative "model_profile"
|
|
21
22
|
require_relative "effective_model"
|
|
@@ -1352,6 +1353,19 @@ module Samagotchi
|
|
|
1352
1353
|
Array(@session&.messages).any? { |entry| entry[:role].to_s == "model" }
|
|
1353
1354
|
end
|
|
1354
1355
|
|
|
1356
|
+
# The latest known context size used by a request, for a cache-restart
|
|
1357
|
+
# confirmation; nil before any request reports a count.
|
|
1358
|
+
# @return [Integer, nil]
|
|
1359
|
+
def cache_restart_tokens
|
|
1360
|
+
used = @metrics.snapshot.dig(:context, :used_tokens)
|
|
1361
|
+
used if used.is_a?(Integer)
|
|
1362
|
+
end
|
|
1363
|
+
|
|
1364
|
+
# Whether a cache restart can invalidate preserved thinking blocks.
|
|
1365
|
+
def cache_restart_anthropic?
|
|
1366
|
+
effective_model.entry&.api.to_s == "anthropic"
|
|
1367
|
+
end
|
|
1368
|
+
|
|
1355
1369
|
# What a thinking level change costs the effective model's prompt
|
|
1356
1370
|
# cache (Thinking.cache_cost): :full, :provider or :tail.
|
|
1357
1371
|
# @return [Symbol]
|
|
@@ -1646,13 +1660,28 @@ module Samagotchi
|
|
|
1646
1660
|
return reject_verify_anchor(anchor, :reply_missing) unless reply
|
|
1647
1661
|
|
|
1648
1662
|
quote_text = verify_comparison_text(anchor.quote)
|
|
1649
|
-
|
|
1650
|
-
return reject_verify_anchor(anchor, :quote_missing)
|
|
1663
|
+
quoted = !quote_text.empty? && verify_quote_in?(quote_text, (reply[:content] || reply["content"]).to_s, source)
|
|
1664
|
+
return reject_verify_anchor(anchor, :quote_missing) unless quoted
|
|
1651
1665
|
|
|
1652
1666
|
anchor
|
|
1653
1667
|
end
|
|
1654
1668
|
private :verified_anchor
|
|
1655
1669
|
|
|
1670
|
+
# Whether +quote_text+ is in +answer+ as the model wrote it, or as the
|
|
1671
|
+
# pages show it: a page quotes its own text, which reads the session's
|
|
1672
|
+
# folder relative, the home folder as ~ and a secret shape as
|
|
1673
|
+
# [redacted] (a participant's page gets it so, ParticipantView::Redact;
|
|
1674
|
+
# the owner's page rewrites its quote the same way, claim_link.js
|
|
1675
|
+
# normalizeClaim, so claim links match both pages). +source+ is the
|
|
1676
|
+
# session whose folder that is.
|
|
1677
|
+
def verify_quote_in?(quote_text, answer, source)
|
|
1678
|
+
return true if verify_comparison_text(answer).include?(quote_text)
|
|
1679
|
+
|
|
1680
|
+
viewer = Web::ParticipantView::Viewer.new(cwd: source.working_directory)
|
|
1681
|
+
verify_comparison_text(Web::ParticipantView::Redact.text(answer, viewer)).include?(quote_text)
|
|
1682
|
+
end
|
|
1683
|
+
private :verify_quote_in?
|
|
1684
|
+
|
|
1656
1685
|
def verify_anchor_source_session(session, anchor)
|
|
1657
1686
|
return session unless session.verify_fresh
|
|
1658
1687
|
|
|
@@ -112,8 +112,8 @@ module Samagotchi
|
|
|
112
112
|
# A resolved match only adds an ask or a deny; were a verdict
|
|
113
113
|
# ever to let a call through, a symlink into its glob must not.
|
|
114
114
|
if resolved_match?
|
|
115
|
-
real_base ||=
|
|
116
|
-
forms +=
|
|
115
|
+
real_base ||= targets.real_rule_base
|
|
116
|
+
forms += targets.real_forms(p).map { |real| [real, real_base] }
|
|
117
117
|
end
|
|
118
118
|
forms.any? do |form, from|
|
|
119
119
|
candidate = absolute ? form : relative(form, from)
|
|
@@ -379,6 +379,19 @@ module Samagotchi
|
|
|
379
379
|
@paths.any? { |p| Outside.outside?(p, root: @session_root) }
|
|
380
380
|
end
|
|
381
381
|
|
|
382
|
+
# The resolved forms used by rules' path: globs, once per target path.
|
|
383
|
+
def real_forms(path)
|
|
384
|
+
@real_forms ||= {}
|
|
385
|
+
@real_forms[path] ||= PathResolve.reals(path)
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# The resolved base used by rules' path: globs.
|
|
389
|
+
def real_rule_base
|
|
390
|
+
return @real_rule_base if defined?(@real_rule_base)
|
|
391
|
+
|
|
392
|
+
@real_rule_base = PathResolve.real(@repo_root || @cwd)
|
|
393
|
+
end
|
|
394
|
+
|
|
382
395
|
# Where a shell call runs mutating git (ShellGitDirs: dirs, and
|
|
383
396
|
# :unknown for one the text doesn't tell); [] for other tools.
|
|
384
397
|
def git_dirs
|
|
@@ -81,9 +81,10 @@ module Samagotchi
|
|
|
81
81
|
return nil unless before && would
|
|
82
82
|
|
|
83
83
|
cost = spec.cache_cost.call(@engine, before.level, would.level)
|
|
84
|
-
|
|
84
|
+
step = CacheConfirm.ask(line: "#{spec.name} #{word}", cost: cost, changed: would.level != before.level,
|
|
85
|
+
engine: @engine)
|
|
86
|
+
return nil unless step
|
|
85
87
|
|
|
86
|
-
step = CacheConfirm.confirm(spec.name, word)
|
|
87
88
|
[[listing(before), "Switching to #{would.level} restarts the prompt cache: #{spec.cost_notes[cost]}", step.text].join("\n"),
|
|
88
89
|
false, step]
|
|
89
90
|
end
|
data/lib/samagotchi/session.rb
CHANGED
|
@@ -14,6 +14,7 @@ require_relative "session_setup"
|
|
|
14
14
|
require_relative "prompt_note"
|
|
15
15
|
require_relative "working_set_pin"
|
|
16
16
|
require_relative "session_review"
|
|
17
|
+
require_relative "version"
|
|
17
18
|
|
|
18
19
|
require_relative "paths"
|
|
19
20
|
require_relative "project_scope"
|
|
@@ -351,17 +352,27 @@ module Samagotchi
|
|
|
351
352
|
# saved.
|
|
352
353
|
attr_accessor :opener
|
|
353
354
|
|
|
355
|
+
# The top-level keys of the session file this chi doesn't know (a newer
|
|
356
|
+
# chi's fields), frozen, string keys: #to_h writes them back after the
|
|
357
|
+
# FIELDS as they were. Never read by this chi.
|
|
358
|
+
attr_reader :extra_fields
|
|
359
|
+
|
|
360
|
+
# The chi version that last saved the loaded file (#to_h writes this
|
|
361
|
+
# chi's VERSION); nil for a file from 0.130.0 or older.
|
|
362
|
+
attr_reader :written_by
|
|
363
|
+
|
|
354
364
|
def initialize(id:, mode:, model_name:, working_directory:, messages:, created_at:, updated_at:,
|
|
355
|
-
metadata_version: METADATA_VERSION, status: STATUS_IDLE, last_prompt: "",
|
|
365
|
+
metadata_version: METADATA_VERSION, written_by: nil, status: STATUS_IDLE, last_prompt: "",
|
|
356
366
|
first_preview: "", first_turn_id: nil, test_run: false, pending_question: nil,
|
|
357
367
|
used_memory_names: [], project_root: nil,
|
|
358
368
|
preloaded_memory_names: [], muted_memory_names: [], parent_id: nil, scratch: false,
|
|
359
369
|
last_turn: nil, model_typed: nil, delegate: false, llm_context: nil, prompt_notes: [],
|
|
360
370
|
continues: nil, thinking: nil, reasoning_history: nil, appended_prompt_files: [],
|
|
361
371
|
toolkit: nil, toolkit_origin: "chosen", settings: nil, interrupted_turn: nil, parent_answers: nil, title: nil,
|
|
362
|
-
working_set_pins: [], review: nil, verdicts: nil, verify_fresh: nil)
|
|
372
|
+
working_set_pins: [], review: nil, verdicts: nil, verify_fresh: nil, extra_fields: {})
|
|
363
373
|
@id = id
|
|
364
374
|
@metadata_version = metadata_version
|
|
375
|
+
@written_by = written_by
|
|
365
376
|
@mode = mode
|
|
366
377
|
@model_name = model_name
|
|
367
378
|
@model_typed = model_typed
|
|
@@ -399,12 +410,25 @@ module Samagotchi
|
|
|
399
410
|
self.verdicts = verdicts
|
|
400
411
|
self.verify_fresh = verify_fresh
|
|
401
412
|
self.interrupted_turn = interrupted_turn
|
|
413
|
+
@extra_fields = extra_fields.to_h { |key, value| [key.to_s, value] }.freeze
|
|
402
414
|
@turn_checkpoint = nil
|
|
403
415
|
@archived = false
|
|
404
416
|
# #save's lock: one snapshot-and-write at a time (see #save).
|
|
405
417
|
@save_lock = Mutex.new
|
|
406
418
|
end
|
|
407
419
|
|
|
420
|
+
# Whether a chi newer than this one saved the loaded file: its
|
|
421
|
+
# written_by is a later version, or it has keys this chi doesn't know
|
|
422
|
+
# (a hand-edited or future file). The worker logs it (newer_writer).
|
|
423
|
+
def newer_writer?
|
|
424
|
+
return true unless @extra_fields.empty?
|
|
425
|
+
|
|
426
|
+
# correct?(nil) is true (a 0 version): a file without the field is no newer.
|
|
427
|
+
return false unless @written_by.is_a?(String) && Gem::Version.correct?(@written_by)
|
|
428
|
+
|
|
429
|
+
Gem::Version.new(@written_by) > Gem::Version.new(VERSION)
|
|
430
|
+
end
|
|
431
|
+
|
|
408
432
|
# A `delegate` child: its turns report to its parent (ChildReports). A
|
|
409
433
|
# child saved before the field is known by the memory every delegate
|
|
410
434
|
# starts with; a fork is neither.
|
|
@@ -543,8 +567,24 @@ module Samagotchi
|
|
|
543
567
|
# the way (symbols in memory, strings on disk); last_turn, prompt_notes
|
|
544
568
|
# and pending_question are value objects in memory (LastTurn,
|
|
545
569
|
# PromptNote, PendingQuestion).
|
|
570
|
+
#
|
|
571
|
+
# A new field: a chi older than it (from the release after 0.130.0)
|
|
572
|
+
# keeps the key as it was (#extra_fields) but doesn't honour it, and the
|
|
573
|
+
# next newer chi reads it again. So it must be safe to come back
|
|
574
|
+
# unchanged after an older chi ran turns on the session: static data, or
|
|
575
|
+
# append-only data the older chi never appends to (review, verify_fresh,
|
|
576
|
+
# first_turn_id, verdicts); a cursor into messages, which an older chi
|
|
577
|
+
# would have moved, needs a check against what it points at. Only
|
|
578
|
+
# top-level keys are kept: the value objects (SessionReview,
|
|
579
|
+
# VerifyFreshOrigin, Toolkit, LastTurn, PendingQuestion, InterruptedTurn,
|
|
580
|
+
# PromptNote, WorkingSetPin, LLMContextOverride) write their own members
|
|
581
|
+
# only, so an older chi still drops a new sub-key of one of them; add a
|
|
582
|
+
# top-level field when an older chi must keep it. A removed key would
|
|
583
|
+
# stay an extra forever: leave it out of the extras in .from_h then (no
|
|
584
|
+
# key was ever removed).
|
|
546
585
|
FIELDS = {
|
|
547
586
|
"metadata_version" => 1,
|
|
587
|
+
"written_by" => nil,
|
|
548
588
|
"id" => REQUIRED,
|
|
549
589
|
"mode" => REQUIRED,
|
|
550
590
|
"model_name" => REQUIRED,
|
|
@@ -599,6 +639,7 @@ module Samagotchi
|
|
|
599
639
|
end
|
|
600
640
|
attrs[:settings] = SessionConfig::Settings.new(core: attrs.delete(:config_settings), bundles: attrs.delete(:bundle_settings))
|
|
601
641
|
attrs[:messages] = messages ? Array(attrs[:messages]).map { |msg| symbolize_message_keys(msg) } : []
|
|
642
|
+
attrs[:extra_fields] = data.reject { |key, _| FIELDS.key?(key) }
|
|
602
643
|
new(**attrs)
|
|
603
644
|
end
|
|
604
645
|
|
|
@@ -720,8 +761,10 @@ module Samagotchi
|
|
|
720
761
|
self
|
|
721
762
|
end
|
|
722
763
|
|
|
723
|
-
# The session as #save writes it
|
|
724
|
-
#
|
|
764
|
+
# The session as #save writes it: FIELDS, in their order, then the keys
|
|
765
|
+
# this chi doesn't know (#extra_fields, in the file's order; a FIELDS key
|
|
766
|
+
# wins); a running turn's checkpoint (#turn_checkpoint) in place of its
|
|
767
|
+
# messages.
|
|
725
768
|
def to_h
|
|
726
769
|
checkpoint = checkpointed
|
|
727
770
|
FIELDS.keys.to_h do |key|
|
|
@@ -730,6 +773,7 @@ module Samagotchi
|
|
|
730
773
|
(checkpoint ? checkpoint[:messages] : @messages).map { |msg| scrub_utf8(stringify_message_keys(msg)) }
|
|
731
774
|
when "interrupted_turn" then (checkpoint ? checkpoint[:marker] : @interrupted_turn)&.to_file
|
|
732
775
|
when "pending_question" then @pending_question && scrub_utf8(@pending_question.to_file)
|
|
776
|
+
when "written_by" then VERSION
|
|
733
777
|
when "test_run" then !!@test_run
|
|
734
778
|
when "delegate" then @delegate
|
|
735
779
|
when "llm_context" then llm_context_file
|
|
@@ -755,6 +799,7 @@ module Samagotchi
|
|
|
755
799
|
end
|
|
756
800
|
[key, value]
|
|
757
801
|
end.reject { |key, value| value.nil? && OPTIONAL_FIELDS.include?(key) }
|
|
802
|
+
.merge(@extra_fields.reject { |key, _| FIELDS.key?(key) })
|
|
758
803
|
end
|
|
759
804
|
|
|
760
805
|
# The running turn's checkpoint, while the turn runs (#turn_checkpoint).
|
|
@@ -59,7 +59,7 @@ module Samagotchi
|
|
|
59
59
|
# /help's note per listed mid_turn (the web's command list says the same).
|
|
60
60
|
MID_TURN_NOTES = { "anytime" => "mid-turn too", "depends" => "show: mid-turn too" }.freeze
|
|
61
61
|
|
|
62
|
-
#
|
|
62
|
+
# Commands that may ask before a change that restarts the prompt cache
|
|
63
63
|
# (CacheConfirm): #run passes them its confirm flag.
|
|
64
64
|
CONFIRMING = %i[thinking reasoning_history toolkit].freeze
|
|
65
65
|
|
|
@@ -287,8 +287,8 @@ module Samagotchi
|
|
|
287
287
|
# How help and errors name an entry (the shell's "!" is "!<cmd>").
|
|
288
288
|
def self.display_name(entry) = entry.name == SHELL_BANG_PREFIX ? "#{SHELL_BANG_PREFIX}<cmd>" : entry.name
|
|
289
289
|
|
|
290
|
-
# @param confirm [Boolean] a costly
|
|
291
|
-
#
|
|
290
|
+
# @param confirm [Boolean] a costly setting change asks first
|
|
291
|
+
# (CacheConfirm): true for a person at a web tab or
|
|
292
292
|
# a TUI; false applies it at once (chi send, a delegate, a plugin,
|
|
293
293
|
# chi -p)
|
|
294
294
|
# @param as_parent [Boolean] a parent agent sent it (client cli:answer):
|
|
@@ -304,7 +304,7 @@ module Samagotchi
|
|
|
304
304
|
return nil unless entry&.handler
|
|
305
305
|
return run_bundle_command(entry, text) unless entry.source == "core"
|
|
306
306
|
|
|
307
|
-
#
|
|
307
|
+
# Only commands that may ask take the flag.
|
|
308
308
|
return send(entry.id, text, confirm: confirm) if CONFIRMING.include?(entry.id)
|
|
309
309
|
# Who sent it goes to its handler per call, never through an ivar:
|
|
310
310
|
# an anytime command runs on another thread against this instance.
|
|
@@ -54,8 +54,8 @@ module Samagotchi
|
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
cost = Toolkit.cache_cost(before.prompt_key, after.prompt_key)
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
step = CacheConfirm.ask(line: "#{NAME} #{words}", cost: cost, changed: true, engine: @engine) if confirm && !yes
|
|
58
|
+
if step
|
|
59
59
|
return [[change_line(before, after), reread_line(before, after), step.text].join("\n"), false, step]
|
|
60
60
|
end
|
|
61
61
|
|
|
@@ -85,7 +85,10 @@ module Samagotchi
|
|
|
85
85
|
# @param stop_reason [#call] the stop's reason (:signal: a signal to
|
|
86
86
|
# chi, :session_stopped: a session stop, which the result then names
|
|
87
87
|
# instead of the user)
|
|
88
|
-
# @param env [Hash, nil] added to the command's environment
|
|
88
|
+
# @param env [Hash, nil] added to the command's environment, over
|
|
89
|
+
# chi's Ruby/Bundler variables scrubbed as task_create scrubs them
|
|
90
|
+
# (TaskRuntime.sanitized_spawn_env); chi's callers build it, so it
|
|
91
|
+
# may set those keys (task_create refuses them from the model)
|
|
89
92
|
# @param on_timeout [Symbol, String, nil] :task or :kill (ON_TIMEOUT);
|
|
90
93
|
# nil reads execute.on_timeout
|
|
91
94
|
def self.call(command, cwd: nil, cancelled: -> { false }, stop_reason: -> {}, env: nil, on_timeout: nil)
|
|
@@ -267,7 +270,7 @@ module Samagotchi
|
|
|
267
270
|
# file, and each write lands at the end.
|
|
268
271
|
ios = logs.map { |path| File.open(path, "a") }
|
|
269
272
|
ios.each { |io| io.flock(File::LOCK_SH) }
|
|
270
|
-
ProcessGroup.spawn(env || {}, *Shell.argv(TaskRuntime.wrapped_shell_command(command, exit_code_path)),
|
|
273
|
+
ProcessGroup.spawn(TaskRuntime.sanitized_spawn_env.merge(env || {}), *Shell.argv(TaskRuntime.wrapped_shell_command(command, exit_code_path)),
|
|
271
274
|
chdir: cwd, in: File::NULL, out: ios[0], err: ios[1])
|
|
272
275
|
ensure
|
|
273
276
|
ios&.each(&:close)
|
|
@@ -445,9 +445,11 @@ module Samagotchi
|
|
|
445
445
|
"trap #{Shellwords.escape(write)} EXIT\n#{command}\n"
|
|
446
446
|
end
|
|
447
447
|
|
|
448
|
+
# chi's own Ruby/Bundler setup, kept out of a command's environment
|
|
449
|
+
# (execute and task_create both spawn with it): run under `bundle
|
|
450
|
+
# exec`, chi would otherwise hand a project's `ruby -e` or
|
|
451
|
+
# `bundle exec` its Gemfile (RUBYOPT=-rbundler/setup, BUNDLE_GEMFILE).
|
|
448
452
|
def sanitized_spawn_env
|
|
449
|
-
# Running from temporary directories can inherit Bundler-specific env
|
|
450
|
-
# (for example RUBYOPT=-rbundler/setup) that breaks plain `ruby -e`.
|
|
451
453
|
{
|
|
452
454
|
"RUBYOPT" => sanitized_rubyopt,
|
|
453
455
|
"RUBYLIB" => nil,
|
|
@@ -250,9 +250,17 @@ module Samagotchi
|
|
|
250
250
|
return row.tap { |r| r.status = "not installed" } unless helper.installed?
|
|
251
251
|
|
|
252
252
|
row.from = helper.app_version
|
|
253
|
+
if (problem = helper.launch_problem)
|
|
254
|
+
return failed(row, problem)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
if helper.launch_newer?
|
|
258
|
+
return up_to_date(row).tap { |r| r.status += " (kept: chi #{helper.launch_version} set it up; this chi is older)" }
|
|
259
|
+
end
|
|
260
|
+
|
|
253
261
|
if helper.stale?
|
|
254
262
|
row.to = VERSION
|
|
255
|
-
done(row, :update, @dry_run ? "rebuilds and restarts it" : "restarted") { helper.upgrade { nil } }
|
|
263
|
+
done(row, :update, @dry_run ? "rebuilds and restarts it" : "restarted") { helper.upgrade(keep_launch: true) { nil } }
|
|
256
264
|
elsif helper.launch_outdated?
|
|
257
265
|
helper.refresh_launch_file unless @dry_run
|
|
258
266
|
up_to_date(row).tap { |r| r.status += @dry_run ? " (would refresh the launch file)" : " (launch file refreshed)" }
|