samagotchi 0.134.0 → 0.135.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.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -1
  3. data/bin/chi +2 -3
  4. data/docs/architecture.md +6 -3
  5. data/docs/cli.md +39 -34
  6. data/docs/configuration.md +1 -2
  7. data/docs/guardrails.md +53 -9
  8. data/docs/internals/engine.md +6 -3
  9. data/docs/internals/participants.md +1 -1
  10. data/docs/internals/web-frontend.md +262 -0
  11. data/docs/sessions.md +2 -0
  12. data/docs/testing.md +27 -2
  13. data/lib/samagotchi/bridge.rb +11 -6
  14. data/lib/samagotchi/bridge_client.rb +8 -5
  15. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  16. data/lib/samagotchi/config.rb +0 -2
  17. data/lib/samagotchi/engine.rb +69 -12
  18. data/lib/samagotchi/guardrail_wiring.rb +8 -6
  19. data/lib/samagotchi/guardrails/approval.rb +41 -5
  20. data/lib/samagotchi/guardrails/implicit_writers.rb +19 -4
  21. data/lib/samagotchi/guardrails/pair_boundary.rb +5 -1
  22. data/lib/samagotchi/guardrails/pair_command.rb +387 -78
  23. data/lib/samagotchi/guardrails/parent_approvals.rb +12 -15
  24. data/lib/samagotchi/guardrails/read_only_shell.rb +24 -2
  25. data/lib/samagotchi/guardrails/shared_git.rb +1 -1
  26. data/lib/samagotchi/guardrails/shell_git_dirs.rb +1 -1
  27. data/lib/samagotchi/guardrails/shell_lex.rb +22 -0
  28. data/lib/samagotchi/guardrails_command.rb +13 -10
  29. data/lib/samagotchi/llm/chat_loop.rb +4 -3
  30. data/lib/samagotchi/message_parts.rb +1 -1
  31. data/lib/samagotchi/revise_anchor.rb +33 -0
  32. data/lib/samagotchi/session_inbox.rb +20 -12
  33. data/lib/samagotchi/session_manager.rb +13 -11
  34. data/lib/samagotchi/system_one/approval_question.rb +21 -16
  35. data/lib/samagotchi/system_one/self_protect.rb +13 -15
  36. data/lib/samagotchi/tools/delegate_relay.rb +6 -5
  37. data/lib/samagotchi/tools/delegate_wait.rb +5 -5
  38. data/lib/samagotchi/tools/execute.rb +6 -6
  39. data/lib/samagotchi/tools/output_guardrails.rb +9 -2
  40. data/lib/samagotchi/tools/read.rb +5 -5
  41. data/lib/samagotchi/turn_anchors.rb +49 -0
  42. data/lib/samagotchi/verify_anchor.rb +5 -5
  43. data/lib/samagotchi/version.rb +1 -1
  44. data/lib/samagotchi/web/app.rb +2 -14
  45. data/lib/samagotchi/web/message_display.rb +17 -1
  46. data/lib/samagotchi/web/participant_access.rb +2 -2
  47. data/lib/samagotchi/web/participant_view.rb +8 -0
  48. data/lib/samagotchi/web/public/activity.js +1 -1
  49. data/lib/samagotchi/web/public/annotations.js +1 -1
  50. data/lib/samagotchi/web/public/answer_fold.js +3 -3
  51. data/lib/samagotchi/web/public/app.js +447 -340
  52. data/lib/samagotchi/web/public/bubbles.js +3 -3
  53. data/lib/samagotchi/web/public/cards_place.js +4 -2
  54. data/lib/samagotchi/web/public/composer.css +8 -0
  55. data/lib/samagotchi/web/public/context_chips.js +9 -6
  56. data/lib/samagotchi/web/public/data.js +4 -2
  57. data/lib/samagotchi/web/public/dock_reserve.js +13 -15
  58. data/lib/samagotchi/web/public/feature_gates.js +33 -0
  59. data/lib/samagotchi/web/public/history_reveal.js +8 -4
  60. data/lib/samagotchi/web/public/index.html +38 -9
  61. data/lib/samagotchi/web/public/latest.js +46 -0
  62. data/lib/samagotchi/web/public/lists_view.js +15 -13
  63. data/lib/samagotchi/web/public/live_timing.js +2 -0
  64. data/lib/samagotchi/web/public/markdown_place.js +1 -1
  65. data/lib/samagotchi/web/public/method_api.js +14 -0
  66. data/lib/samagotchi/web/public/notifications.js +4 -11
  67. data/lib/samagotchi/web/public/participant.js +11 -6
  68. data/lib/samagotchi/web/public/participant_names.js +1 -1
  69. data/lib/samagotchi/web/public/participants_panel.js +4 -2
  70. data/lib/samagotchi/web/public/project_picker.js +18 -22
  71. data/lib/samagotchi/web/public/question_cards.js +2 -2
  72. data/lib/samagotchi/web/public/restore.js +2 -0
  73. data/lib/samagotchi/web/public/revise_stack.js +372 -0
  74. data/lib/samagotchi/web/public/scope.js +12 -20
  75. data/lib/samagotchi/web/public/send_turn.js +29 -16
  76. data/lib/samagotchi/web/public/session_actions.js +15 -14
  77. data/lib/samagotchi/web/public/session_fields.js +152 -0
  78. data/lib/samagotchi/web/public/session_payload.js +35 -0
  79. data/lib/samagotchi/web/public/session_state.js +2 -0
  80. data/lib/samagotchi/web/public/session_store.js +191 -0
  81. data/lib/samagotchi/web/public/stage_model.js +4 -4
  82. data/lib/samagotchi/web/public/stage_view.js +11 -16
  83. data/lib/samagotchi/web/public/stream_handlers.js +88 -90
  84. data/lib/samagotchi/web/public/task_stop.js +1 -1
  85. data/lib/samagotchi/web/public/timing.js +2 -2
  86. data/lib/samagotchi/web/public/{turn_view.js → turn_block.js} +27 -92
  87. data/lib/samagotchi/web/public/turn_events.js +2 -2
  88. data/lib/samagotchi/web/public/turn_html.js +5 -3
  89. data/lib/samagotchi/web/public/turn_model.js +6 -6
  90. data/lib/samagotchi/web/public/verdict_fresh.js +2 -0
  91. data/lib/samagotchi/web/public/working_set_view.js +3 -1
  92. data/lib/samagotchi/web/server.rb +4 -4
  93. data/lib/samagotchi/web/session_control.rb +2 -2
  94. data/lib/samagotchi/worker.rb +9 -9
  95. data/lib/samagotchi/worker_inbound.rb +6 -5
  96. metadata +12 -3
  97. data/lib/samagotchi/web/public/hold.js +0 -78
