samagotchi 0.134.0 → 0.136.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 (114) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +78 -1
  3. data/bin/chi +2 -3
  4. data/docs/architecture.md +6 -3
  5. data/docs/cli.md +43 -38
  6. data/docs/configuration.md +11 -9
  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/prompt-caching.md +1 -1
  11. data/docs/internals/web-frontend.md +263 -0
  12. data/docs/internals/working-set.md +2 -2
  13. data/docs/sessions.md +5 -3
  14. data/docs/testing.md +36 -6
  15. data/lib/samagotchi/bridge.rb +11 -6
  16. data/lib/samagotchi/bridge_client.rb +8 -5
  17. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  18. data/lib/samagotchi/config.rb +0 -2
  19. data/lib/samagotchi/engine.rb +69 -12
  20. data/lib/samagotchi/guardrail_wiring.rb +8 -6
  21. data/lib/samagotchi/guardrails/approval.rb +41 -5
  22. data/lib/samagotchi/guardrails/implicit_writers.rb +19 -4
  23. data/lib/samagotchi/guardrails/pair_boundary.rb +5 -1
  24. data/lib/samagotchi/guardrails/pair_command.rb +387 -78
  25. data/lib/samagotchi/guardrails/parent_approvals.rb +12 -15
  26. data/lib/samagotchi/guardrails/read_only_shell.rb +24 -2
  27. data/lib/samagotchi/guardrails/shared_git.rb +1 -1
  28. data/lib/samagotchi/guardrails/shell_git_dirs.rb +1 -1
  29. data/lib/samagotchi/guardrails/shell_lex.rb +22 -0
  30. data/lib/samagotchi/guardrails_command.rb +13 -10
  31. data/lib/samagotchi/llm/chat_loop.rb +4 -3
  32. data/lib/samagotchi/message_parts.rb +1 -1
  33. data/lib/samagotchi/revise_anchor.rb +33 -0
  34. data/lib/samagotchi/session_inbox.rb +20 -12
  35. data/lib/samagotchi/session_manager.rb +13 -11
  36. data/lib/samagotchi/system_one/approval_question.rb +21 -16
  37. data/lib/samagotchi/system_one/self_protect.rb +13 -15
  38. data/lib/samagotchi/tools/delegate_relay.rb +6 -5
  39. data/lib/samagotchi/tools/delegate_wait.rb +5 -5
  40. data/lib/samagotchi/tools/execute.rb +6 -6
  41. data/lib/samagotchi/tools/output_guardrails.rb +9 -2
  42. data/lib/samagotchi/tools/read.rb +5 -5
  43. data/lib/samagotchi/turn_anchors.rb +49 -0
  44. data/lib/samagotchi/verify_anchor.rb +5 -5
  45. data/lib/samagotchi/version.rb +1 -1
  46. data/lib/samagotchi/web/app.rb +6 -18
  47. data/lib/samagotchi/web/message_display.rb +33 -1
  48. data/lib/samagotchi/web/participant_access.rb +2 -2
  49. data/lib/samagotchi/web/participant_view.rb +8 -0
  50. data/lib/samagotchi/web/public/activity.js +1 -1
  51. data/lib/samagotchi/web/public/annotations.js +1 -1
  52. data/lib/samagotchi/web/public/answer_fold.js +100 -31
  53. data/lib/samagotchi/web/public/app.js +451 -367
  54. data/lib/samagotchi/web/public/bubbles.js +30 -10
  55. data/lib/samagotchi/web/public/cards_place.js +4 -2
  56. data/lib/samagotchi/web/public/chip_popover.js +2 -2
  57. data/lib/samagotchi/web/public/composer.css +8 -0
  58. data/lib/samagotchi/web/public/context_chips.js +9 -6
  59. data/lib/samagotchi/web/public/data.js +4 -2
  60. data/lib/samagotchi/web/public/dock_reserve.js +13 -15
  61. data/lib/samagotchi/web/public/feature_gates.js +33 -0
  62. data/lib/samagotchi/web/public/history_reveal.js +10 -6
  63. data/lib/samagotchi/web/public/index.html +105 -53
  64. data/lib/samagotchi/web/public/info_bar.js +11 -12
  65. data/lib/samagotchi/web/public/info_bar_model.js +8 -10
  66. data/lib/samagotchi/web/public/latest.js +46 -0
  67. data/lib/samagotchi/web/public/level_chip.js +33 -95
  68. data/lib/samagotchi/web/public/lists_view.js +15 -13
  69. data/lib/samagotchi/web/public/live_timing.js +2 -0
  70. data/lib/samagotchi/web/public/llm_context_chip.js +13 -144
  71. data/lib/samagotchi/web/public/markdown_place.js +1 -1
  72. data/lib/samagotchi/web/public/method_api.js +14 -0
  73. data/lib/samagotchi/web/public/notifications.js +4 -11
  74. data/lib/samagotchi/web/public/participant.js +11 -6
  75. data/lib/samagotchi/web/public/participant_names.js +1 -1
  76. data/lib/samagotchi/web/public/participants_panel.js +4 -2
  77. data/lib/samagotchi/web/public/project_picker.js +18 -22
  78. data/lib/samagotchi/web/public/question_cards.js +2 -2
  79. data/lib/samagotchi/web/public/reasoning_history_chip.js +8 -34
  80. data/lib/samagotchi/web/public/restore.js +2 -0
  81. data/lib/samagotchi/web/public/revise_stack.js +378 -0
  82. data/lib/samagotchi/web/public/scope.js +12 -20
  83. data/lib/samagotchi/web/public/send_turn.js +29 -16
  84. data/lib/samagotchi/web/public/session_actions.js +15 -14
  85. data/lib/samagotchi/web/public/session_fields.js +152 -0
  86. data/lib/samagotchi/web/public/session_payload.js +35 -0
  87. data/lib/samagotchi/web/public/session_state.js +9 -5
  88. data/lib/samagotchi/web/public/session_store.js +191 -0
  89. data/lib/samagotchi/web/public/settings_chip.js +212 -0
  90. data/lib/samagotchi/web/public/settings_panel.js +121 -0
  91. data/lib/samagotchi/web/public/shell_command.js +26 -0
  92. data/lib/samagotchi/web/public/stage_model.js +4 -4
  93. data/lib/samagotchi/web/public/stage_view.js +11 -16
  94. data/lib/samagotchi/web/public/start_settings.js +25 -88
  95. data/lib/samagotchi/web/public/stream_handlers.js +88 -90
  96. data/lib/samagotchi/web/public/task_stop.js +1 -1
  97. data/lib/samagotchi/web/public/thinking_chip.js +7 -45
  98. data/lib/samagotchi/web/public/time_gaps.js +5 -2
  99. data/lib/samagotchi/web/public/timing.js +15 -5
  100. data/lib/samagotchi/web/public/toolkit_chip.js +37 -21
  101. data/lib/samagotchi/web/public/{turn_view.js → turn_block.js} +33 -93
  102. data/lib/samagotchi/web/public/turn_events.js +3 -3
  103. data/lib/samagotchi/web/public/turn_html.js +20 -16
  104. data/lib/samagotchi/web/public/turn_model.js +6 -6
  105. data/lib/samagotchi/web/public/verdict_fresh.js +2 -0
  106. data/lib/samagotchi/web/public/working_set_view.js +3 -1
  107. data/lib/samagotchi/web/server.rb +4 -4
  108. data/lib/samagotchi/web/session_control.rb +2 -2
  109. data/lib/samagotchi/web/session_create.rb +1 -1
  110. data/lib/samagotchi/web/session_show.rb +6 -3
  111. data/lib/samagotchi/worker.rb +9 -9
  112. data/lib/samagotchi/worker_inbound.rb +6 -5
  113. metadata +15 -3
  114. 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.136.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)
