samagotchi 0.170.0 → 0.171.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 +29 -1
- data/docs/architecture.md +2 -2
- data/docs/cli.md +37 -23
- data/docs/configuration.md +1 -1
- data/docs/internals/block-renderers.md +2 -2
- data/docs/internals/bridge.md +5 -4
- data/docs/internals/context-telemetry.md +13 -1
- data/docs/internals/engine.md +4 -4
- data/docs/internals/inline-widgets.md +1 -1
- data/docs/internals/objectives.md +1 -1
- data/docs/internals/participants.md +235 -119
- data/docs/internals/web-frontend.md +164 -33
- data/docs/internals/working-set.md +69 -5
- data/docs/releasing.md +2 -1
- data/docs/sessions.md +6 -3
- data/lib/samagotchi/bridge/turn_accumulator.rb +8 -4
- data/lib/samagotchi/bridge.rb +39 -12
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/delivery.rb +13 -0
- data/lib/samagotchi/engine.rb +5 -1
- data/lib/samagotchi/event_fanout.rb +22 -1
- data/lib/samagotchi/grants.rb +5 -0
- data/lib/samagotchi/guardrails/review_paths.rb +2 -2
- data/lib/samagotchi/image_store.rb +51 -12
- data/lib/samagotchi/kernel_loop.rb +7 -1
- data/lib/samagotchi/llm/stray_tool_call.rb +7 -4
- data/lib/samagotchi/llm_context_stale.rb +4 -1
- data/lib/samagotchi/participant_messages.rb +39 -0
- data/lib/samagotchi/participant_revise.rb +61 -0
- data/lib/samagotchi/participant_uploads.rb +98 -0
- data/lib/samagotchi/reply_wait/poll.rb +5 -1
- data/lib/samagotchi/revise_anchor.rb +9 -0
- data/lib/samagotchi/send_command.rb +5 -0
- data/lib/samagotchi/session_inbox.rb +20 -12
- data/lib/samagotchi/session_manager/delivery.rb +7 -0
- data/lib/samagotchi/session_stores.rb +2 -0
- data/lib/samagotchi/tool_runner.rb +39 -5
- data/lib/samagotchi/tools/memory.rb +26 -7
- data/lib/samagotchi/turn_anchors.rb +0 -3
- data/lib/samagotchi/turn_stages/verify_anchors.rb +13 -1
- data/lib/samagotchi/verify_anchor.rb +6 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +42 -77
- data/lib/samagotchi/web/participant_access.rb +129 -26
- data/lib/samagotchi/web/participant_images.rb +185 -0
- data/lib/samagotchi/web/participant_review.rb +19 -138
- data/lib/samagotchi/web/participant_show.rb +240 -0
- data/lib/samagotchi/web/participant_stream_filter.rb +25 -2
- data/lib/samagotchi/web/participant_view.rb +331 -24
- data/lib/samagotchi/web/public/annotate_bar.css +1 -2
- data/lib/samagotchi/web/public/annotate_bar.js +1 -2
- data/lib/samagotchi/web/public/app.js +124 -88
- data/lib/samagotchi/web/public/block_host.js +7 -4
- data/lib/samagotchi/web/public/bubbles.js +9 -1
- data/lib/samagotchi/web/public/cards_offscreen.js +69 -0
- data/lib/samagotchi/web/public/chip_popover.js +5 -4
- data/lib/samagotchi/web/public/claim_link.js +4 -4
- data/lib/samagotchi/web/public/composer.css +4 -6
- data/lib/samagotchi/web/public/composer_mode.js +11 -6
- data/lib/samagotchi/web/public/composer_mount.js +4 -4
- data/lib/samagotchi/web/public/ctx.js +12 -3
- data/lib/samagotchi/web/public/delivery_line.js +34 -5
- data/lib/samagotchi/web/public/history_shape.js +5 -1
- data/lib/samagotchi/web/public/images.js +7 -1
- data/lib/samagotchi/web/public/index.html +40 -4
- data/lib/samagotchi/web/public/info_bar_model.js +2 -1
- data/lib/samagotchi/web/public/markdown_place.js +5 -4
- data/lib/samagotchi/web/public/owner_claim.js +3 -3
- data/lib/samagotchi/web/public/owner_verify.js +0 -2
- data/lib/samagotchi/web/public/participant_bar.js +108 -0
- data/lib/samagotchi/web/public/participant_main.js +289 -0
- data/lib/samagotchi/web/public/participant_model.js +5 -292
- data/lib/samagotchi/web/public/participant_names.js +6 -1
- data/lib/samagotchi/web/public/participant_revise.js +66 -0
- data/lib/samagotchi/web/public/participants_panel.js +3 -12
- data/lib/samagotchi/web/public/question_cards.js +14 -3
- data/lib/samagotchi/web/public/refs.js +53 -36
- data/lib/samagotchi/web/public/reload_check.js +68 -11
- data/lib/samagotchi/web/public/revise_stack.js +21 -10
- data/lib/samagotchi/web/public/send_turn.js +24 -6
- data/lib/samagotchi/web/public/session_fields.js +1 -1
- data/lib/samagotchi/web/public/session_state.js +12 -4
- data/lib/samagotchi/web/public/session_sync.js +9 -2
- data/lib/samagotchi/web/public/session_view.js +17 -7
- data/lib/samagotchi/web/public/stage_asking.js +6 -4
- data/lib/samagotchi/web/public/stage_owner_wait.js +35 -0
- data/lib/samagotchi/web/public/stage_view.js +18 -4
- data/lib/samagotchi/web/public/stage_waiting.js +74 -6
- data/lib/samagotchi/web/public/storage.js +22 -0
- data/lib/samagotchi/web/public/stream_handlers.js +2 -1
- data/lib/samagotchi/web/public/tokens.css +1 -1
- data/lib/samagotchi/web/public/turn_block.js +3 -2
- data/lib/samagotchi/web/public/turn_events.js +15 -6
- data/lib/samagotchi/web/public/turn_html.js +9 -2
- data/lib/samagotchi/web/public/turn_range.js +1 -2
- data/lib/samagotchi/web/public/verify_target.js +2 -4
- data/lib/samagotchi/web/public/viewer.js +59 -0
- data/lib/samagotchi/web/public/viewer_self.js +53 -0
- data/lib/samagotchi/web/public/working_set.js +11 -5
- data/lib/samagotchi/web/public/working_set_view.js +53 -8
- data/lib/samagotchi/web/public/ws_cards.js +89 -0
- data/lib/samagotchi/web/public/ws_peek.js +106 -0
- data/lib/samagotchi/web/public/ws_preview.css +126 -0
- data/lib/samagotchi/web/public/ws_reader.js +319 -0
- data/lib/samagotchi/web/public/ws_timeline.js +200 -0
- data/lib/samagotchi/web/public/ws_way_in.js +25 -0
- data/lib/samagotchi/web/session_control.rb +39 -14
- data/lib/samagotchi/web/session_create.rb +1 -1
- data/lib/samagotchi/web/session_show.rb +75 -28
- data/lib/samagotchi/web/viewer.rb +70 -0
- data/lib/samagotchi/web/working_set_routes.rb +132 -0
- data/lib/samagotchi/worker.rb +19 -14
- data/lib/samagotchi/working_set.rb +260 -8
- metadata +20 -6
- data/lib/samagotchi/web/public/participant.html +0 -218
- data/lib/samagotchi/web/public/participant.js +0 -719
- data/lib/samagotchi/web/public/participant_annotate.js +0 -76
- data/lib/samagotchi/web/public/review_history.js +0 -72
- data/lib/samagotchi/web/public/tools_overview.js +0 -42
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "answer_tail"
|
|
4
|
+
require_relative "steer"
|
|
5
|
+
require_relative "verify_anchor"
|
|
6
|
+
|
|
7
|
+
module Samagotchi
|
|
8
|
+
# Whether a participant may Revise a turn: the rule chi web checks at the
|
|
9
|
+
# send (Web::ParticipantAccess#participant_revise_refusal) and the worker
|
|
10
|
+
# again at the stamp (TurnStages::VerifyAnchors#revision_target), from the
|
|
11
|
+
# author the server stamped on each opening, never the body's. A turn
|
|
12
|
+
# they may revise is one of their own, answered, not a Verify turn, with
|
|
13
|
+
# only their own turns after it. The owner's Revise is never checked here.
|
|
14
|
+
# See docs/internals/participants.md (Turns).
|
|
15
|
+
module ParticipantRevise
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# A turn's opening as the rule counts it: a prompt or a wake turn's
|
|
19
|
+
# note, never a stored `!shell` line (the owner's, which a participant
|
|
20
|
+
# doesn't see: ParticipantView drops it, so it counts for nothing).
|
|
21
|
+
def opening?(message) = Steer.turn_prompt?(message) && !Steer.shell_line?(message)
|
|
22
|
+
|
|
23
|
+
# Why +author+ ("p:<id>") may not revise the turn +turn_id+ in
|
|
24
|
+
# +messages+, or nil when they may:
|
|
25
|
+
# - :not_own: no opening carries that id, or it isn't theirs, has no
|
|
26
|
+
# answer, or is a Verify turn (a missing turn answers the same as
|
|
27
|
+
# someone else's: no existence oracle);
|
|
28
|
+
# - :overtaken: a turn opening after it isn't theirs (the owner's,
|
|
29
|
+
# another participant's, a wake turn's).
|
|
30
|
+
# +before+: the Revise turn's own id, whose opening and everything after
|
|
31
|
+
# it are ignored (the worker's check, the turn already in the messages).
|
|
32
|
+
# @return [Symbol, nil]
|
|
33
|
+
def refusal(messages, turn_id:, author:, before: nil)
|
|
34
|
+
list = Array(messages)
|
|
35
|
+
author = Steer.sender_author(author)
|
|
36
|
+
return :not_own unless author
|
|
37
|
+
|
|
38
|
+
start = list.rindex { |m| opening?(m) && field(m, :turn_id).to_s == turn_id.to_s }
|
|
39
|
+
return :not_own unless start && own_answered?(list, start, author)
|
|
40
|
+
|
|
41
|
+
list[(start + 1)..].each do |message|
|
|
42
|
+
next unless opening?(message)
|
|
43
|
+
break if before && field(message, :turn_id).to_s == before.to_s
|
|
44
|
+
return :overtaken unless Steer.sender_author(field(message, :author)) == author
|
|
45
|
+
end
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def own_answered?(list, start, author)
|
|
50
|
+
opening = list[start]
|
|
51
|
+
return false unless Steer.sender_author(field(opening, :author)) == author
|
|
52
|
+
return false if VerifyAnchor.marks?(field(opening, :verify))
|
|
53
|
+
|
|
54
|
+
# Its slice, as AnswerTail.turn_slice bounds one: up to the next prompt.
|
|
55
|
+
stop = list.each_index.find { |i| i > start && Steer.turn_prompt?(list[i]) } || list.size
|
|
56
|
+
list[start...stop].any? { |message| AnswerTail.answer?(message) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def field(message, key) = message[key] || message[key.to_s]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
require_relative "atomic_file"
|
|
6
|
+
require_relative "log"
|
|
7
|
+
|
|
8
|
+
module Samagotchi
|
|
9
|
+
# Which image files of a session's images/ a participant uploaded: the
|
|
10
|
+
# one thing that lets a participant name an image in their turn or read
|
|
11
|
+
# it back (chi web's ParticipantImages). ImageStore is content-addressed
|
|
12
|
+
# and keeps no metadata, so ownership is chi web's own record, written
|
|
13
|
+
# only here, never derived from a ref's source, a name or a message.
|
|
14
|
+
# Keyed by (file, grant): an owner's or another grant's identical image
|
|
15
|
+
# never changes hands. Also each grant's upload attempts, accepted or
|
|
16
|
+
# refused (Grants::MAX_IMAGE_ATTEMPTS).
|
|
17
|
+
#
|
|
18
|
+
# participant_images.json in the session folder (beside participants.json,
|
|
19
|
+
# not in images/, which the image route serves), written whole
|
|
20
|
+
# (AtomicFile). Callers serialize read-then-write themselves (chi web's
|
|
21
|
+
# @participant_images_mutex). A file that doesn't read fails closed: every
|
|
22
|
+
# grant is at quota and owns nothing (Log.warn); the owner can delete it.
|
|
23
|
+
# See docs/internals/participants.md.
|
|
24
|
+
module ParticipantUploads
|
|
25
|
+
FILE = "participant_images.json"
|
|
26
|
+
|
|
27
|
+
# One upload: +file+ is the ref's "images/<hash>.<ext>".
|
|
28
|
+
Entry = Data.define(:file, :grant_id, :at)
|
|
29
|
+
|
|
30
|
+
# What the file holds; +broken+ when it doesn't read (then every grant
|
|
31
|
+
# is at quota and at its attempts cap, and owns nothing). Read it once
|
|
32
|
+
# per check and ask it (#count, #attempts, #owned?).
|
|
33
|
+
State = Data.define(:entries, :attempts, :broken) do
|
|
34
|
+
def count(grant_id) = broken ? Float::INFINITY : entries.count { |entry| entry.grant_id == grant_id }
|
|
35
|
+
def attempts_of(grant_id) = broken ? Float::INFINITY : attempts.fetch(grant_id, 0)
|
|
36
|
+
def owned?(file, grant_id) = entries.any? { |entry| entry.file == file && entry.grant_id == grant_id }
|
|
37
|
+
end
|
|
38
|
+
BROKEN = State.new(entries: [].freeze, attempts: {}.freeze, broken: true)
|
|
39
|
+
|
|
40
|
+
module_function
|
|
41
|
+
|
|
42
|
+
# @return [State]
|
|
43
|
+
def read(dir)
|
|
44
|
+
path = File.join(dir, FILE)
|
|
45
|
+
return State.new(entries: [], attempts: {}, broken: false) unless File.exist?(path)
|
|
46
|
+
|
|
47
|
+
data = JSON.parse(File.read(path))
|
|
48
|
+
raise TypeError, "not an object" unless data.is_a?(Hash)
|
|
49
|
+
|
|
50
|
+
entries = Array(data["images"]).map do |hash|
|
|
51
|
+
Entry.new(file: hash.fetch("file").to_s, grant_id: hash.fetch("grant_id").to_s, at: hash["at"].to_s)
|
|
52
|
+
end
|
|
53
|
+
attempts = data["attempts"] || {}
|
|
54
|
+
raise TypeError, "attempts not an object" unless attempts.is_a?(Hash)
|
|
55
|
+
|
|
56
|
+
State.new(entries: entries, attempts: attempts.transform_values(&:to_i), broken: false)
|
|
57
|
+
rescue JSON::ParserError, SystemCallError, TypeError, KeyError, NoMethodError => e
|
|
58
|
+
Log.warn(:web, "participant_images_unreadable", file: path, msg: e.message)
|
|
59
|
+
BROKEN
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Distinct files +grant_id+ uploaded; a broken record reads as at quota.
|
|
63
|
+
def count(dir, grant_id) = read(dir).count(grant_id)
|
|
64
|
+
|
|
65
|
+
# Uploads +grant_id+ tried, refused ones counted; a broken record reads
|
|
66
|
+
# as at the cap.
|
|
67
|
+
def attempts(dir, grant_id) = read(dir).attempts_of(grant_id)
|
|
68
|
+
|
|
69
|
+
# Whether +grant_id+ uploaded +file+ ("images/<hash>.<ext>").
|
|
70
|
+
def owned?(dir, file, grant_id) = read(dir).owned?(file, grant_id)
|
|
71
|
+
|
|
72
|
+
# Records +file+ as +grant_id+'s (once: an owned file adds nothing).
|
|
73
|
+
# A broken record is left as it is. +state+: the record as the caller
|
|
74
|
+
# just read it (under its lock), else read here.
|
|
75
|
+
# @return [Boolean] whether an entry was added
|
|
76
|
+
def add(dir, file:, grant_id:, at: Time.now, state: read(dir))
|
|
77
|
+
return false if state.broken
|
|
78
|
+
return false if state.owned?(file, grant_id)
|
|
79
|
+
|
|
80
|
+
write(dir, state.entries + [Entry.new(file: file, grant_id: grant_id, at: at.utc.iso8601)], state.attempts)
|
|
81
|
+
true
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Counts one upload attempt of +grant_id+. A broken record is left as it
|
|
85
|
+
# is. +state+ as #add's.
|
|
86
|
+
def attempt!(dir, grant_id, state: read(dir))
|
|
87
|
+
return if state.broken
|
|
88
|
+
|
|
89
|
+
write(dir, state.entries, state.attempts.merge(grant_id => state.attempts.fetch(grant_id, 0) + 1))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def write(dir, entries, attempts)
|
|
93
|
+
body = { "images" => entries.map { |entry| entry.to_h.transform_keys(&:to_s) }, "attempts" => attempts }
|
|
94
|
+
AtomicFile.write(File.join(dir, FILE), "#{JSON.pretty_generate(body)}\n", perm: 0o600)
|
|
95
|
+
end
|
|
96
|
+
private_class_method :write
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -92,7 +92,11 @@ module Samagotchi
|
|
|
92
92
|
return unless @turn && !@claimed
|
|
93
93
|
|
|
94
94
|
waiting = SessionInbox.waiting_input_data(@session_dir, @turn)
|
|
95
|
-
|
|
95
|
+
# A participant's next-step message may go into their own running
|
|
96
|
+
# turn (its author's), so it runs a turn of its own only when it
|
|
97
|
+
# would wait even there.
|
|
98
|
+
@own_turn_input ||= !waiting.nil? &&
|
|
99
|
+
SessionInbox.turn_end_data?(waiting, running_author: Steer.sender_author(waiting["author"]))
|
|
96
100
|
@participant_input ||= !waiting.nil? && Steer.participant_author?(waiting["author"])
|
|
97
101
|
@claimed = waiting.nil?
|
|
98
102
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "participant_messages"
|
|
3
4
|
require_relative "verify_anchor"
|
|
4
5
|
|
|
5
6
|
module Samagotchi
|
|
@@ -26,6 +27,14 @@ module Samagotchi
|
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
ReviseAnchor::FEATURE = "revise"
|
|
30
|
+
# A worker that checks a participant's Revise (ParticipantRevise, at the
|
|
31
|
+
# stamp) and keeps their draft: what chi web needs to pass one on.
|
|
32
|
+
ReviseAnchor::PARTICIPANT_FEATURE = "participant_revise"
|
|
33
|
+
# A participant's Revise to a live worker without PARTICIPANT_FEATURE
|
|
34
|
+
# (SessionManager::Delivery), in their words, never UNAVAILABLE's owner ones.
|
|
35
|
+
ReviseAnchor::PARTICIPANT_UNAVAILABLE = {
|
|
36
|
+
"error" => "revise_unavailable", "detail" => ParticipantMessages::REVISE_UNAVAILABLE
|
|
37
|
+
}.freeze
|
|
29
38
|
ReviseAnchor::UNAVAILABLE = {
|
|
30
39
|
"error" => "revise_unavailable",
|
|
31
40
|
"detail" => "this session's worker predates Revise turns; restart this session to use Revise"
|
|
@@ -221,6 +221,11 @@ module Samagotchi
|
|
|
221
221
|
|
|
222
222
|
UNKNOWN_VISION = VisionSupport::Answer.new(value: nil, reason: nil)
|
|
223
223
|
|
|
224
|
+
# .vision_refusal for +model_name+ (+typed+: as the user typed it) on
|
|
225
|
+
# .vision_answer's answer: chi web's turn and create routes and a
|
|
226
|
+
# participant's turn ask this one helper.
|
|
227
|
+
def self.vision_refusal_for(model_name, typed: nil) = vision_refusal(model_name, vision_answer(model_name, typed: typed))
|
|
228
|
+
|
|
224
229
|
# What to say when +answer+ (.vision_answer's for +model_name+) is a
|
|
225
230
|
# no, or nil: the words chi send and chi web's turn and create routes
|
|
226
231
|
# refuse images with.
|
|
@@ -313,15 +313,18 @@ module Samagotchi
|
|
|
313
313
|
# Whether a mid-turn drain leaves this unclaimed input file for a turn
|
|
314
314
|
# of its own instead of merging it at a step boundary: a queued message
|
|
315
315
|
# (`delivery: queue`), an image message (steering merges text only), or
|
|
316
|
-
#
|
|
317
|
-
# own turn, never
|
|
318
|
-
#
|
|
316
|
+
# one whose author isn't the running turn's: a participant's goes into
|
|
317
|
+
# their own running turn only, never another author's, and nobody's
|
|
318
|
+
# goes into a participant's but theirs (D6; the Bridge queues it, and
|
|
319
|
+
# this is the worker's own layer). +running_author+: the running turn's
|
|
320
|
+
# participant author (TurnOrigin#participant_author), nil for the
|
|
321
|
+
# owner's.
|
|
319
322
|
# The drain skips such a file without stopping at it, so a later text
|
|
320
323
|
# line still merges; the drain's file filter reads this one predicate,
|
|
321
324
|
# so what a boundary leaves changes in one place.
|
|
322
|
-
def self.waits_for_turn_end?(input_file)
|
|
325
|
+
def self.waits_for_turn_end?(input_file, running_author: nil)
|
|
323
326
|
data = JSON.parse(File.read(input_file))
|
|
324
|
-
data.is_a?(Hash) && turn_end_data?(data)
|
|
327
|
+
data.is_a?(Hash) && turn_end_data?(data, running_author: running_author)
|
|
325
328
|
rescue JSON::ParserError, SystemCallError
|
|
326
329
|
false
|
|
327
330
|
end
|
|
@@ -339,10 +342,13 @@ module Samagotchi
|
|
|
339
342
|
|
|
340
343
|
# A parsed input file's #waits_for_turn_end?. A Verify or a Revise turn
|
|
341
344
|
# waits too: merged into a running turn as a steer, its anchor would be
|
|
342
|
-
# lost.
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
345
|
+
# lost. Without +running_author+ (the owner's turn, or none named) a
|
|
346
|
+
# participant's file waits, as before.
|
|
347
|
+
def self.turn_end_data?(data, running_author: nil)
|
|
348
|
+
return true if Array(data["images"]).any? || Delivery.queue?(data["delivery"]) || TurnAnchors.named_in?(data)
|
|
349
|
+
|
|
350
|
+
author = Steer.sender_author(data["author"])
|
|
351
|
+
running_author ? author != running_author : Steer.participant_author?(data["author"])
|
|
346
352
|
end
|
|
347
353
|
|
|
348
354
|
# Whether a message waits that a running turn's next boundary merges and
|
|
@@ -352,11 +358,13 @@ module Samagotchi
|
|
|
352
358
|
# ends on it (Worker#input_waiting?). It only peeks: nothing is claimed;
|
|
353
359
|
# a file the drain or a dequeue claims meanwhile (ENOENT) or one that
|
|
354
360
|
# doesn't parse is skipped.
|
|
355
|
-
|
|
361
|
+
# In a participant's turn (+running_author+) only their own message
|
|
362
|
+
# counts.
|
|
363
|
+
def self.steer_waiting?(session_dir, running_author: nil)
|
|
356
364
|
find_new_input_files(session_dir).any? do |file|
|
|
357
365
|
data = JSON.parse(File.read(file))
|
|
358
|
-
data.is_a?(Hash) && !turn_end_data?(data) && !data["prompt"].to_s.strip.empty? &&
|
|
359
|
-
Steer.cuts?(Steer.source_for_client(data["client_id"]))
|
|
366
|
+
data.is_a?(Hash) && !turn_end_data?(data, running_author: running_author) && !data["prompt"].to_s.strip.empty? &&
|
|
367
|
+
(running_author || Steer.cuts?(Steer.source_for_client(data["client_id"])))
|
|
360
368
|
rescue JSON::ParserError, SystemCallError
|
|
361
369
|
false
|
|
362
370
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../turn_anchors"
|
|
4
|
+
|
|
3
5
|
module Samagotchi
|
|
4
6
|
class SessionManager
|
|
5
7
|
# How long a turn waits for the Bridge of a worker a resume just spawned.
|
|
@@ -221,6 +223,11 @@ module Samagotchi
|
|
|
221
223
|
return queue_unavailable if Delivery.queue?(delivery) && !worker_has?(client, Delivery::QUEUE_FEATURE)
|
|
222
224
|
# A participant's turn only to a worker with the review boundary.
|
|
223
225
|
return review_unavailable if Steer.participant_author?(author) && !worker_has?(client, Delivery::REVIEW_FEATURE)
|
|
226
|
+
# A participant's Revise only to a worker that checks it at the
|
|
227
|
+
# stamp (ParticipantRevise): an older one would stamp it unchecked.
|
|
228
|
+
if Steer.participant_author?(author) && anchors.revises && !worker_has?(client, ReviseAnchor::PARTICIPANT_FEATURE)
|
|
229
|
+
return DeliverResult.new(status: :refused, code: 409, ack: ReviseAnchor::PARTICIPANT_UNAVAILABLE)
|
|
230
|
+
end
|
|
224
231
|
|
|
225
232
|
begin
|
|
226
233
|
options = { prompt: prompt, client_id: client_id, enqueued_id: enqueued_id }
|
|
@@ -27,6 +27,8 @@ module Samagotchi
|
|
|
27
27
|
continue: :context, corpus_dir: CONTEXT),
|
|
28
28
|
Store.new(file: "participants.json", owner: "Participants", removal: :participants, cross_version: true,
|
|
29
29
|
continue: "a continue ends the previous link's review", corpus_dir: SIDE_CAR),
|
|
30
|
+
Store.new(file: "participant_images.json", owner: "ParticipantUploads", removal: :session_files, cross_version: false,
|
|
31
|
+
continue: "a continue ends the previous link's review", corpus_dir: nil),
|
|
30
32
|
Store.new(file: "judgments.jsonl", owner: "SystemOne::RunsStore", removal: :system_one_runs, cross_version: false,
|
|
31
33
|
continue: "saved judgments belong to the previous link", corpus_dir: nil),
|
|
32
34
|
Store.new(file: "context_candidates.jsonl", owner: "ContextCandidatesLog", removal: :context_candidates_log,
|
|
@@ -29,7 +29,9 @@ module Samagotchi
|
|
|
29
29
|
# loaded, stamp) are nil. +loaded+: the tools a find_tools hit loads
|
|
30
30
|
# (Tools::ToolIndex::FoundText#loaded), by their wire names. +stamp+: an
|
|
31
31
|
# ok read/edit/write's file right after it ran ({path:, mtime_ns:,
|
|
32
|
-
# size:}), what WorkingSet tells a file changed since by
|
|
32
|
+
# size:}), what WorkingSet tells a file changed since by; for an ok
|
|
33
|
+
# memory_read, one entry per name it read, with the file that answered and
|
|
34
|
+
# the model overlay read with it (ToolRunner#memory_stamp).
|
|
33
35
|
ToolRun = Data.define(:output, :capped_output, :truncated, :activity, :images, :shown_params, :shown_label, :refs, :diff,
|
|
34
36
|
:output_file, :loaded, :stamp) do
|
|
35
37
|
def initialize(output: nil, capped_output: nil, truncated: nil, activity: nil, images: nil, shown_params: nil,
|
|
@@ -205,7 +207,8 @@ module Samagotchi
|
|
|
205
207
|
images: images&.any? ? images : nil,
|
|
206
208
|
shown_params: params && plugin_tool?(call[:name]) ? params : nil,
|
|
207
209
|
shown_label: label, refs: refs.empty? ? nil : refs, diff: diff, output_file: output_file,
|
|
208
|
-
loaded: result[:loaded],
|
|
210
|
+
loaded: result[:loaded],
|
|
211
|
+
stamp: status == "ok" ? file_stamp(call, context) || memory_stamp(result) : nil)
|
|
209
212
|
end
|
|
210
213
|
|
|
211
214
|
private
|
|
@@ -216,12 +219,43 @@ module Samagotchi
|
|
|
216
219
|
# can't place and a stat that fails or isn't a file's.
|
|
217
220
|
def file_stamp(call, context)
|
|
218
221
|
ref = FileRef.for(call[:name], call, cwd: session_cwd(context))
|
|
219
|
-
|
|
222
|
+
ref ? stat_entry(ref.path) : nil
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# A memory_read's stamp, one entry per name it read: the file that
|
|
226
|
+
# answered it, and the model overlay read with it when there was one, as
|
|
227
|
+
# they are right after the call:
|
|
228
|
+
# {memories: {name => {path:, mtime_ns:, size:, overlay: {path:, mtime_ns:,
|
|
229
|
+
# size:}}}} (no +overlay+ key when no overlay was read). Nothing at the
|
|
230
|
+
# top carries mtime_ns, so a reader that knows only file stamps
|
|
231
|
+
# (WorkingSet.stamp_of) sees none here, and a session saved before memory
|
|
232
|
+
# stamps has none: either way its memories are never stale (compat N-1).
|
|
233
|
+
# nil when the read answered no name (the index listing, every name
|
|
234
|
+
# missing), or none of its files can be stat'd.
|
|
235
|
+
def memory_stamp(result)
|
|
236
|
+
reads = result[:memory_reads]
|
|
237
|
+
return nil unless reads.is_a?(Hash)
|
|
238
|
+
|
|
239
|
+
entries = reads.filter_map do |name, read|
|
|
240
|
+
entry = stat_entry(read[:path])
|
|
241
|
+
next if entry.nil?
|
|
242
|
+
|
|
243
|
+
overlay = stat_entry(read[:overlay_path])
|
|
244
|
+
entry[:overlay] = overlay if overlay
|
|
245
|
+
[name, entry]
|
|
246
|
+
end
|
|
247
|
+
entries.empty? ? nil : { memories: entries.to_h }
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# A file as it is now: {path:, mtime_ns:, size:}; nil for a path that is
|
|
251
|
+
# not a file or cannot be stat'd.
|
|
252
|
+
def stat_entry(path)
|
|
253
|
+
return nil if path.to_s.empty?
|
|
220
254
|
|
|
221
|
-
stat = File.stat(
|
|
255
|
+
stat = File.stat(path)
|
|
222
256
|
return nil unless stat.file?
|
|
223
257
|
|
|
224
|
-
{ path:
|
|
258
|
+
{ path: path, mtime_ns: (stat.mtime.to_i * 1_000_000_000) + stat.mtime.nsec, size: stat.size }
|
|
225
259
|
rescue SystemCallError
|
|
226
260
|
nil
|
|
227
261
|
end
|
|
@@ -25,37 +25,53 @@ module Samagotchi
|
|
|
25
25
|
|
|
26
26
|
SEPARATOR = "\n\n---\n\n"
|
|
27
27
|
|
|
28
|
+
# A memory_read's text, and which file answered each name it read
|
|
29
|
+
# (+reads+: name => {path:, overlay_path:}, absolute paths, the overlay
|
|
30
|
+
# only when one was read with the entry). A String, so every reader of
|
|
31
|
+
# the result reads the same bytes the model does; ToolRunner turns
|
|
32
|
+
# +reads+ into the run's stamp (ToolRunner#memory_stamp), which is what
|
|
33
|
+
# later tells a memory the model read from one whose file changed after.
|
|
34
|
+
class Result < String
|
|
35
|
+
attr_reader :reads
|
|
36
|
+
|
|
37
|
+
def initialize(text, reads = {})
|
|
38
|
+
super(text)
|
|
39
|
+
@reads = reads
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
28
43
|
# @param fallback_model_key [String, nil] read when +model_key+ has no overlay
|
|
29
44
|
def self.call(entry_name, scope: nil, model_key: nil, fallback_model_key: nil)
|
|
30
45
|
entry_name = entry_name.to_s.strip
|
|
31
46
|
scope = normalize_scope(scope)
|
|
32
47
|
|
|
33
48
|
if entry_name.empty?
|
|
34
|
-
return scoped_index(scope) + (scope == "project" ? orphan_hint : "") if scope
|
|
49
|
+
return Result.new(scoped_index(scope) + (scope == "project" ? orphan_hint : "")) if scope
|
|
35
50
|
|
|
36
51
|
project = scoped_index("project") + orphan_hint
|
|
37
52
|
system = scoped_index("system")
|
|
38
|
-
return [
|
|
53
|
+
return Result.new([
|
|
39
54
|
"Project memories:",
|
|
40
55
|
project,
|
|
41
56
|
"",
|
|
42
57
|
"System memories:",
|
|
43
58
|
system
|
|
44
|
-
].join("\n")
|
|
59
|
+
].join("\n"))
|
|
45
60
|
end
|
|
46
61
|
|
|
47
62
|
names = parse_names(entry_name)
|
|
48
63
|
|
|
49
64
|
if names.empty?
|
|
50
|
-
return "Error: no memory names provided"
|
|
65
|
+
return Result.new("Error: no memory names provided")
|
|
51
66
|
end
|
|
52
67
|
|
|
53
68
|
bad = names.find { |name| invalid_name?(name) }
|
|
54
|
-
return invalid_name_error(bad) if bad
|
|
69
|
+
return Result.new(invalid_name_error(bad)) if bad
|
|
55
70
|
|
|
56
71
|
resolved_scopes = scope ? [scope] : %w[project system]
|
|
57
72
|
|
|
58
73
|
results = []
|
|
74
|
+
reads = {}
|
|
59
75
|
missing = []
|
|
60
76
|
|
|
61
77
|
names.each do |name|
|
|
@@ -65,27 +81,30 @@ module Samagotchi
|
|
|
65
81
|
next unless File.exist?(path)
|
|
66
82
|
|
|
67
83
|
body = File.read(path, encoding: "UTF-8")
|
|
84
|
+
overlay = nil
|
|
68
85
|
# Append model-specific overlay if key is provided and overlay exists.
|
|
69
86
|
[model_key, fallback_model_key].compact.each do |key|
|
|
70
87
|
overlay_path = ModelOverlay.overlay_path_for(name, key, resolved_scope)
|
|
71
88
|
next unless overlay_path && File.exist?(overlay_path)
|
|
72
89
|
|
|
73
90
|
body += SEPARATOR + "Model-specific guidance (#{key}):\n" + File.read(overlay_path, encoding: "UTF-8")
|
|
91
|
+
overlay = overlay_path
|
|
74
92
|
break
|
|
75
93
|
end
|
|
76
94
|
results << body
|
|
95
|
+
reads[name] = { path: path, overlay_path: overlay }
|
|
77
96
|
found = true
|
|
78
97
|
break
|
|
79
98
|
end
|
|
80
99
|
missing << name unless found
|
|
81
100
|
end
|
|
82
101
|
|
|
83
|
-
return results.join(SEPARATOR) if missing.empty?
|
|
102
|
+
return Result.new(results.join(SEPARATOR), reads) if missing.empty?
|
|
84
103
|
|
|
85
104
|
# The hint goes after the first line: WorkingSet reads the missing
|
|
86
105
|
# names off it (WorkingSet::MISSING) and a failure off its "Error".
|
|
87
106
|
miss = "Error: memory not found: #{missing.join(", ")}#{orphan_hint if resolved_scopes.include?("project")}"
|
|
88
|
-
results.empty? ? miss : results.join(SEPARATOR) + SEPARATOR + miss
|
|
107
|
+
results.empty? ? Result.new(miss) : Result.new(results.join(SEPARATOR) + SEPARATOR + miss, reads)
|
|
89
108
|
rescue Errno::ENOENT => e
|
|
90
109
|
"Error: #{e.message}"
|
|
91
110
|
rescue StandardError => e
|
|
@@ -4,6 +4,7 @@ require "time"
|
|
|
4
4
|
require_relative "../answer_tail"
|
|
5
5
|
require_relative "../log"
|
|
6
6
|
require_relative "../output_formatter"
|
|
7
|
+
require_relative "../participant_revise"
|
|
7
8
|
require_relative "../redact"
|
|
8
9
|
require_relative "../revise_anchor"
|
|
9
10
|
require_relative "../session"
|
|
@@ -90,7 +91,12 @@ module Samagotchi
|
|
|
90
91
|
# anything. A draft no prompt in the messages carries is no target (a
|
|
91
92
|
# stale page, a rolled-back turn); one a stamped turn already revised
|
|
92
93
|
# is re-targeted to its stack's top, the newest version, which the
|
|
93
|
-
# person meant ("this draft").
|
|
94
|
+
# person meant ("this draft"). A participant's draft first meets
|
|
95
|
+
# ParticipantRevise (theirs, with only their own turns after it, up
|
|
96
|
+
# to this turn): chi web checked it at the send, and this is the
|
|
97
|
+
# check at the stamp, so a turn someone else's overtook since runs
|
|
98
|
+
# as a plain turn (logged). The re-target walk then stays inside
|
|
99
|
+
# their own turns.
|
|
94
100
|
def revision_target(messages, turn, outcome)
|
|
95
101
|
return unless turn.revises && outcome == "completed"
|
|
96
102
|
|
|
@@ -98,6 +104,12 @@ module Samagotchi
|
|
|
98
104
|
return unless slice&.any? { |message| AnswerTail.answer?(message) }
|
|
99
105
|
|
|
100
106
|
target = turn.revises.turn_id
|
|
107
|
+
author = turn.origin&.author
|
|
108
|
+
if Steer.participant_author?(author) &&
|
|
109
|
+
(refused = ParticipantRevise.refusal(messages, turn_id: target, author: author, before: turn.id))
|
|
110
|
+
return reject_revise(turn.revises, refused)
|
|
111
|
+
end
|
|
112
|
+
|
|
101
113
|
seen = [turn.id]
|
|
102
114
|
until seen.include?(target)
|
|
103
115
|
return reject_revise(turn.revises, :turn_missing) unless AnswerTail.turn_slice(messages, target)
|
|
@@ -32,5 +32,11 @@ module Samagotchi
|
|
|
32
32
|
"detail" => "this session's worker predates Verify turns; restart this session to use Verify"
|
|
33
33
|
}.freeze
|
|
34
34
|
|
|
35
|
+
# Whether a turn opening's +verify+ marks a Verify turn: a well-formed
|
|
36
|
+
# claim (#from), or the display's `verify: true` (MessageDisplay). One
|
|
37
|
+
# test for ParticipantRevise and the participant's view, so a malformed
|
|
38
|
+
# one reads the same to both (not a Verify turn).
|
|
39
|
+
def VerifyAnchor.marks?(value) = value == true || !VerifyAnchor.from(value).nil?
|
|
40
|
+
|
|
35
41
|
def VerifyAnchor.worker_unavailable?(features) = !features.nil? && !features.include?(VerifyAnchor::FEATURE)
|
|
36
42
|
end
|