@@ -192,7 +192,7 @@ module Samagotchi
192
192
  # of a relay), else its hook; an approval's call (#what_text).
193
193
  # @param question [PendingQuestion] the child's
194
194
  def what(question)
195
- return what_text(symbolized(question.approval)) unless question.kind.to_s == HOOK_KIND
195
+ return what_text(Guardrails::ApprovalFacts.from_h(question.approval)) unless question.kind.to_s == HOOK_KIND
196
196
 
197
197
  inner = symbolized(question.relay, nil)
198
198
  header = (inner ? inner[:header] : question.header).to_s
@@ -204,11 +204,12 @@ module Samagotchi
204
204
  end
205
205
 
206
206
  # execute: git push origin main
207
+ # @param facts [Guardrails::ApprovalFacts, nil] the saved card's
207
208
  def what_text(facts)
208
- facts = {} unless facts.is_a?(Hash)
209
- tool = facts[:label] || facts[:tool] || "tool"
210
- what = facts[:command] || Array(facts[:paths]).join(", ")
211
- what = facts[:args].to_s if what.to_s.empty?
209
+ facts ||= Guardrails::ApprovalFacts.new
210
+ tool = facts.label || facts.tool || "tool"
211
+ what = facts.command || Array(facts.paths).join(", ")
212
+ what = facts.args.to_s if what.to_s.empty?
212
213
  what.to_s.empty? ? tool.to_s : "#{tool}: #{what}"
213
214
  end
214
215
 
@@ -337,13 +337,13 @@ module Samagotchi
337
337
  "truncated=true",