@@ -1292,7 +1289,7 @@ module Samagotchi
1292
1289
  nil
1293
1290
  end
1294
1291
 
1295
- # The LLM context +model+ starts under (the start page's llm ctx chip);
1292
+ # The LLM context +model+ starts under (the start page's ⚙ panel);
1296
1293
  # one that can't be read leaves this model without, not the others.
1297
1294
  def add_llm_context(model, target, names, settings)
1298
1295
  model[:llm_context] = LLMContextStrategy.explain(target, names: names, models: settings).summary(nil)
@@ -1301,7 +1298,7 @@ module Samagotchi
1301
1298
  end
1302
1299
 
1303
1300
  # The thinking level +model+ starts at, with where it came from (the
1304
- # start page's think chip; SettingExplained#summary, no session).
1301
+ # start page's ⚙ panel; SettingExplained#summary, no session).
1305
1302
  def add_thinking(model, target, names, settings)
1306
1303
  model[:thinking] = Thinking.explain(target, names: names, models: settings).summary
1307
1304
  rescue StandardError
@@ -1476,7 +1473,7 @@ module Samagotchi
1476
1473
  # set when it seeds a session's and when the expanded view opens (a
1477
1474
  # file changed while the session sat idle). A live worker's snapshot
1478
1475
  # messages are the truth, as #handle_show's, else the file's; the
1479
- # strategy is the session's llm ctx chip's, the folder its working
1476
+ # strategy is the session's llm_context summary's, the folder its working
1480
1477
  # directory. running: a turn runs. The snapshot's messages lag it, so
1481
1478
  # the answer is then the turn's last :working_set_updated (the
1482
1479
  # snapshot's working_set, TurnAccumulator#working_set); before the
