samagotchi 0.121.0 → 0.121.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 +82 -1
- data/bin/chi +3 -2
- data/docs/architecture.md +36 -11
- data/docs/bundle-authoring.md +2 -1
- data/docs/cli.md +10 -4
- data/docs/configuration.md +7 -3
- data/docs/guardrails.md +48 -19
- data/docs/internals/anthropic.md +1 -1
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/participants.md +3 -1
- data/docs/internals/remote-sessions.md +15 -11
- data/docs/internals/web-push.md +22 -11
- data/docs/plugins.md +6 -2
- data/docs/sessions.md +15 -0
- data/docs/testing.md +41 -1
- data/lib/samagotchi/answer_command.rb +1 -1
- data/lib/samagotchi/bridge.rb +8 -8
- data/lib/samagotchi/bridge_client/event_stream.rb +11 -27
- data/lib/samagotchi/bridge_client/stream_open.rb +79 -0
- data/lib/samagotchi/bridge_client/stream_request.rb +8 -2
- data/lib/samagotchi/bridge_client.rb +52 -65
- data/lib/samagotchi/bridge_secret.rb +9 -7
- data/lib/samagotchi/bundle_command/check.rb +27 -2
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/claude-peer/scripts/pair +20 -6
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/codex-peer/scripts/pair +14 -3
- data/lib/samagotchi/bundles/github-pr/manifest.yml +2 -2
- data/lib/samagotchi/bundles/github-pr/plugin.rb +2 -6
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/child_move.rb +5 -7
- data/lib/samagotchi/children_status.rb +2 -1
- data/lib/samagotchi/guardrail_wiring.rb +19 -2
- data/lib/samagotchi/guardrails/parent_approvals.rb +9 -0
- data/lib/samagotchi/guardrails_command.rb +41 -6
- data/lib/samagotchi/image_store.rb +6 -3
- data/lib/samagotchi/interrupted_turn.rb +6 -3
- data/lib/samagotchi/live_versions.rb +6 -7
- data/lib/samagotchi/memory_bundle/profile.rb +1 -1
- data/lib/samagotchi/memory_bundle/uninstaller.rb +6 -2
- data/lib/samagotchi/plugin/api.rb +8 -5
- data/lib/samagotchi/plugin/sessions.rb +13 -4
- data/lib/samagotchi/push_command.rb +1 -1
- data/lib/samagotchi/relay_verifier.rb +2 -3
- data/lib/samagotchi/relay_watcher.rb +2 -2
- data/lib/samagotchi/remote_attach_command.rb +1 -1
- data/lib/samagotchi/reply_wait/poll.rb +8 -2
- data/lib/samagotchi/reply_wait.rb +4 -3
- data/lib/samagotchi/review_command.rb +1 -1
- data/lib/samagotchi/run_options.rb +1 -0
- data/lib/samagotchi/session.rb +4 -0
- data/lib/samagotchi/session_access/local.rb +18 -8
- data/lib/samagotchi/session_access/remote.rb +5 -0
- data/lib/samagotchi/session_access/web_bridge_client.rb +31 -19
- data/lib/samagotchi/session_access.rb +21 -9
- data/lib/samagotchi/session_continue.rb +2 -3
- data/lib/samagotchi/session_manager.rb +118 -28
- data/lib/samagotchi/sessions_command.rb +10 -5
- data/lib/samagotchi/system_one/client.rb +6 -1
- data/lib/samagotchi/system_one/fake_backend.rb +0 -2
- data/lib/samagotchi/system_one/log.rb +21 -6
- data/lib/samagotchi/system_one/self_protect.rb +39 -43
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +8 -3
- data/lib/samagotchi/terminal_ui/attached_loop.rb +4 -2
- data/lib/samagotchi/terminal_ui/local_link.rb +28 -19
- data/lib/samagotchi/toolkit.rb +68 -2
- data/lib/samagotchi/tools/delegate.rb +1 -1
- data/lib/samagotchi/tools/delegate_relay.rb +9 -7
- data/lib/samagotchi/turn_checkpoint.rb +15 -9
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +123 -65
- data/lib/samagotchi/web/participant_access.rb +26 -7
- data/lib/samagotchi/web/participant_review.rb +2 -1
- data/lib/samagotchi/web/participant_view.rb +7 -3
- data/lib/samagotchi/web/participants_admin.rb +13 -0
- data/lib/samagotchi/web/public/app.js +1 -1
- data/lib/samagotchi/web/public/notifications.js +21 -1
- data/lib/samagotchi/web/public/participant.html +3 -2
- data/lib/samagotchi/web/public/participant.js +23 -9
- data/lib/samagotchi/web/public/participants_panel.js +46 -11
- data/lib/samagotchi/web/public/push.js +4 -3
- data/lib/samagotchi/web/public/push_devices.js +17 -3
- data/lib/samagotchi/web/public/review_history.js +14 -2
- data/lib/samagotchi/web/public/review_start.js +1 -3
- data/lib/samagotchi/web/public/session_actions.js +14 -2
- data/lib/samagotchi/web/public/storage.js +16 -0
- data/lib/samagotchi/web/push/client.rb +5 -0
- data/lib/samagotchi/web/push/store.rb +16 -3
- data/lib/samagotchi/web/push_routes.rb +3 -2
- data/lib/samagotchi/web/session_access_routes.rb +25 -17
- data/lib/samagotchi/web/session_create.rb +4 -4
- data/lib/samagotchi/web/session_hub.rb +6 -4
- data/lib/samagotchi/web/session_summary.rb +8 -16
- data/lib/samagotchi/worker.rb +1 -1
- data/lib/samagotchi/worker_link.rb +213 -0
- data/lib/samagotchi/worker_sidecar.rb +15 -37
- metadata +3 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "prism"
|
|
3
4
|
require "rbconfig"
|
|
4
5
|
require "stringio"
|
|
5
6
|
require "tmpdir"
|
|
@@ -214,8 +215,9 @@ module Samagotchi
|
|
|
214
215
|
floor = lower_bound(manifest.requires_chi)
|
|
215
216
|
return unless floor && File.file?(path(manifest.plugin[:file]))
|
|
216
217
|
|
|
217
|
-
|
|
218
|
-
|
|
218
|
+
source = File.read(path(manifest.plugin[:file]))
|
|
219
|
+
calls = source.scan(API_CALL).flat_map { |call| [call, call.split(".").first(2).join(".")] }
|
|
220
|
+
calls = (calls + keyword_calls(source)).uniq.select { |call| Plugin::Api::SINCE.key?(call) }
|
|
219
221
|
call = calls.max_by { |c| Gem::Version.new(Plugin::Api::SINCE[c]) } or return
|
|
220
222
|
version = Plugin::Api::SINCE[call]
|
|
221
223
|
return unless Gem::Version.new(version) > floor
|
|
@@ -226,6 +228,29 @@ module Samagotchi
|
|
|
226
228
|
fix: "requires_chi: \">= #{version}\"")
|
|
227
229
|
end
|
|
228
230
|
|
|
231
|
+
# Each API call's keywords in +source+, as "chi.toolkit(groups:)": the
|
|
232
|
+
# call's receiver as written, its name and the keyword (Prism sees
|
|
233
|
+
# them with or without parens, over lines). Nothing when it doesn't parse.
|
|
234
|
+
def keyword_calls(source)
|
|
235
|
+
result = Prism.parse(source)
|
|
236
|
+
return [] unless result.success?
|
|
237
|
+
|
|
238
|
+
found = []
|
|
239
|
+
nodes = [result.value]
|
|
240
|
+
while (node = nodes.pop)
|
|
241
|
+
nodes.concat(node.compact_child_nodes)
|
|
242
|
+
next unless node.is_a?(Prism::CallNode) && node.receiver && node.arguments
|
|
243
|
+
|
|
244
|
+
call = "#{node.receiver.slice}.#{node.name}"
|
|
245
|
+
node.arguments.arguments.grep(Prism::KeywordHashNode).each do |hash|
|
|
246
|
+
hash.elements.each do |pair|
|
|
247
|
+
found << "#{call}(#{pair.key.unescaped}:)" if pair.is_a?(Prism::AssocNode) && pair.key.is_a?(Prism::SymbolNode)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
found
|
|
252
|
+
end
|
|
253
|
+
|
|
229
254
|
def lower_bound(requirement)
|
|
230
255
|
requirement.to_s.split(",").filter_map do |part|
|
|
231
256
|
version = part.strip[LOWER_BOUND, 1]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: claude-peer
|
|
3
|
-
version: 0.4.
|
|
3
|
+
version: 0.4.5
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Pair this session with a Claude Code session you work with: the claude_send tool and /claude send it a part of the work (a plan step, a diff, a finding) to validate or as context, and Claude's answer comes back as the claude-outbox context; /claude install-skill installs Claude's chi-peer skill, which pairs"
|
|
6
6
|
trust_level: reviewed
|
|
@@ -12,7 +12,7 @@ plugin:
|
|
|
12
12
|
file: plugin.rb
|
|
13
13
|
sha256: sha256:50f427d3862ae9d0d15693b4d887c528162b96c0320cc663df89d59d830d4660
|
|
14
14
|
scripts:
|
|
15
|
-
pair: sha256:
|
|
15
|
+
pair: sha256:d8089ec422a816367d8c41393235223e06b21cbc0ae1e37b6fc22e94ea9a1497
|
|
16
16
|
chi-peer-SKILL.md: sha256:b9366f8f4aee9c54d209368ce421eab7b971c03c75efab0415d1988fd40f9e60
|
|
17
17
|
# The plugin needs Samagotchi::Plugin::Peer and chi.peer_source (chi 0.67.0).
|
|
18
18
|
# An agreeing answer's closing wake (the skills say an OK wakes chi once)
|
|
@@ -26,17 +26,20 @@ while [ $# -gt 0 ]; do
|
|
|
26
26
|
case "$1" in
|
|
27
27
|
--inbox-socket)
|
|
28
28
|
[ $# -ge 2 ] || usage
|
|
29
|
-
inbox=$2
|
|
29
|
+
inbox=$2
|
|
30
|
+
shift 2
|
|
30
31
|
[ -n "$inbox" ] || usage
|
|
31
32
|
;;
|
|
32
33
|
--inbox-socket=*)
|
|
33
|
-
inbox=${1#--inbox-socket=}
|
|
34
|
+
inbox=${1#--inbox-socket=}
|
|
35
|
+
shift
|
|
34
36
|
[ -n "$inbox" ] || usage
|
|
35
37
|
;;
|
|
36
38
|
-h | --help) usage ;;
|
|
37
39
|
*)
|
|
38
40
|
[ -z "$target" ] || usage
|
|
39
|
-
target=$1
|
|
41
|
+
target=$1
|
|
42
|
+
shift
|
|
40
43
|
;;
|
|
41
44
|
esac
|
|
42
45
|
done
|
|
@@ -84,10 +87,21 @@ case "$target" in
|
|
|
84
87
|
*) id=$target ;;
|
|
85
88
|
esac
|
|
86
89
|
|
|
87
|
-
# A JSON string: backslashes
|
|
90
|
+
# A JSON string: backslashes and quotes escaped, newlines, tabs and
|
|
91
|
+
# carriage returns as \n, \t, \r, any other control character as \u00XX.
|
|
88
92
|
json() {
|
|
89
|
-
printf '%s' "$1" |
|
|
90
|
-
|
|
93
|
+
printf '%s' "$1" | awk '
|
|
94
|
+
BEGIN {
|
|
95
|
+
ORS = ""; printf "\""
|
|
96
|
+
for (i = 1; i < 32; i++) esc[sprintf("%c", i)] = sprintf("\\u%04x", i)
|
|
97
|
+
esc["\t"] = "\\t"; esc["\r"] = "\\r"; esc["\\"] = "\\\\"; esc["\""] = "\\\""
|
|
98
|
+
}
|
|
99
|
+
NR > 1 { print "\\n" }
|
|
100
|
+
{
|
|
101
|
+
n = length($0)
|
|
102
|
+
for (i = 1; i <= n; i++) { c = substr($0, i, 1); print ((c in esc) ? esc[c] : c) }
|
|
103
|
+
}
|
|
104
|
+
END { printf "\"" }'
|
|
91
105
|
}
|
|
92
106
|
|
|
93
107
|
# Attaches a pushed source, unless the session has one of that name already.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: codex-peer
|
|
3
|
-
version: 0.3.
|
|
3
|
+
version: 0.3.4
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Pair this session with an OpenAI Codex CLI session you work with: the codex_send tool and /codex send it a part of the work (a plan step, a diff, a finding) to validate or as context, with codex queue, and Codex's answer comes back as the codex-outbox context; /codex install-skill installs Codex's chi-peer skill, which pairs"
|
|
6
6
|
trust_level: reviewed
|
|
@@ -12,7 +12,7 @@ plugin:
|
|
|
12
12
|
file: plugin.rb
|
|
13
13
|
sha256: sha256:b03465566272891237ac1906980ff6956579a4703d3a42c414ce50a81de551f6
|
|
14
14
|
scripts:
|
|
15
|
-
pair: sha256:
|
|
15
|
+
pair: sha256:a61c3d311ce3f3e5386e90f0f867f649a39763a5883877e731e43d84dd9c85aa
|
|
16
16
|
chi-peer-SKILL.md: sha256:7c392ac04c115ea1e762ad562b5c4c44c3eca71558ba5d9dfeaeabdd533b6a82
|
|
17
17
|
# The plugin needs Samagotchi::Plugin::Peer and chi.peer_source (chi 0.67.0).
|
|
18
18
|
# An agreeing answer's closing wake (the skills say an OK wakes chi once)
|
|
@@ -93,10 +93,21 @@ MSG
|
|
|
93
93
|
exit 1
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
# A JSON string: backslashes
|
|
96
|
+
# A JSON string: backslashes and quotes escaped, newlines, tabs and
|
|
97
|
+
# carriage returns as \n, \t, \r, any other control character as \u00XX.
|
|
97
98
|
json() {
|
|
98
|
-
printf '%s' "$1" |
|
|
99
|
-
|
|
99
|
+
printf '%s' "$1" | awk '
|
|
100
|
+
BEGIN {
|
|
101
|
+
ORS = ""; printf "\""
|
|
102
|
+
for (i = 1; i < 32; i++) esc[sprintf("%c", i)] = sprintf("\\u%04x", i)
|
|
103
|
+
esc["\t"] = "\\t"; esc["\r"] = "\\r"; esc["\\"] = "\\\\"; esc["\""] = "\\\""
|
|
104
|
+
}
|
|
105
|
+
NR > 1 { print "\\n" }
|
|
106
|
+
{
|
|
107
|
+
n = length($0)
|
|
108
|
+
for (i = 1; i <= n; i++) { c = substr($0, i, 1); print ((c in esc) ? esc[c] : c) }
|
|
109
|
+
}
|
|
110
|
+
END { printf "\"" }'
|
|
100
111
|
}
|
|
101
112
|
|
|
102
113
|
# Attaches a pushed source, unless the session has one of that name already.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: github-pr
|
|
3
|
-
version: 0.5.
|
|
3
|
+
version: 0.5.1
|
|
4
4
|
scope: system
|
|
5
5
|
description: Attaches the branch's open GitHub pull request to a session as context (and any PR URL with chi context add or the web's "+ URL"); the agent gets a note when reviews, comments, checks or its state change; in the web, a path:line in an answer links to that line of the PR; the auto_attach setting can offer the PR with a card first, or attach nothing
|
|
6
6
|
trust_level: reviewed
|
|
@@ -11,7 +11,7 @@ needs:
|
|
|
11
11
|
hint: brew install gh, then gh auth login
|
|
12
12
|
plugin:
|
|
13
13
|
file: plugin.rb
|
|
14
|
-
sha256: sha256:
|
|
14
|
+
sha256: sha256:cfadb7668528302b3631681e570899230b6878c80ccbc87dfb16c80d43dead7b
|
|
15
15
|
scripts:
|
|
16
16
|
pr_context.rb: sha256:a883d2780e89c68c0d7b6d5e0fcb22c273f17bb167e79064e0e99db6bd2ee6f0
|
|
17
17
|
context_providers:
|
|
@@ -649,12 +649,8 @@ class LineLinker
|
|
|
649
649
|
out << head[pos..] << text[MAX_SCAN..].to_s
|
|
650
650
|
end
|
|
651
651
|
|
|
652
|
-
#
|
|
653
|
-
# no PR has the file, or more than one does.
|
|
654
|
-
def url_for(path, from, to = nil) = line_for(path, from, to)&.fetch(:url)
|
|
655
|
-
|
|
656
|
-
# {url:, diff:, path:, line:, end_line:} for +path+'s lines (see #url_for),
|
|
657
|
-
# or nil.
|
|
652
|
+
# {url:, diff:, path:, line:, end_line:} for +path+'s lines +from+..+to+
|
|
653
|
+
# (+to+ nil: one line), or nil: no PR has the file, or more than one does.
|
|
658
654
|
def line_for(path, from, to = nil)
|
|
659
655
|
found = @prs.filter_map do |pr, data|
|
|
660
656
|
file = resolve(data.files, path)
|
|
@@ -6,7 +6,7 @@ require "time"
|
|
|
6
6
|
require_relative "atomic_file"
|
|
7
7
|
require_relative "session"
|
|
8
8
|
require_relative "session_inbox"
|
|
9
|
-
require_relative "
|
|
9
|
+
require_relative "worker_link"
|
|
10
10
|
require_relative "log"
|
|
11
11
|
require_relative "tools/delegate_cursor"
|
|
12
12
|
|
|
@@ -93,13 +93,11 @@ module Samagotchi
|
|
|
93
93
|
# installed (SessionManager.worker_command): it moves.
|
|
94
94
|
# @return [String, nil]
|
|
95
95
|
def unmovable(child_id, state_dir:)
|
|
96
|
-
|
|
97
|
-
return "open in a chi REPL; close it there" if owner
|
|
96
|
+
link = WorkerLink.resolve(Session.session_dir(child_id, state_dir: state_dir), unlink: false)
|
|
97
|
+
return "open in a chi REPL; close it there" if link.owner&.tui?
|
|
98
|
+
return nil if !link.live? || link.features.include?(FEATURE)
|
|
98
99
|
|
|
99
|
-
|
|
100
|
-
return nil if sidecar.nil? || sidecar.features.include?(FEATURE)
|
|
101
|
-
|
|
102
|
-
"older chi worker#{" #{sidecar.version}" if sidecar.version}; restart it"
|
|
100
|
+
"older chi worker#{" #{link.version}" if link.version}; restart it"
|
|
103
101
|
end
|
|
104
102
|
|
|
105
103
|
# Move +ids+ (direct children of +from+) to +to+: the intent, the
|
|
@@ -5,6 +5,7 @@ require_relative "session"
|
|
|
5
5
|
require_relative "archive_store"
|
|
6
6
|
require_relative "git_head"
|
|
7
7
|
require_relative "reply_wait"
|
|
8
|
+
require_relative "worker_link"
|
|
8
9
|
require_relative "tools/delegate"
|
|
9
10
|
require_relative "tools/delegate_cursor"
|
|
10
11
|
|
|
@@ -106,7 +107,7 @@ module Samagotchi
|
|
|
106
107
|
# The keys of a children_of row that state_of and child read, for a
|
|
107
108
|
# session counts loaded itself.
|
|
108
109
|
def summary_of(session, state_dir:)
|
|
109
|
-
live =
|
|
110
|
+
live = WorkerLink.resolve(Session.session_dir(session.id, state_dir: state_dir), unlink: false).worker_owned?
|
|
110
111
|
{ live: live, busy: live && session.status == Session::STATUS_RUNNING, status: session.status,
|
|
111
112
|
updated_at: session.updated_at, waiting: session.waiting_question(live: live)&.dig(:kind),
|
|
112
113
|
preview: session.last_prompt.to_s }
|
|
@@ -271,7 +271,14 @@ module Samagotchi
|
|
|
271
271
|
# System 1: the ask is judged before the user sees it, and in `on` mode
|
|
272
272
|
# one that passes every gate is allowed once without reaching them
|
|
273
273
|
# (docs/guardrails.md, "System 1").
|
|
274
|
-
judgment =
|
|
274
|
+
judgment = begin
|
|
275
|
+
system_one_judge(payload)
|
|
276
|
+
rescue LLM::RequestCancelled
|
|
277
|
+
return turn_stopped(verdict)
|
|
278
|
+
end
|
|
279
|
+
# A turn stopped while the judge was asked opens no card.
|
|
280
|
+
return turn_stopped(verdict) if @cancelled_lookup.call
|
|
281
|
+
|
|
275
282
|
if judgment && judgment[:auto]
|
|
276
283
|
verdict.judgment = { id: judgment[:id], p: judgment[:p] }
|
|
277
284
|
verdict.settle!(:allow, decided_by: "system one")
|
|
@@ -286,6 +293,12 @@ module Samagotchi
|
|
|
286
293
|
|
|
287
294
|
private
|
|
288
295
|
|
|
296
|
+
# The deny for a call whose turn was stopped before its ask reached the
|
|
297
|
+
# user (as Guardrails::Gate words one stopped before the vote).
|
|
298
|
+
def turn_stopped(verdict)
|
|
299
|
+
verdict.settle!(:deny, decided_by: "core", note: "The turn was stopped before the user was asked.")
|
|
300
|
+
end
|
|
301
|
+
|
|
289
302
|
# The judgments log (SystemOne::Log), beside the approvals store; nil
|
|
290
303
|
# while there is no store dir.
|
|
291
304
|
def system_one_log
|
|
@@ -319,6 +332,8 @@ module Samagotchi
|
|
|
319
332
|
entries: entries),
|
|
320
333
|
questions: { SystemOne::ApprovalQuestion::ID => SystemOne::ApprovalQuestion.question },
|
|
321
334
|
cancel_controller: @cancel_controller.call)
|
|
335
|
+
rescue LLM::RequestCancelled
|
|
336
|
+
raise
|
|
322
337
|
rescue StandardError => e
|
|
323
338
|
# The client already turns every failure it knows into a nil; this is
|
|
324
339
|
# the unexpected one. Either way the ask goes to the user.
|
|
@@ -338,6 +353,8 @@ module Samagotchi
|
|
|
338
353
|
latency_ms: latency_ms, decision: decision, origin: "live")
|
|
339
354
|
# A judgment the log cannot hold is no judgment: the user is asked.
|
|
340
355
|
{ id: id, p: p, auto: auto && !id.nil? }
|
|
356
|
+
rescue LLM::RequestCancelled
|
|
357
|
+
raise
|
|
341
358
|
rescue StandardError => e
|
|
342
359
|
# The approval never fails because of System 1.
|
|
343
360
|
Samagotchi::Log.warn(:guardrails, "system_one_judgment_failed", error: "#{e.class}: #{e.message}")
|
|
@@ -408,7 +425,7 @@ module Samagotchi
|
|
|
408
425
|
"deny"
|
|
409
426
|
end
|
|
410
427
|
end
|
|
411
|
-
system_one_log.label(id, label)
|
|
428
|
+
system_one_log.label(id, label, question_id: (answer.id if answer.is_a?(Samagotchi::QuestionAnswer)))
|
|
412
429
|
rescue StandardError => e
|
|
413
430
|
Samagotchi::Log.warn(:guardrails, "system_one_label_failed", error: "#{e.class}: #{e.message}")
|
|
414
431
|
nil
|
|
@@ -69,6 +69,15 @@ module Samagotchi
|
|
|
69
69
|
# memory-remove rule's ask, which a parent may answer.
|
|
70
70
|
return false if fetch(facts, :rule).to_s == MEMORY_REMOVE_RULE
|
|
71
71
|
|
|
72
|
+
chi_files?(facts)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Whether an approval's paths or command name chi's own dirs (#chi_dirs)
|
|
76
|
+
# or CHI_TEXT, whatever its rule: the part of #protected_facts? that
|
|
77
|
+
# System 1's SystemOne::SelfProtect shares.
|
|
78
|
+
def chi_files?(facts)
|
|
79
|
+
return false unless facts.is_a?(Hash)
|
|
80
|
+
|
|
72
81
|
dirs = chi_dirs
|
|
73
82
|
paths = Array(fetch(facts, :paths)).map(&:to_s).reject(&:empty?)
|
|
74
83
|
return true if paths.any? { |path| dirs.any? { |dir| ProtectedPaths.within?(ProtectedPaths.real(path), dir) } }
|
|
@@ -61,6 +61,7 @@ module Samagotchi
|
|
|
61
61
|
# uses.
|
|
62
62
|
SKIP_WORDS = {
|
|
63
63
|
already_replayed: "already replayed",
|
|
64
|
+
judged_live: "already judged live",
|
|
64
65
|
answered_by_parent: "answered by a parent agent",
|
|
65
66
|
parent_cannot_allow: "answered by a parent that could not allow",
|
|
66
67
|
unanswered: "unanswered",
|
|
@@ -73,6 +74,36 @@ module Samagotchi
|
|
|
73
74
|
# and when the user was asked.
|
|
74
75
|
Ask = Data.define(:question_id, :session_id, :approval, :label, :asked_at)
|
|
75
76
|
|
|
77
|
+
# What the judgments log already holds, so no ask is judged twice: the
|
|
78
|
+
# question ids its judgments name, with their origin (a replay's own, a
|
|
79
|
+
# live one's through its label), and, for live judgments with none (an ask that was
|
|
80
|
+
# cancelled, or a line older than the id), how many each (session, call
|
|
81
|
+
# digest) has. A card with no id match takes one of those (#take_live).
|
|
82
|
+
Judged = Data.define(:question_ids, :live_calls) do
|
|
83
|
+
# @param entries [Array<Hash>] SystemOne::Log#entries
|
|
84
|
+
def self.of(entries)
|
|
85
|
+
ids = entries.filter_map { |entry| [entry["question_id"], entry["origin"]] if entry["question_id"] }.to_h
|
|
86
|
+
live = Hash.new(0)
|
|
87
|
+
entries.each do |entry|
|
|
88
|
+
next unless entry["origin"] == "live" && entry["question_id"].nil?
|
|
89
|
+
|
|
90
|
+
live[[entry["session_id"], entry["call_digest"]]] += 1
|
|
91
|
+
end
|
|
92
|
+
new(question_ids: ids, live_calls: live)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Whether a live judgment without a question id is this card's: true
|
|
96
|
+
# (and that judgment counted off) while one with its session and call
|
|
97
|
+
# digest is left.
|
|
98
|
+
def take_live(session_id, approval)
|
|
99
|
+
key = [session_id, SystemOne::ApprovalQuestion.call_fields(approval)["call_digest"]]
|
|
100
|
+
return false unless live_calls[key].positive?
|
|
101
|
+
|
|
102
|
+
live_calls[key] -= 1
|
|
103
|
+
true
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
76
107
|
# @param argv [Array<String>] the arguments after "guardrails"
|
|
77
108
|
# @param client [SystemOne::Client, nil] the judgment client; nil: the
|
|
78
109
|
# one config.yml describes (the seam the specs pass a fake through)
|
|
@@ -122,7 +153,7 @@ module Samagotchi
|
|
|
122
153
|
|
|
123
154
|
sessions_dir = @sessions_dir || Session.default_state_dir
|
|
124
155
|
log = @log || SystemOne::Log.new(dir: Guardrails::Approvals.dir_for(sessions_dir))
|
|
125
|
-
|
|
156
|
+
judged = Judged.of(File.exist?(log.path) ? log.entries : [])
|
|
126
157
|
@dry_run = options[:dry_run] ? true : false
|
|
127
158
|
|
|
128
159
|
asks = []
|
|
@@ -140,7 +171,7 @@ module Samagotchi
|
|
|
140
171
|
end
|
|
141
172
|
|
|
142
173
|
entries.each do |entry|
|
|
143
|
-
ask = ask_of(entry, name,
|
|
174
|
+
ask = ask_of(entry, name, judged, sessions_dir)
|
|
144
175
|
next if ask.nil?
|
|
145
176
|
|
|
146
177
|
ask.is_a?(Symbol) ? skipped[ask] += 1 : asks << ask
|
|
@@ -168,9 +199,10 @@ module Samagotchi
|
|
|
168
199
|
end
|
|
169
200
|
|
|
170
201
|
# One card entry as an ask to judge: an Ask, a skip reason (:already_replayed,
|
|
171
|
-
# :answered_by_parent, :parent_cannot_allow, :unanswered,
|
|
172
|
-
# :no_question_id), or nil for an entry that is not a
|
|
173
|
-
|
|
202
|
+
# :judged_live, :answered_by_parent, :parent_cannot_allow, :unanswered,
|
|
203
|
+
# :no_approval, :no_question_id), or nil for an entry that is not a
|
|
204
|
+
# question at all.
|
|
205
|
+
def ask_of(entry, session_id, judged, sessions_dir)
|
|
174
206
|
return nil unless entry.is_a?(Hash) && entry[:type] == "question"
|
|
175
207
|
|
|
176
208
|
pending = entry[:pending_question]
|
|
@@ -178,7 +210,10 @@ module Samagotchi
|
|
|
178
210
|
|
|
179
211
|
question_id = pending[:id].to_s
|
|
180
212
|
return :no_question_id if question_id.empty?
|
|
181
|
-
|
|
213
|
+
|
|
214
|
+
origin = judged.question_ids[question_id]
|
|
215
|
+
return origin == "replay" ? :already_replayed : :judged_live if origin
|
|
216
|
+
return :judged_live if judged.take_live(session_id, pending[:approval])
|
|
182
217
|
|
|
183
218
|
answer = entry[:answer]
|
|
184
219
|
label = if entry[:cancelled]
|
|
@@ -201,6 +201,9 @@ module Samagotchi
|
|
|
201
201
|
# a request is sent.
|
|
202
202
|
module ImageStore
|
|
203
203
|
class Error < StandardError
|
|
204
|
+
# A ref to a file this session doesn't hold as an image: one wording
|
|
205
|
+
# for a turn's refs (#check_refs) and a web upload's (#ref_for).
|
|
206
|
+
def self.unknown_image(file) = new("unknown image #{file.to_s[0, 80]}")
|
|
204
207
|
end
|
|
205
208
|
|
|
206
209
|
DIR = "images"
|
|
@@ -278,7 +281,7 @@ module Samagotchi
|
|
|
278
281
|
|
|
279
282
|
ref = symbolize(image)
|
|
280
283
|
return "images are refs to uploaded files, not paths" if ref.key?(:path)
|
|
281
|
-
return
|
|
284
|
+
return Error.unknown_image(ref[:file]).message unless valid_ref?(session_dir, ref)
|
|
282
285
|
|
|
283
286
|
{ file: ref[:file].to_s, name: File.basename(ref[:name].to_s)[0, 120] }
|
|
284
287
|
end
|
|
@@ -321,11 +324,11 @@ module Samagotchi
|
|
|
321
324
|
# A ref for an image already stored in this session (a web upload),
|
|
322
325
|
# rebuilt from the file itself: only its name comes from the caller.
|
|
323
326
|
def self.ref_for(session_dir, file:, name: nil, source: "user")
|
|
324
|
-
raise Error
|
|
327
|
+
raise Error.unknown_image(file) unless valid_ref?(session_dir, { file: file })
|
|
325
328
|
|
|
326
329
|
bytes = File.binread(File.join(session_dir.to_s, file.to_s))
|
|
327
330
|
info = ImageHeader.read(bytes)
|
|
328
|
-
raise Error
|
|
331
|
+
raise Error.unknown_image(file) unless info && MIME.key?(info.format) && info.width
|
|
329
332
|
|
|
330
333
|
name = File.basename(name.to_s).gsub(/[[:cntrl:]]/, "")[0, 120]
|
|
331
334
|
{ file: file.to_s, mime: MIME.fetch(info.format), width: info.width, height: info.height,
|
|
@@ -13,9 +13,12 @@ module Samagotchi
|
|
|
13
13
|
# runs (its ack's id, LastTurn#enqueued_id once it ends; a Continue turn
|
|
14
14
|
# carries its offer turn's), so any process that loads the session can
|
|
15
15
|
# tell which turn is running (ReplyWait.own_turn); nil when it ran none,
|
|
16
|
-
# or a worker from before it wrote one.
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
# or a worker from before it wrote one. author: a participant's turn's
|
|
17
|
+
# "p:<id>" (nil for the owner's), which its settled record keeps, so a
|
|
18
|
+
# chi web restarted since still knows whose wait it is
|
|
19
|
+
# (ParticipantAccess#turn_author).
|
|
20
|
+
InterruptedTurn = Data.define(:id, :started_at, :at, :iteration, :enqueued_id, :author) do
|
|
21
|
+
def initialize(id: nil, started_at: nil, at: nil, iteration: nil, enqueued_id: nil, author: nil) = super
|
|
19
22
|
|
|
20
23
|
# A saved record (string or symbol keys), an InterruptedTurn as it is;
|
|
21
24
|
# nil for anything else.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "session"
|
|
4
4
|
require_relative "version"
|
|
5
|
-
require_relative "
|
|
5
|
+
require_relative "worker_link"
|
|
6
6
|
require_relative "web/info_probe"
|
|
7
7
|
|
|
8
8
|
module Samagotchi
|
|
@@ -21,13 +21,12 @@ module Samagotchi
|
|
|
21
21
|
|
|
22
22
|
# @return [Array<Worker>] the workers whose Bridge answers, by session id
|
|
23
23
|
def workers(state_dir: Session.default_state_dir)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
next unless sidecar
|
|
24
|
+
WorkerLink.each_dir(state_dir).filter_map do |session_dir|
|
|
25
|
+
link = WorkerLink.resolve(session_dir, unlink: false)
|
|
26
|
+
next unless link.live?
|
|
28
27
|
|
|
29
|
-
Worker.new(session_id: File.basename(session_dir), version:
|
|
30
|
-
restart:
|
|
28
|
+
Worker.new(session_id: File.basename(session_dir), version: link.version,
|
|
29
|
+
restart: link.features.include?("restart"))
|
|
31
30
|
end
|
|
32
31
|
end
|
|
33
32
|
|
|
@@ -152,7 +152,7 @@ module Samagotchi
|
|
|
152
152
|
# @raise [Uninstaller::UninstallError] when +name+ isn't installed
|
|
153
153
|
def uninstall(name, shipped_dir: SourceNormalizer::SHIPPED_DIR, force: false)
|
|
154
154
|
bundle = Provenance.new(name: name).record
|
|
155
|
-
raise Uninstaller::UninstallError
|
|
155
|
+
raise Uninstaller::UninstallError.not_installed(name) unless bundle
|
|
156
156
|
|
|
157
157
|
shipped = File.join(shipped_dir, name)
|
|
158
158
|
manifest = File.file?(File.join(shipped, "manifest.yml")) ? Manifest.read(dir: shipped) : nil
|
|
@@ -12,7 +12,11 @@ require_relative "../model_overlay"
|
|
|
12
12
|
module Samagotchi
|
|
13
13
|
module MemoryBundle
|
|
14
14
|
class Uninstaller
|
|
15
|
-
class UninstallError < StandardError
|
|
15
|
+
class UninstallError < StandardError
|
|
16
|
+
# The bundle +name+ has no install record: one wording for the
|
|
17
|
+
# uninstall of a bundle and of a profile (Profile#uninstall).
|
|
18
|
+
def self.not_installed(name) = new("Bundle '#{name}' is not installed")
|
|
19
|
+
end
|
|
16
20
|
|
|
17
21
|
# removed_files: the bundle's own hooks/ and plugin/ files (deleted);
|
|
18
22
|
# trashed_files: its memory files, moved to trash_dir (Trash), nil
|
|
@@ -34,7 +38,7 @@ module Samagotchi
|
|
|
34
38
|
def run
|
|
35
39
|
provenance = Provenance.new(name: @name)
|
|
36
40
|
bundle = provenance.record
|
|
37
|
-
raise UninstallError
|
|
41
|
+
raise UninstallError.not_installed(@name) unless bundle
|
|
38
42
|
|
|
39
43
|
# The scope from the flag, else the record's
|
|
40
44
|
target_scope = @scope.to_s.strip.empty? ? bundle.effective_scope : @scope
|
|
@@ -24,9 +24,11 @@ module Samagotchi
|
|
|
24
24
|
# A call not listed gets no floor check: it is in the first tag (v0.2.0:
|
|
25
25
|
# chi.command/tool/on/service/init, ctx.settings, ctx.sessions,
|
|
26
26
|
# ctx.ask_model, ctx.notify, ctx.card, ctx.messages…). A member of
|
|
27
|
-
# one (ctx.sessions.children) is listed when it came later
|
|
28
|
-
# no "?" (API_CALL drops it). A
|
|
29
|
-
#
|
|
27
|
+
# one (ctx.sessions.children) is listed when it came later; fork, send
|
|
28
|
+
# and read are in v0.2.0. Keys have no "?" (API_CALL drops it). A
|
|
29
|
+
# keyword added later is "call(keyword:)", the call's receiver as the
|
|
30
|
+
# plugin writes it (Check#keyword_calls: `ctx.sessions.send(…,
|
|
31
|
+
# delivery:)` is seen, a `sessions = ctx.sessions` alias isn't).
|
|
30
32
|
# Events aren't listed: chi.on takes any symbol and an older chi never
|
|
31
33
|
# fires one it doesn't know (harmless; chi bundle check warns on it),
|
|
32
34
|
# and a manifest hook's events: list (BundleHook) needs the bundle's
|
|
@@ -42,7 +44,8 @@ module Samagotchi
|
|
|
42
44
|
"chi.tool_source" => "0.67.0", "chi.peer_source" => "0.67.0", "ctx.http" => "0.80.0",
|
|
43
45
|
"ctx.cost" => "0.83.0", "ctx.sessions.children" => "0.34.0", "ctx.load_tools" => "0.99.0",
|
|
44
46
|
"ctx.usage" => "0.100.0", "ctx.sessions.stop" => "0.34.0", "ctx.turn_author" => "0.106.0",
|
|
45
|
-
"ctx.sessions.review" => "0.107.0"
|
|
47
|
+
"ctx.sessions.review" => "0.107.0", "ctx.sessions.send(delivery:)" => "0.64.0",
|
|
48
|
+
"chi.toolkit(prompt:)" => "0.72.0", "ctx.ask_model(model:)" => "0.80.0", "chi.toolkit(groups:)" => "0.121.0"
|
|
46
49
|
}.freeze
|
|
47
50
|
|
|
48
51
|
# @param bundle [String]
|
|
@@ -319,7 +322,7 @@ module Samagotchi
|
|
|
319
322
|
name = name.to_s
|
|
320
323
|
raise ArgumentError, "toolkit name #{name.inspect} must be a-z, 0-9, _ and -" unless name.match?(TOOLKIT_NAME)
|
|
321
324
|
|
|
322
|
-
problem = Toolkit.prompt_error(prompt)
|
|
325
|
+
problem = Toolkit.prompt_error(prompt) || (Toolkit.groups_error(groups) unless groups == :all)
|
|
323
326
|
raise ArgumentError, "toolkit #{name}: #{problem}" if problem
|
|
324
327
|
raise ArgumentError, "this chi has no toolkits (plugins: false)" unless @registries.toolkits
|
|
325
328
|
if Toolkit::BUILTIN_KITS.key?(name)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require_relative "../client_id"
|
|
4
4
|
require_relative "../delivery"
|
|
5
5
|
require_relative "../session"
|
|
6
|
-
require_relative "../
|
|
6
|
+
require_relative "../worker_link"
|
|
7
7
|
require_relative "../bridge/turn_accumulator"
|
|
8
8
|
require_relative "../tools/delegate"
|
|
9
9
|
require_relative "../children_status"
|
|
@@ -27,6 +27,11 @@ module Samagotchi
|
|
|
27
27
|
# A fork, send, read or stop that couldn't be done; the message says why.
|
|
28
28
|
class Error < StandardError; end
|
|
29
29
|
|
|
30
|
+
# What starting a session can be refused by, answered as Error with
|
|
31
|
+
# chi's own message: a model that is gone, a toolkit the config refuses,
|
|
32
|
+
# a bad argument. #review adds its own: a subject it won't take.
|
|
33
|
+
REFUSED = [ModelProfile::MissingModel, Toolkit::Refused, ArgumentError].freeze
|
|
34
|
+
|
|
30
35
|
# @param host [Host] session_id, cwd, model_name and state_dir
|
|
31
36
|
def initialize(host)
|
|
32
37
|
@host = host
|
|
@@ -44,7 +49,9 @@ module Samagotchi
|
|
|
44
49
|
# @param prompt [String, nil] the child's first turn
|
|
45
50
|
# @return [String] the child's id
|
|
46
51
|
# @raise [Error] no session yet, a scratch session (the child would
|
|
47
|
-
# outlive it),
|
|
52
|
+
# outlive it), too many children running, or the model or toolkit the
|
|
53
|
+
# child starts on being refused (a config kit with an invalid groups:,
|
|
54
|
+
# a model that is gone)
|
|
48
55
|
def fork(messages:, title: nil, prompt: nil)
|
|
49
56
|
parent_id = @host.session_id.call or raise Error, "this session has no id yet"
|
|
50
57
|
raise Error, "a scratch session starts no other sessions: they would outlive it" if @host.scratch&.call
|
|
@@ -60,6 +67,8 @@ module Samagotchi
|
|
|
60
67
|
setup: @host.setup&.call || SessionSetup.new
|
|
61
68
|
)
|
|
62
69
|
child.id
|
|
70
|
+
rescue *REFUSED => e
|
|
71
|
+
raise Error, e.message
|
|
63
72
|
end
|
|
64
73
|
|
|
65
74
|
# A review of this session (ReviewStart.fork, chi review --from): a new
|
|
@@ -81,7 +90,7 @@ module Samagotchi
|
|
|
81
90
|
|
|
82
91
|
ReviewStart.fork(from: source, subjects: Array(subjects), seed: seed, state_dir: state_dir,
|
|
83
92
|
recap_wait: recap_wait).id
|
|
84
|
-
rescue ReviewStart::Refused
|
|
93
|
+
rescue *REFUSED, ReviewStart::Refused => e
|
|
85
94
|
raise Error, e.message
|
|
86
95
|
end
|
|
87
96
|
|
|
@@ -212,7 +221,7 @@ module Samagotchi
|
|
|
212
221
|
end
|
|
213
222
|
|
|
214
223
|
def live_snapshot(sid, state_dir)
|
|
215
|
-
|
|
224
|
+
WorkerLink.client(sid, session_dir: Session.session_dir(sid, state_dir: state_dir))&.get_json("snapshot")
|
|
216
225
|
rescue StandardError
|
|
217
226
|
nil
|
|
218
227
|
end
|
|
@@ -135,7 +135,7 @@ module Samagotchi
|
|
|
135
135
|
# route does). @return [Boolean] delivered
|
|
136
136
|
def test(sub)
|
|
137
137
|
result = client_for(sub).deliver(sub, Web::Push::TEST_PAYLOAD, ttl: Web::Push::TEST_TTL, urgency: "high")
|
|
138
|
-
words =
|
|
138
|
+
words = [result.status, result.code, ("(#{result.ms} ms)" if result.ms)].reject { |part| part.to_s.empty? }.join(" ")
|
|
139
139
|
words += ": #{result.error}" if result.error && !result.ok?
|
|
140
140
|
if result.status == :gone
|
|
141
141
|
@store.remove(sub.id)
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "client_id"
|
|
4
4
|
require_relative "session"
|
|
5
|
-
require_relative "
|
|
6
|
-
require_relative "worker_sidecar"
|
|
5
|
+
require_relative "worker_link"
|
|
7
6
|
require_relative "question_desk"
|
|
8
7
|
require_relative "log"
|
|
9
8
|
|
|
@@ -87,7 +86,7 @@ module Samagotchi
|
|
|
87
86
|
# @return [Hash, nil] what the parent's live Bridge says about the relay
|
|
88
87
|
def parent_relay(parent_id, relay_id)
|
|
89
88
|
dir = Session.session_dir(parent_id, state_dir: @state_dir)
|
|
90
|
-
client =
|
|
89
|
+
client = WorkerLink.client(parent_id, session_dir: dir) or return nil
|
|
91
90
|
response = client.relay_status(relay_id)
|
|
92
91
|
response.ok? ? response.json : nil
|
|
93
92
|
rescue SystemCallError, IOError
|