338
338
  "preview_strategy=head_tail",
339
339
  "reply_bytes=#{text.bytesize}",
340
- "returned_preview_bytes=#{preview[:returned_preview_bytes]}",
341
- "omitted_bytes=#{preview[:omitted_bytes]}",
340
+ "returned_preview_bytes=#{preview.returned_preview_bytes}",
341
+ "omitted_bytes=#{preview.omitted_bytes}",
342
342
  "[TRUNCATED_PREVIEW_HEAD]",
343
- preview[:head],
344
- "[... omitted #{preview[:omitted_bytes]} bytes ...]",
343
+ preview.head,
344
+ "[... omitted #{preview.omitted_bytes} bytes ...]",
345
345
  "[TRUNCATED_PREVIEW_TAIL]",
346
- preview[:tail]
346
+ preview.tail
347
347
  ].join("\n")
348
348
  end
349
349
  end
@@ -469,13 +469,13 @@ module Samagotchi
469
469
  "truncated=true",
470
470
  "preview_strategy=head_tail",
471
471
  "#{label}_bytes=#{bytes}",
472
- "returned_preview_bytes=#{preview[:returned_preview_bytes]}",
473
- "omitted_bytes=#{preview[:omitted_bytes]}",
472
+ "returned_preview_bytes=#{preview.returned_preview_bytes}",
473
+ "omitted_bytes=#{preview.omitted_bytes}",
474
474
  "[TRUNCATED_PREVIEW_HEAD]",
475
- preview[:head],
476
- "[... omitted #{preview[:omitted_bytes]} bytes ...]",
475
+ preview.head,
476
+ "[... omitted #{preview.omitted_bytes} bytes ...]",
477
477
  "[TRUNCATED_PREVIEW_TAIL]",
478
- preview[:tail]
478
+ preview.tail
479
479
  ].join("\n")
480
480
  end
481
481
 
@@ -492,7 +492,7 @@ module Samagotchi
492
492
  # A big log: its size, and the head and tail read by seek, which
493
493
  # output_block shows as it shows a long string's.
494
494
  class LogPreview
495
- attr_reader :bytesize, :preview
495
+ attr_reader :bytesize, :preview # Integer, OutputGuardrails::PreviewParts
496
496
 
497
497
  def initialize(bytesize, preview)
498
498
  @bytesize = bytesize
@@ -11,6 +11,11 @@ module Samagotchi
11
11
  DEFAULT_TELEMETRY_THRESHOLD_PCT = 80.0
12
12
  DEFAULT_CHARS_PER_TOKEN = 4.0
13
13
 
14
+ # A large output's head and tail (UTF-8 safe) with the byte counts the
15
+ # truncated-preview result lines report.
16
+ PreviewParts = Data.define(:head, :tail, :head_bytes, :tail_bytes, :returned_preview_bytes,
17
+ :omitted_bytes, :total_bytes)
18
+
14
19
  module_function
15
20
 
16
21
  # The config setting +key+ (read.*, execute.*, context.*) when positive, else +default+.
@@ -41,6 +46,7 @@ module Samagotchi
41
46
  .encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "?")
42
47
  end
43
48
 
49
+ # @return [PreviewParts]
44
50
  def head_tail_from_file(path:, file_size:, preview_bytes:)
45
51
  half = [preview_bytes / 2, 1].max
46
52
  head = File.binread(path, half, 0)
@@ -50,6 +56,7 @@ module Samagotchi
50
56
  build_preview_parts(head: head, tail: tail, total_bytes: file_size)
51
57
  end
52
58
 
59
+ # @return [PreviewParts]
53
60
  def head_tail_from_string(content:, preview_bytes:)
54
61
  half = [preview_bytes / 2, 1].max
55
62
  head = content.byteslice(0, half) || ""
@@ -78,7 +85,7 @@ module Samagotchi
78
85
  tail_bytes = tail.bytesize
79
86
  omitted_bytes = [total_bytes - (head_bytes + tail_bytes), 0].max
80
87
 