@@ -1509,7 +1506,7 @@ module Samagotchi
1509
1506
  { items: set["items"], at: set["at"] }
1510
1507
  end
1511
1508
 
1512
- # The strategy an llm ctx chip names ("none", "stale,forget"), as
1509
+ # The strategy an llm_context summary names ("none", "stale,forget"), as
1513
1510
  # LLMContextView takes it; none when there is no chip.
1514
1511
  def working_set_strategy(llm_context)
1515
1512
  named = llm_context.is_a?(Hash) ? (llm_context[:strategy] || llm_context["strategy"]).to_s : ""
@@ -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"
@@ -28,9 +29,19 @@ module Samagotchi
28
29
  # A POST /api/render body may be this big: a decision message is a few
29
30
  # KB, and the cap bounds the work a page can ask for.
30
31
  MAX_RENDER_BYTES = 64 * 1024
32
+ # A delegate report's leading "session: <id>" / "status: <s>" lines
33
+ # (Tools::DelegateWait.result).
34
+ REPORT_HEADER = /\A((?:(?:session|status): [^\n]*\n)+)/
31
35
 
32
36
  private
33
37
 
38
+ # A delegate report's text as the markdown it renders from: its header
39
+ # lines one to a line and a paragraph of their own, so a "---" after
40
+ # them is a rule, not a heading's underline.
41
+ def report_markdown(text)
42
+ text.sub(REPORT_HEADER) { "#{Regexp.last_match(1).lines.map(&:chomp).join(" \n")}\n\n" }
43
+ end
44
+
34
45
  # The recap saved with the session and how many user turns came since:
35
46
  # a live worker's (it counts against its Engine's messages), else
36
47
  # recap.json against the session file. A stopped session gets its
@@ -70,7 +81,7 @@ module Samagotchi
70
81
 
71
82
  # @param msgs [Array<Hash>] a session's messages; symbol keys from disk,
72
83
  # string keys from a Bridge snapshot
73
- # @param parts [Boolean] the turn view's reload (?parts=1): each
84
+ # @param parts [Boolean] the turn block's reload (?parts=1): each
74
85
  # assistant message also carries what it did (MessageParts: thinking,
75
86
  # tool calls with params and output), and a message with nothing to
76
87
  # show but that (a text-less step) is kept with empty content
@@ -91,6 +102,7 @@ module Samagotchi
91
102
  if Samagotchi::Steer.turn_prompt?(m)
92
103
  note[:turn_start] = true
93
104
  note[:turn_id] = (m[:turn_id] || m["turn_id"]).to_s
105
+ note.merge!(revises_of(m))
94
106
  end
95
107
  filtered << note
96
108
  next
@@ -130,11 +142,24 @@ module Samagotchi
130
142
  message.merge!(merged: true, step: steer_step(list, index)) unless Samagotchi::Steer.turn_prompt?(m)
131
143
  source = (m[:source] || m["source"]).to_s
132
144
  message[:source] = source unless source.empty?
145
+ # A delegate report is the child's markdown: rendered as an
146
+ # answer is (its copy source stays the text). Another user line
147
+ # stays plain.
148
+ if source == Samagotchi::Steer::DELEGATE_REPORT && @markdown_renderer.available?
149
+ message[:html] = @markdown_renderer.render(report_markdown(stripped))
150
+ end
133
151
  end
134
152
  # The turn it started (Engine): the page pairs it with that turn's
135
153
  # record by this, not by its place.
136
154
  turn_id = m[:turn_id] || m["turn_id"]
137
155
  message[:turn_id] = turn_id.to_s if norm_role == "user" && turn_id
156
+ # The turn it revised (a Revise stack, Engine#stamp_opening), and
157
+ # whether it was a Verify turn (the page offers no Revise on its
158
+ # answer).
159
+ if norm_role == "user" && turn_id && !message[:merged]
160
+ message.merge!(revises_of(m))
161
+ message[:verify] = true if Samagotchi::VerifyAnchor.from(m[:verify] || m["verify"])
162
+ end
138
163
  # A participant's message names them (the page's author badge).
139
164
  if norm_role == "user"
140
165
  message.merge!(Samagotchi::Steer.author_fields(m[:author] || m["author"], m[:author_name] || m["author_name"]))
@@ -166,6 +191,13 @@ module Samagotchi
166
191
  []
167
192
  end
168
193
 
194
+ # A turn opening's Revise stamp as {revises: {turn_id:}}, checked again
195
+ # (a stored file is never trusted), or nothing.
196
+ def revises_of(message)
197
+ anchor = Samagotchi::ReviseAnchor.from(message[:revises] || message["revises"])
198
+ anchor ? { revises: anchor.to_h } : {}
199
+ end
200
+
169
201
  # The entry's wall time (MessageTime: the page's hover and gap
170
202
  # dividers) as {at:}, or nothing for an entry saved without one.
171
203
  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");