81
- {
88
+ PreviewParts.new(
82
89
  head: safe_utf8(head),
83
90
  tail: safe_utf8(tail),
84
91
  head_bytes: head_bytes,
@@ -86,7 +93,7 @@ module Samagotchi
86
93
  returned_preview_bytes: head_bytes + tail_bytes,
87
94
  omitted_bytes: omitted_bytes,
88
95
  total_bytes: total_bytes
89
- }
96
+ )
90
97
  end
91
98
  private_class_method :build_preview_parts
92
99
  end
@@ -81,10 +81,10 @@ module Samagotchi
81
81
 
82
82
  preview_block = [
83
83
  "[TRUNCATED_PREVIEW_HEAD]",
84
- preview[:head],
85
- "[... omitted #{preview[:omitted_bytes]} bytes ...]",
84
+ preview.head,
85
+ "[... omitted #{preview.omitted_bytes} bytes ...]",
86
86
  "[TRUNCATED_PREVIEW_TAIL]",
87
- preview[:tail]
87
+ preview.tail
88
88
  ].join("\n")
89
89
 
90
90
  metadata_lines = [
@@ -92,8 +92,8 @@ module Samagotchi
92
92
  "path=#{path}",
93
93
  "preview_strategy=head_tail",
94
94
  "file_bytes=#{file_size}",
95
- "returned_preview_bytes=#{preview[:returned_preview_bytes]}",
96
- "omitted_bytes=#{preview[:omitted_bytes]}"
95
+ "returned_preview_bytes=#{preview.returned_preview_bytes}",
96
+ "omitted_bytes=#{preview.omitted_bytes}"
97
97
  ]
98
98
 
99
99
  telemetry = telemetry_lines_for(preview_block)
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "revise_anchor"
4
+ require_relative "verify_anchor"
5
+
6
+ module Samagotchi
7
+ # What a turn points at in an earlier turn: a Verify turn's claim
8
+ # (VerifyAnchor) and a Revise turn's draft (ReviseAnchor). One carrier
9
+ # through every layer a turn's input travels (chi web's POST /turn, the
10
+ # Bridge, the input file, SessionInbox::Input, WorkerInbound::Prompt, a
11
+ # failed turn's restore), so a new anchor is one field here. On the wire
12
+ # and in the input file each is its own key ("verify", "revises"); a
13
+ # missing or malformed one is nil. The Engine takes them as run_turn's
14
+ # keywords (#kwargs): what each means is its own.
15
+ TurnAnchors = Data.define(:verify, :revises) do
16
+ def self.from(value)
17
+ return value if value.is_a?(self)
18
+
19
+ fields = value.is_a?(Hash) ? value.to_h { |key, entry| [key.to_s, entry] } : {}
20
+ new(verify: fields["verify"], revises: fields["revises"])
21
+ end
22
+
23
+ # Whether a parsed input file names an anchor (a Hash under its key,
24
+ # well-formed or not): SessionInbox.turn_end_data? holds it.
25
+ def self.named_in?(data) = %w[verify revises].any? { |key| data[key].is_a?(Hash) }
26
+
27
+ def initialize(verify: nil, revises: nil)
28
+ super(verify: VerifyAnchor.from(verify), revises: ReviseAnchor.from(revises))
29
+ end
30
+
31
+ def empty? = verify.nil? && revises.nil?
32
+
33
+ # The anchors given, each answering FEATURE (the worker's Bridge
34
+ # feature it needs) and UNAVAILABLE (the refusal without it).
35
+ def present = [verify, revises].compact
36
+
37
+ # The body's and the input file's keys.
38
+ def to_file = { "verify" => verify&.to_file, "revises" => revises&.to_file }.compact
39
+
40
+ # A :prompt_restored event's fields.
41
+ def to_h = { verify: verify&.to_h, revises: revises&.to_h }.compact
42
+
43
+ # Engine#run_turn's keywords.
44
+ def kwargs = { verify:, revises: }.compact
45
+
46
+ # A participant's: they can't Revise (participant_access.rb, the Bridge).
47
+ def without_revises = with(revises: nil)
48
+ end
49
+ end
@@ -3,9 +3,6 @@
3
3
  module Samagotchi
4
4
  # A claim selected in an earlier turn, carried only by a Verify turn.
5
5
  VerifyAnchor = Data.define(:turn_id, :quote) do
6
- UUID = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
7
- QUOTE_MAX = 200
8
-
9
6
  def self.from(value)
10
7
  return value if value.is_a?(self)
11
8
  return nil unless value.is_a?(Hash)
@@ -15,8 +12,8 @@ module Samagotchi
15
12
 
16
13
  turn_id = fields["turn_id"]
17
14
  quote = fields["quote"]
18
- return nil unless turn_id.is_a?(String) && UUID.match?(turn_id)
19
- return nil unless quote.is_a?(String) && quote.length.between?(1, QUOTE_MAX) && !quote.strip.empty?
15
+ return nil unless turn_id.is_a?(String) && self::UUID.match?(turn_id)
16
+ return nil unless quote.is_a?(String) && quote.length.between?(1, self::QUOTE_MAX) && !quote.strip.empty?
20
17
 
21
18
  new(turn_id:, quote:)
22
19
  end
@@ -26,6 +23,9 @@ module Samagotchi
26
23
  def to_file = { "turn_id" => turn_id, "quote" => quote }
27
24
  end
28
25
 
26
+ # The shape of the turn id an anchor names (ReviseAnchor's too).
27
+ VerifyAnchor::UUID = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
28
+ VerifyAnchor::QUOTE_MAX = 200
29
29
  VerifyAnchor::FEATURE = "verify"
30
30
  VerifyAnchor::UNAVAILABLE = {
31
31
  "error" => "verify_unavailable",
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Samagotchi
4
- VERSION = "0.134.0"
4
+ VERSION = "0.135.0"
5
5
  # Sent as the User-Agent of every HTTP request chi makes.
6
6
  USER_AGENT = "chi/#{VERSION}"
7
7
  end
@@ -134,8 +134,6 @@ module Samagotchi
134
134
 
135
135
  # @param bridge_wait_timeout [Float] bounded seconds to wait for a
136
136
  # freshly-spawned worker's bridge before answering POST /api/sessions.
137
- # @param view ["stage", "turn"] the page's view of a turn (web.view);
138
- # ?view=stage|turn overrides it for one page load
139
137
  # @param working_set [Boolean] the stage view's working set
140
138
  # (web.working_set); ?working_set=1|0 overrides it for one page load
141
139
  # @param annotate_presets [String, Array] the quick replies next to
@@ -168,7 +166,7 @@ module Samagotchi
168
166
  # @param start_dir [String] the folder chi web starts in (StartDir):
169
167
  # where a new session without a dir starts (nil: the cwd)
170
168
  def initialize(manager: nil, session_class: nil, state_dir: nil, public_dir: nil,
171
- bridge_wait_timeout: BRIDGE_WAIT_TIMEOUT, markdown: false, view: Config::BY_KEY["web.view"].default, hub: nil,
169
+ bridge_wait_timeout: BRIDGE_WAIT_TIMEOUT, markdown: false, hub: nil,
172
170
  annotate_presets: Config::BY_KEY["web.annotate_presets"].default,
173
171
  editor: Config::BY_KEY["web.editor"].default,
174
172
  events_heartbeat: EVENTS_HEARTBEAT, events_queue: EVENTS_QUEUE,
@@ -188,7 +186,6 @@ module Samagotchi
188
186
  @public_dir = public_dir || File.expand_path("public", __dir__)
189
187
  @bridge_wait_timeout = bridge_wait_timeout
190
188
  @markdown_renderer = MarkdownRenderer.new(enabled: markdown)
191
- @view = view
192
189
  @working_set = working_set
193
190
  @annotate_presets = annotate_presets.is_a?(Array) ? annotate_presets.join("|") : annotate_presets.to_s
194
191
  @editor_url = Editor.template(editor)
@@ -2015,8 +2012,7 @@ module Samagotchi
2015
2012
  # The owner's home: a claim link's quote reads it "~", as the
2016
2013
  # participant page shows it (claim_link.js).
2017
2014
  attrs["home"] = Dir.home
2018
- attrs["view"] = view_name(req.params["view"])
2019
- # Only when on: the page reads its presence (the turn view ignores it).
2015
+ # Only when on: the page reads its presence.
2020
2016
  attrs["working-set"] = "1" if working_set?(req.params["working_set"])
2021
2017
  attrs.merge!(annotate_presets_data)
2022
2018
  dir, = scope_dir(req.params["dir"])
@@ -2060,14 +2056,6 @@ module Samagotchi
2060
2056
  File.directory?(dir) && ProjectScope.root_for(dir) ? dir : nil
2061
2057
  end
2062
2058
 
2063
- VIEWS = Config::BY_KEY["web.view"].enum_values
2064
-
2065
- # The view for this page load: ?view=stage|turn wins, anything
2066
- # else leaves the config's choice (web.view).
2067
- def view_name(param)
2068
- VIEWS.include?(param) ? param : @view
2069
- end
2070
-
2071
2059
  # The working set for this page load: ?working_set=1|0 wins, anything
2072
2060
  # else leaves the config's choice (web.working_set).
2073
2061
  def working_set?(param)
@@ -10,6 +10,7 @@ require_relative "../message_time"
10
10
  require_relative "../output_formatter"
11
11
  require_relative "../recap_store"
12
12
  require_relative "../ref"
13
+ require_relative "../revise_anchor"
13
14
  require_relative "../steer"
14
15
  require_relative "../turn_note"
15
16
  require_relative "../bridge/card_store"
@@ -70,7 +71,7 @@ module Samagotchi
70
71
 
71
72
  # @param msgs [Array<Hash>] a session's messages; symbol keys from disk,
72
73
  # string keys from a Bridge snapshot
73
- # @param parts [Boolean] the turn view's reload (?parts=1): each
74
+ # @param parts [Boolean] the turn block's reload (?parts=1): each
74
75
  # assistant message also carries what it did (MessageParts: thinking,
75
76
  # tool calls with params and output), and a message with nothing to
76
77
  # show but that (a text-less step) is kept with empty content
@@ -91,6 +92,7 @@ module Samagotchi
91
92
  if Samagotchi::Steer.turn_prompt?(m)
92
93
  note[:turn_start] = true
93
94
  note[:turn_id] = (m[:turn_id] || m["turn_id"]).to_s
95
+ note.merge!(revises_of(m))
94
96
  end
95
97
  filtered << note
96
98
  next
@@ -135,6 +137,13 @@ module Samagotchi
135
137
  # record by this, not by its place.
136
138
  turn_id = m[:turn_id] || m["turn_id"]
137
139
  message[:turn_id] = turn_id.to_s if norm_role == "user" && turn_id
140
+ # The turn it revised (a Revise stack, Engine#stamp_opening), and
141
+ # whether it was a Verify turn (the page offers no Revise on its
142
+ # answer).
143
+ if norm_role == "user" && turn_id && !message[:merged]
144
+ message.merge!(revises_of(m))
145
+ message[:verify] = true if Samagotchi::VerifyAnchor.from(m[:verify] || m["verify"])
146
+ end
138
147
  # A participant's message names them (the page's author badge).
139
148
  if norm_role == "user"
140
149
  message.merge!(Samagotchi::Steer.author_fields(m[:author] || m["author"], m[:author_name] || m["author_name"]))
@@ -166,6 +175,13 @@ module Samagotchi
166
175
  []
167
176
  end
168
177
 
178
+ # A turn opening's Revise stamp as {revises: {turn_id:}}, checked again
179
+ # (a stored file is never trusted), or nothing.
180
+ def revises_of(message)
181
+ anchor = Samagotchi::ReviseAnchor.from(message[:revises] || message["revises"])
182
+ anchor ? { revises: anchor.to_h } : {}
183
+ end
184
+
169
185
  # The entry's wall time (MessageTime: the page's hover and gap
170
186
  # dividers) as {at:}, or nothing for an entry saved without one.
171
187
  def time_of(message)
@@ -272,10 +272,10 @@ module Samagotchi
272
272
  reason = participant_turn_refusal(grant, prompt, body)
273
273
  return participant_turn_refused(grant, prompt, *reason) if reason
274
274
 
275
- verify = VerifyAnchor.from(body["verify"])
275
+ # A Verify turn's claim; never a Revise (the owner's stacks).
276
276
  result = SessionManager.deliver_turn(id, prompt: prompt, client_id: ClientId.review(grant.id),
277
277
  delivery: Delivery::QUEUE, author: grant.author,
278
- verify: verify,
278
+ anchors: TurnAnchors.from(body).without_revises,
279
279
  pending_limit: grant.queue_limit + 1,
280
280
  state_dir: @state_dir, manager: @manager,
281
281
  bridge: -> { live_bridge_client(id) })
@@ -4,6 +4,7 @@ require_relative "../output_formatter"
4
4
  require_relative "../steer"
5
5
  require_relative "../turn_origin"
6
6
  require_relative "../participant_mode"
7
+ require_relative "../revise_anchor"
7
8
  require_relative "../verify_anchor"
8
9
 
9
10
  module Samagotchi
@@ -134,6 +135,10 @@ module Samagotchi
134
135
  out = { "type" => field(event, :type).to_s, **id_fields(event) }
135
136
  verdict = verdict_record(field(event, :verdict), viewer)
136
137
  out["verdict"] = verdict if verdict
138
+ # The turn a completed Revise turn revised (as on its prompt; the
139
+ # page doesn't draw stacks yet).
140
+ revises = ReviseAnchor.from(field(event, :revises))
141
+ out["revises"] = revises.to_file if revises
137
142
  out
138
143
  end
139
144
 
@@ -249,6 +254,9 @@ module Samagotchi
249
254
  out["content"] = field(message, :content).to_s
250
255
  out["turn_id"] = turn_id.to_s if turn_id
251
256
  out["merged"] = true if merged
257
+ # The turn it revised (a Revise stack; the page doesn't draw it yet).
258
+ revises = ReviseAnchor.from(field(message, :revises))
259
+ out["revises"] = revises.to_file if revises && turn_id && !merged
252
260
  out.merge!(author_fields(field(message, :author), field(message, :author_name), viewer,
253
261
  field(message, :participant_mode)))
254
262
  images = field(message, :images)
@@ -154,7 +154,7 @@ export function addCompleted(model, event = {}, now = null) {
154
154
  ...(guardrail.judgment_id ? { judgment_id: guardrail.judgment_id } : {}),
155
155
  };
156
156
  }
157
- // The images the tool returned (a reload in the turn view draws them from
157
+ // The images the tool returned (a reload's turn block draws them from
158
158
  // the saved parts, turn_html.js toolRowTemplate).
159
159
  if (event.images?.length) row.images = event.images;
160
160
  // What an edit/write changed in its file (a reload draws it from the
@@ -27,7 +27,7 @@ export function annotationSource(node) {
27
27
  }
28
28
  const thinking = el.closest(".thinking-body");
29
29
  if (thinking) return { kind: "thinking", root: thinking };
30
- // A step's narration in the turn view (the live one streams).
30
+ // A step's narration in a turn block (the live one streams).
31
31
  const step = el.closest(".gen-text");
32
32
  if (step) return step.classList.contains("streaming") ? null : { kind: "step", root: step };
33
33
  const own = el.closest(".bubble.user .user-message");
@@ -3,7 +3,7 @@
3
3
  // gets a fold row: a sibling just before the bubble, never inside it (the
4
4
  // bubble's copy text, its claims and a live end's innerHTML stay the
5
5
  // bubble's own). Folded, the bubble has the class `.folded` (CSS hides it;
6
- // no inline style: hold.js strips that) and the row reads its first line
6
+ // no inline style) and the row reads its first line
7
7
  // and its size, "‹first line› · N lines ▸"; unfolded, the row stays as a
8
8
  // slim "▾ N lines" header.
9
9
  //
@@ -37,8 +37,8 @@ const LABEL_CHARS = 160;
37
37
 
38
38
  const ROW_CLASS = "answer-fold";
39
39
  const ANSWERS = ".bubble.output";
40
- // Not done yet: still streaming, or held by the answer pop (hold.js).
41
- const UNFINISHED = ".streaming, .boxed";
40
+ // Not done yet: still streaming.
41
+ const UNFINISHED = ".streaming";
42
42
 
43
43
  // +text+'s lines (a trailing newline ends the last one, it adds none).
44
44
  export function lineCount(text) {