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.
Files changed (120) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -1
  3. data/docs/architecture.md +2 -2
  4. data/docs/cli.md +37 -23
  5. data/docs/configuration.md +1 -1
  6. data/docs/internals/block-renderers.md +2 -2
  7. data/docs/internals/bridge.md +5 -4
  8. data/docs/internals/context-telemetry.md +13 -1
  9. data/docs/internals/engine.md +4 -4
  10. data/docs/internals/inline-widgets.md +1 -1
  11. data/docs/internals/objectives.md +1 -1
  12. data/docs/internals/participants.md +235 -119
  13. data/docs/internals/web-frontend.md +164 -33
  14. data/docs/internals/working-set.md +69 -5
  15. data/docs/releasing.md +2 -1
  16. data/docs/sessions.md +6 -3
  17. data/lib/samagotchi/bridge/turn_accumulator.rb +8 -4
  18. data/lib/samagotchi/bridge.rb +39 -12
  19. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  20. data/lib/samagotchi/delivery.rb +13 -0
  21. data/lib/samagotchi/engine.rb +5 -1
  22. data/lib/samagotchi/event_fanout.rb +22 -1
  23. data/lib/samagotchi/grants.rb +5 -0
  24. data/lib/samagotchi/guardrails/review_paths.rb +2 -2
  25. data/lib/samagotchi/image_store.rb +51 -12
  26. data/lib/samagotchi/kernel_loop.rb +7 -1
  27. data/lib/samagotchi/llm/stray_tool_call.rb +7 -4
  28. data/lib/samagotchi/llm_context_stale.rb +4 -1
  29. data/lib/samagotchi/participant_messages.rb +39 -0
  30. data/lib/samagotchi/participant_revise.rb +61 -0
  31. data/lib/samagotchi/participant_uploads.rb +98 -0
  32. data/lib/samagotchi/reply_wait/poll.rb +5 -1
  33. data/lib/samagotchi/revise_anchor.rb +9 -0
  34. data/lib/samagotchi/send_command.rb +5 -0
  35. data/lib/samagotchi/session_inbox.rb +20 -12
  36. data/lib/samagotchi/session_manager/delivery.rb +7 -0
  37. data/lib/samagotchi/session_stores.rb +2 -0
  38. data/lib/samagotchi/tool_runner.rb +39 -5
  39. data/lib/samagotchi/tools/memory.rb +26 -7
  40. data/lib/samagotchi/turn_anchors.rb +0 -3
  41. data/lib/samagotchi/turn_stages/verify_anchors.rb +13 -1
  42. data/lib/samagotchi/verify_anchor.rb +6 -0
  43. data/lib/samagotchi/version.rb +1 -1
  44. data/lib/samagotchi/web/app.rb +42 -77
  45. data/lib/samagotchi/web/participant_access.rb +129 -26
  46. data/lib/samagotchi/web/participant_images.rb +185 -0
  47. data/lib/samagotchi/web/participant_review.rb +19 -138
  48. data/lib/samagotchi/web/participant_show.rb +240 -0
  49. data/lib/samagotchi/web/participant_stream_filter.rb +25 -2
  50. data/lib/samagotchi/web/participant_view.rb +331 -24
  51. data/lib/samagotchi/web/public/annotate_bar.css +1 -2
  52. data/lib/samagotchi/web/public/annotate_bar.js +1 -2
  53. data/lib/samagotchi/web/public/app.js +124 -88
  54. data/lib/samagotchi/web/public/block_host.js +7 -4
  55. data/lib/samagotchi/web/public/bubbles.js +9 -1
  56. data/lib/samagotchi/web/public/cards_offscreen.js +69 -0
  57. data/lib/samagotchi/web/public/chip_popover.js +5 -4
  58. data/lib/samagotchi/web/public/claim_link.js +4 -4
  59. data/lib/samagotchi/web/public/composer.css +4 -6
  60. data/lib/samagotchi/web/public/composer_mode.js +11 -6
  61. data/lib/samagotchi/web/public/composer_mount.js +4 -4
  62. data/lib/samagotchi/web/public/ctx.js +12 -3
  63. data/lib/samagotchi/web/public/delivery_line.js +34 -5
  64. data/lib/samagotchi/web/public/history_shape.js +5 -1
  65. data/lib/samagotchi/web/public/images.js +7 -1
  66. data/lib/samagotchi/web/public/index.html +40 -4
  67. data/lib/samagotchi/web/public/info_bar_model.js +2 -1
  68. data/lib/samagotchi/web/public/markdown_place.js +5 -4
  69. data/lib/samagotchi/web/public/owner_claim.js +3 -3
  70. data/lib/samagotchi/web/public/owner_verify.js +0 -2
  71. data/lib/samagotchi/web/public/participant_bar.js +108 -0
  72. data/lib/samagotchi/web/public/participant_main.js +289 -0
  73. data/lib/samagotchi/web/public/participant_model.js +5 -292
  74. data/lib/samagotchi/web/public/participant_names.js +6 -1
  75. data/lib/samagotchi/web/public/participant_revise.js +66 -0
  76. data/lib/samagotchi/web/public/participants_panel.js +3 -12
  77. data/lib/samagotchi/web/public/question_cards.js +14 -3
  78. data/lib/samagotchi/web/public/refs.js +53 -36
  79. data/lib/samagotchi/web/public/reload_check.js +68 -11
  80. data/lib/samagotchi/web/public/revise_stack.js +21 -10
  81. data/lib/samagotchi/web/public/send_turn.js +24 -6
  82. data/lib/samagotchi/web/public/session_fields.js +1 -1
  83. data/lib/samagotchi/web/public/session_state.js +12 -4
  84. data/lib/samagotchi/web/public/session_sync.js +9 -2
  85. data/lib/samagotchi/web/public/session_view.js +17 -7
  86. data/lib/samagotchi/web/public/stage_asking.js +6 -4
  87. data/lib/samagotchi/web/public/stage_owner_wait.js +35 -0
  88. data/lib/samagotchi/web/public/stage_view.js +18 -4
  89. data/lib/samagotchi/web/public/stage_waiting.js +74 -6
  90. data/lib/samagotchi/web/public/storage.js +22 -0
  91. data/lib/samagotchi/web/public/stream_handlers.js +2 -1
  92. data/lib/samagotchi/web/public/tokens.css +1 -1
  93. data/lib/samagotchi/web/public/turn_block.js +3 -2
  94. data/lib/samagotchi/web/public/turn_events.js +15 -6
  95. data/lib/samagotchi/web/public/turn_html.js +9 -2
  96. data/lib/samagotchi/web/public/turn_range.js +1 -2
  97. data/lib/samagotchi/web/public/verify_target.js +2 -4
  98. data/lib/samagotchi/web/public/viewer.js +59 -0
  99. data/lib/samagotchi/web/public/viewer_self.js +53 -0
  100. data/lib/samagotchi/web/public/working_set.js +11 -5
  101. data/lib/samagotchi/web/public/working_set_view.js +53 -8
  102. data/lib/samagotchi/web/public/ws_cards.js +89 -0
  103. data/lib/samagotchi/web/public/ws_peek.js +106 -0
  104. data/lib/samagotchi/web/public/ws_preview.css +126 -0
  105. data/lib/samagotchi/web/public/ws_reader.js +319 -0
  106. data/lib/samagotchi/web/public/ws_timeline.js +200 -0
  107. data/lib/samagotchi/web/public/ws_way_in.js +25 -0
  108. data/lib/samagotchi/web/session_control.rb +39 -14
  109. data/lib/samagotchi/web/session_create.rb +1 -1
  110. data/lib/samagotchi/web/session_show.rb +75 -28
  111. data/lib/samagotchi/web/viewer.rb +70 -0
  112. data/lib/samagotchi/web/working_set_routes.rb +132 -0
  113. data/lib/samagotchi/worker.rb +19 -14
  114. data/lib/samagotchi/working_set.rb +260 -8
  115. metadata +20 -6
  116. data/lib/samagotchi/web/public/participant.html +0 -218
  117. data/lib/samagotchi/web/public/participant.js +0 -719
  118. data/lib/samagotchi/web/public/participant_annotate.js +0 -76
  119. data/lib/samagotchi/web/public/review_history.js +0 -72
  120. data/lib/samagotchi/web/public/tools_overview.js +0 -42
@@ -17,9 +17,26 @@ require_relative "../session_metrics"
17
17
  require_relative "../session_setting"
18
18
  require_relative "../system_one/log"
19
19
  require_relative "../toolkit"
20
+ require_relative "viewer"
20
21
 
21
22
  module Samagotchi
22
23
  module Web
24
+ # What a session's show is read from, the same for the owner's and a
25
+ # participant's (SessionShow#show_reads): the session file, a live
26
+ # worker's turn snapshot and state snapshot (both nil without one), the
27
+ # messages shown (the snapshot's, else the file's) and the stream cursor
28
+ # the page goes on from (+event_seq+, and the snapshot's +event_id+
29
+ # `<seq>-<epoch>`). Each assembler makes its own payload from it.
30
+ ShowReads = Data.define(:id, :session, :turn_snapshot, :state, :event_seq, :event_id, :raw_messages) do
31
+ # The turn in progress, as the worker's snapshot has it (nil: none).
32
+ def current_turn = turn_snapshot && turn_snapshot["current_turn"]
33
+
34
+ def current_turn_id
35
+ turn = current_turn
36
+ turn.is_a?(Hash) ? turn["turn_id"] : nil
37
+ end
38
+ end
39
+
23
40
  # GET /api/sessions/:id, the page's session read, and its light re-reads
24
41
  # (?cards=1, ?tail=1, ?timing=1; ?brief=1 is SessionAccessRoutes'
25
42
  # handle_brief_read), GET /api/sessions/:id/output, and the timing
@@ -33,7 +50,7 @@ module Samagotchi
33
50
  private
34
51
 
35
52
  def handle_show(req, id)
36
- return participant_show(id) unless principal(req).owner?
53
+ return participant_show_read(req, id) unless principal(req).owner?
37
54
 
38
55
  # The page's light re-reads: never the whole conversation.
39
56
  return handle_cards_read(id) if req.params["cards"] == "1"
@@ -46,35 +63,59 @@ module Samagotchi
46
63
  # (The light re-reads come after a full read only.)
47
64
  return scratch_refused(id) if session.scratch
48
65
 
49
- # Read-only preview: selecting a session never spawns a worker.
50
- # The worker is woken only on POST /turn (handle_turn) via
51
- # SessionManager.resume_session + write_turn_input. This avoids
52
- # replaying last_prompt and spamming workers on preview scrub.
53
- # last_event_seq is nil when no live bridge, so the frontend stays
54
- # silent until the first Send.
55
- #
56
- # A live worker's snapshot is the truth (the session file lags its
57
- # Engine): the messages, the turn in progress and the prompts queued
58
- # behind it, all at one event_seq the client streams on from.
66
+ owner_show(req, show_reads(id, session))
67
+ end
68
+
69
+ # The reads a session's show is made of, whoever it is for (ShowReads).
70
+ # Read-only preview: selecting a session never spawns a worker.
71
+ # The worker is woken only on POST /turn (handle_turn) via
72
+ # SessionManager.resume_session + write_turn_input. This avoids
73
+ # replaying last_prompt and spamming workers on preview scrub.
74
+ # event_seq is nil when no live bridge, so the frontend stays
75
+ # silent until the first Send.
76
+ #
77
+ # A live worker's snapshot is the truth (the session file lags its
78
+ # Engine): the messages, the turn in progress and the prompts queued
79
+ # behind it, all at one event_seq the client streams on from.
80
+ # @return [ShowReads]
81
+ def show_reads(id, session)
59
82
  live = bridge_get_json(id, "snapshot")
60
83
  turn_snapshot = live && live["snapshot"]
61
- snapshot = live && live["session_state_snapshot"]
62
- last_event_seq = live_or_saved(snapshot, "event_seq") { bridge_event_seq(id) }
63
- current_turn = turn_snapshot && turn_snapshot["current_turn"]
84
+ state = live && live["session_state_snapshot"]
85
+ ShowReads.new(
86
+ id: id, session: session, turn_snapshot: turn_snapshot, state: state,
87
+ event_seq: live_or_saved(state, "event_seq") { bridge_event_seq(id) },
88
+ # The stream cursor `<seq>-<epoch>`: a later worker resets it
89
+ # instead of taking the seq as its own. Only the snapshot has it.
90
+ event_id: turn_snapshot && turn_snapshot["event_id"],
91
+ raw_messages: live_or_saved(turn_snapshot, "messages") { session.messages }
92
+ )
93
+ end
94
+
95
+ # Every finished turn of +reads+ drawn from a line (a real one, else
96
+ # one made from its messages, #drawn_lines): its items are stamped,
97
+ # and its drawing goes in turn_drawings. The running turn has none yet.
98
+ def show_drawn_lines(reads, timing:, entries:)
99
+ drawn_lines(reads.id, reads.raw_messages, timing: timing, entries: entries, cwd: reads.session.working_directory,
100
+ skip_turn_id: reads.current_turn_id)
101
+ end
102
+
103
+ # The owner's show of +reads+: the whole page's read (its cards,
104
+ # history, commands, recap, timing and question too).
105
+ def owner_show(req, reads)
106
+ id = reads.id
107
+ session = reads.session
108
+ turn_snapshot = reads.turn_snapshot
109
+ raw_messages = reads.raw_messages
64
110
  owner = session_owner(id)
65
111
  pending = shown_pending_question(session, turn_snapshot, owner)
66
112
  # What the worker's server said it served for that model (after a turn);
67
113
  # without a worker, what the session's analytics.json last saved.
68
- persisted = snapshot ? nil : read_analytics(id)
69
- session_json = shown_session_json(session, snapshot, owner, persisted)
70
- raw_messages = live_or_saved(turn_snapshot, "messages") { session.messages }
71
- timing = timing_payload(id, live_metrics: snapshot && snapshot["metrics"], persisted: persisted)
72
- # Every finished turn is drawn from a line (a real one, else one
73
- # made from its messages): its items are stamped, and its drawing
74
- # goes in turn_drawings. The running turn has none yet.
114
+ persisted = reads.state ? nil : read_analytics(id)
115
+ session_json = shown_session_json(session, reads.state, owner, persisted)
116
+ timing = timing_payload(id, live_metrics: reads.state && reads.state["metrics"], persisted: persisted)
75
117
  raw_cards = live_or_saved(turn_snapshot, "cards") { saved_cards(id, messages: raw_messages) }
76
- drawn = drawn_lines(id, raw_messages, timing: timing, entries: raw_cards, cwd: session.working_directory,
77
- skip_turn_id: current_turn.is_a?(Hash) ? current_turn["turn_id"] : nil)
118
+ drawn = show_drawn_lines(reads, timing: timing, entries: raw_cards)
78
119
  messages = messages_for_display(raw_messages, cwd: session.working_directory, drawings: drawn)
79
120
  json_response(200, {
80
121
  session: session_json,
@@ -82,7 +123,7 @@ module Samagotchi
82
123
  messages: messages,
83
124
  **turn_drawings_field(drawn, messages),
84
125
  failed_turn: failed_turn_for(session, raw_messages),
85
- current_turn: current_turn,
126
+ current_turn: reads.current_turn,
86
127
  # The page's join: the turn in progress and the queue behind it as
87
128
  # the live events that draw them (TurnAccumulator.join_events, one
88
129
  # unfold, here: any worker's snapshot works).
@@ -104,11 +145,11 @@ module Samagotchi
104
145
  markdown_warning: @markdown_renderer.warning,
105
146
  markdown_tip: @markdown_renderer.tip,
106
147
  **viewer_capabilities(req),
148
+ # Who looks and what they may do (Viewer), as <body> says it at boot.
149
+ viewer: Viewer.for(principal(req)).to_h,
107
150
  pending_question: pending,
108
- last_event_seq: last_event_seq,
109
- # The stream cursor `<seq>-<epoch>`: a later worker resets it
110
- # instead of taking the seq as its own. Only the snapshot has it.
111
- last_event_id: turn_snapshot && turn_snapshot["event_id"],
151
+ last_event_seq: reads.event_seq,
152
+ last_event_id: reads.event_id,
112
153
  timing: timing,
113
154
  **objectives_field(id, raw_messages, turn_snapshot)
114
155
  })
@@ -231,6 +272,12 @@ module Samagotchi
231
272
  served_expected_by: snapshot["served_expected_by"])
232
273
  # The model notes its prompt carries now (a /model there changes them).
233
274
  session_json = session_json.merge(prompt_notes: snapshot["prompt_notes"]) if snapshot["prompt_notes"].is_a?(Array)
275
+ # The newest context estimate the worker has (a turn in flight: the
276
+ # last request's), so a page opened mid-turn shows the percent at
277
+ # once. Owner show only: a participant's show is ParticipantView's
278
+ # allowlist. A snapshot without the key (an older worker) sends
279
+ # nothing, so its page reads as it always did.
280
+ session_json = session_json.merge(context_status: snapshot["context_status"]) if snapshot.key?("context_status")
234
281
  else
235
282
  session_json = session_json.merge(saved_served_model(session, persisted))
236
283
  end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../client_id"
4
+
5
+ module Samagotchi
6
+ module Web
7
+ # Who looks at chi web's main page and what they may do there: the
8
+ # page draws a control, and makes a call, only when +can+ names its
9
+ # capability (viewer.js; docs/internals/web-frontend.md, The viewer).
10
+ # Not Web::Capabilities: that one says what the page renders (markdown
11
+ # display) and whether this request's viewer can open a path in this
12
+ # machine's editor; this one says which of the owner's controls this
13
+ # viewer gets at all. The page reads it from <body> at boot
14
+ # (App#index_data_attributes, #body_data) and from a show's +viewer+.
15
+ #
16
+ # +kind+ "owner" | "participant"; +id+ and +name+ a participant's grant's
17
+ # (nil for the owner); +can+ the frozen list of capability names.
18
+ Viewer = Data.define(:kind, :id, :name, :can) do
19
+ # @param principal [Principal] the request's
20
+ # @return [Viewer] the owner's with every capability; a participant's
21
+ # with PARTICIPANT_CAPABILITIES only (what their routes serve).
22
+ def self.for(principal)
23
+ return Viewer::OWNER if principal.owner?
24
+
25
+ grant = principal.grant
26
+ new(kind: "participant", id: grant.id, name: grant.name, can: Viewer::PARTICIPANT_CAPABILITIES)
27
+ end
28
+
29
+ def owner? = kind == "owner"
30
+
31
+ # The client id chi web gives this viewer's turns (it replaces the
32
+ # body's): a participant's review:<grant id> (ClientId.review), so the
33
+ # page can tell its own turns; nil for the owner (each tab names its own).
34
+ def client_id = owner? ? nil : ClientId.review(id)
35
+
36
+ def can?(name) = can.include?(name.to_s)
37
+
38
+ # The <body> data attributes the page reads at boot: the kind and the
39
+ # space-separated +can+ list (no names or ids: those come with a show).
40
+ def body_attributes = { "viewer-kind" => kind, "viewer-can" => can.join(" ") }
41
+
42
+ # A participant's carries their client id too.
43
+ def to_h = { kind: kind, id: id, name: name, can: can, **(owner? ? {} : { client_id: client_id }) }
44
+ end
45
+
46
+ # The capabilities the main page gates by, each named for what the
47
+ # owner does with it (the page's controls and calls behind each one:
48
+ # docs/internals/web-frontend.md, The viewer).
49
+ Viewer::CAPABILITIES = %w[
50
+ sessions models prompt_history push context working_set working_set_items participants_admin rename images
51
+ session_controls revise apply answer_cards render block_renderers stop_any_turn steer_any_turn
52
+ ].freeze
53
+
54
+ # A participant's capabilities: names of their own (the owner's are
55
+ # CAPABILITIES), each one a participant route serves. +send+: their
56
+ # composer (POST …/turn); +stop_own_turn+: the stage's Stop turn on a
57
+ # turn they started (POST …/cancel with its turn_id); +steer_own_turn+:
58
+ # a message sent while their own turn runs goes in at its next step
59
+ # (a worker with Delivery::OWN_TURN_STEER_FEATURE); +revise_own_turn+:
60
+ # Revise on the answer to their own message with only their own turns
61
+ # after it (POST …/turn with revises, ParticipantRevise). And of the owner's,
62
+ # those a participant's route serves filtered: +working_set+ (GET
63
+ # …/working_set, the project's files only) and +images+ (POST …/images
64
+ # within their cap and quota; GET …/images/<name> only for an image
65
+ # they uploaded; their turn takes only those: ParticipantImages).
66
+ Viewer::PARTICIPANT_CAPABILITIES = %w[send stop_own_turn steer_own_turn revise_own_turn working_set images].freeze
67
+
68
+ Viewer::OWNER = Viewer.new(kind: "owner", id: nil, name: nil, can: Viewer::CAPABILITIES)
69
+ end
70
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../working_set"
4
+ require_relative "../llm_context_view"
5
+ require_relative "../session_setting"
6
+ require_relative "../memory_paths"
7
+ require_relative "participant_view"
8
+
9
+ module Samagotchi
10
+ module Web
11
+ # The working set's routes (web.working_set), mixed into App. See
12
+ # docs/internals/working-set.md (Delivery).
13
+ module WorkingSetRoutes
14
+ # What a working-set build reads off a session: its +messages+ (symbol
15
+ # keys at the top, as paired_runs reads them), the +strategy+ its
16
+ # llm_context summary names, its folder (+cwd+) and its +pins+.
17
+ Source = Data.define(:messages, :strategy, :cwd, :pins)
18
+
19
+ private
20
+
21
+ # GET /api/sessions/:id/working_set: the working set as the model has
22
+ # it ({items:, at:, running:}, WorkingSet), what the page overlays on its rows'
23
+ # set when it seeds a session's and when the expanded view opens (a
24
+ # file changed while the session sat idle). A live worker's snapshot
25
+ # messages are the truth, as #handle_show's, else the file's; the
26
+ # strategy is the session's llm_context summary's, the folder its working
27
+ # directory. running: a turn runs. The snapshot's messages lag it, so
28
+ # the answer is then the turn's last :working_set_updated (the
29
+ # snapshot's working_set, TurnAccumulator#working_set); before the
30
+ # turn emitted one, or from an older worker without the field, it is
31
+ # the session's messages, behind that turn's events, and the page
32
+ # keeps theirs (working_set.js newerSnapshot).
33
+ # A participant's is filtered (ParticipantView.working_set: the
34
+ # project's files only).
35
+ def handle_working_set(req, id)
36
+ session = @session_class.load(id, state_dir: default_state_dir)
37
+ set = working_set_payload(id, session)
38
+ return json_response(200, set) if principal(req).owner?
39
+
40
+ viewer = participant_viewer(req, session)
41
+ # Redacted as the stream's event is (ParticipantView.event): a file name may hold a secret's shape.
42
+ shown = Redact.deep(ParticipantView.working_set(set, viewer), viewer)
43
+ json_response(200, shown.merge("running" => set[:running] == true))
44
+ end
45
+
46
+ # GET /api/sessions/:id/working_set/item?kind=&key=[&now=1]: what the
47
+ # model has of one item, its timeline (WorkingSet.timeline_of: every
48
+ # run of it, in conversation order), the reader's and the popover
49
+ # peek's. Owner only. Always built from messages, unlike the set's
50
+ # route: the worker's snapshot messages (they lag a running turn's
51
+ # events; the page asks again on its next :working_set_updated), else
52
+ # the file's. Whole texts: the tool limits bound them (read's
53
+ # truncate_at_bytes, the ToolRunner cap). +now+ (a memory): what its
54
+ # latest read would answer now and the diff (WorkingSet.memory_now).
55
+ def handle_working_set_item(req, id)
56
+ kind = req.params["kind"].to_s
57
+ key = req.params["key"].to_s
58
+ unless [WorkingSet::FILE, WorkingSet::MEMORY, WorkingSet::URL].include?(kind) && !key.strip.empty?
59
+ return error_response(400, "invalid_item", "expected kind (file, memory or url) and key")
60
+ end
61
+
62
+ session = @session_class.load(id, state_dir: default_state_dir)
63
+ source = working_set_source(bridge_get_json(id, "snapshot"), session)
64
+ timeline = WorkingSet.timeline_of(source.messages, kind: kind, key: key, strategy: source.strategy, cwd: source.cwd)
65
+ body = timeline.to_h
66
+ if req.params["now"] == "1" && kind == WorkingSet::MEMORY
67
+ body[:now] = WorkingSet.memory_now(source.messages, key: key, strategy: source.strategy, cwd: source.cwd,
68
+ root: MemoryPaths.system_dir)&.to_h
69
+ end
70
+ json_response(200, body)
71
+ end
72
+
73
+ def working_set_payload(id, session)
74
+ live = bridge_get_json(id, "snapshot")
75
+ worker_snapshot = live && live["snapshot"]
76
+ running = !worker_snapshot&.dig("current_turn").nil?
77
+ turn_set = running && running_working_set(live["snapshot"]["working_set"])
78
+ return turn_set.merge(running: true) if turn_set
79
+
80
+ idle_set = idle_working_set(worker_snapshot && worker_snapshot["idle_working_set"]) unless running
81
+ return idle_set.merge(running: false) if idle_set
82
+
83
+ source = working_set_source(live, session)
84
+ snapshot = WorkingSet.build(source.messages, cwd: source.cwd, strategy: source.strategy, pins: source.pins)
85
+ snapshot.to_h.merge(running: running)
86
+ end
87
+
88
+ # What a build from messages reads (+live+: the worker's Bridge
89
+ # snapshot answer, nil without a worker): the worker's snapshot
90
+ # messages, as #handle_show reads them, else the file's; the strategy
91
+ # the worker's llm_context summary names, else the saved chip's.
92
+ # @return [Source]
93
+ def working_set_source(live, session)
94
+ messages = live_or_saved(live && live["snapshot"], "messages") { session.messages }
95
+ # A snapshot's entries have string keys (paired_runs reads symbols).
96
+ messages = Array(messages).map { |message| message.is_a?(Hash) ? message.transform_keys(&:to_sym) : message }
97
+ llm_context = SessionSetting::LLM_CONTEXT.snapshot_value(live && live["session_state_snapshot"]) do
98
+ SessionSetting::LLM_CONTEXT.saved_summary(session, registry: host_registry)
99
+ end
100
+ Source.new(messages: messages, strategy: working_set_strategy(llm_context), cwd: session.working_directory,
101
+ pins: session.working_set_pins)
102
+ end
103
+
104
+ # The running turn's set from a worker's snapshot ({"items", "at"}), as
105
+ # {items:, at:}; nil when it has none (none emitted yet, an older worker).
106
+ def running_working_set(set)
107
+ return nil unless set.is_a?(Hash) && set["items"].is_a?(Array) && set["at"].is_a?(Integer)
108
+
109
+ { items: set["items"], at: set["at"] }
110
+ end
111
+
112
+ # The worker's between-turns working set ({"items", "at", optional
113
+ # "adds"}); nil for an older or malformed snapshot.
114
+ def idle_working_set(set)
115
+ return nil unless set.is_a?(Hash) && set["items"].is_a?(Array) && set["at"].is_a?(Integer)
116
+ return nil if set.key?("adds") && !set["adds"].is_a?(Array)
117
+
118
+ { items: set["items"], at: set["at"] }.tap do |snapshot|
119
+ snapshot[:adds] = set["adds"] if set.key?("adds")
120
+ end
121
+ end
122
+
123
+ # The strategy an llm_context summary names ("none", "stale,forget"), as
124
+ # LLMContextView takes it; none when there is no chip.
125
+ def working_set_strategy(llm_context)
126
+ named = llm_context.is_a?(Hash) ? (llm_context[:strategy] || llm_context["strategy"]).to_s : ""
127
+ layers = named.split(",").map(&:strip).reject { |layer| layer.empty? || layer == "none" }
128
+ layers.empty? ? LLMContextView::NONE : layers.map(&:to_sym)
129
+ end
130
+ end
131
+ end
132
+ end
@@ -1412,8 +1412,9 @@ module Samagotchi
1412
1412
  @turn_enqueued_id = origin.enqueued_id
1413
1413
  verdicts_before = @session.verdicts.size
1414
1414
  @reporting_turn = reports_to_parent?(origin)
1415
- # A participant's review turn merges no input (#pending_input_drain).
1416
- @participant_turn = origin.participant?
1415
+ # Who wrote the running turn (a participant's author, nil for the
1416
+ # owner's): its drain merges only their own lines (#pending_input_drain).
1417
+ @turn_author = origin.participant_author
1417
1418
  restores = @turn_flow.restores
1418
1419
  begin
1419
1420
  @checkpoint.begin_turn(prompt: prompt, turn_id: turn_args[:id], enqueued_id: origin.enqueued_id, author: turn_author_fields(origin))
@@ -1603,15 +1604,12 @@ module Samagotchi
1603
1604
  # one (steering merges text only): skipped here, not a barrier, so a
1604
1605
  # line sent after it still goes in at this boundary. Only the files
1605
1606
  # claimed below reach @merged_this_turn and the checkpoint.
1606
- # A participant's review turn takes none: the owner's lines (the
1607
- # Bridge queues them, this covers a file it didn't see) run as the
1608
- # owner's own turns after it, never inside its review boundary.
1609
- files = if @participant_turn
1610
- []
1611
- else
1612
- SessionInbox.find_new_input_files(@session_dir).sort
1613
- .reject { |input_file| SessionInbox.waits_for_turn_end?(input_file) }
1614
- end
1607
+ # A participant's turn takes only their own lines: the owner's and
1608
+ # anyone else's (the Bridge queues them, this covers a file it didn't
1609
+ # see) run as their own turns after it, never inside its boundary;
1610
+ # and the owner's turn takes no participant's (SessionInbox.turn_end_data?).
1611
+ files = SessionInbox.find_new_input_files(@session_dir).sort
1612
+ .reject { |input_file| SessionInbox.waits_for_turn_end?(input_file, running_author: @turn_author) }
1615
1613
  merged = files.filter_map do |input_file|
1616
1614
  claimed_file = SessionInbox.claim_input_file(input_file)
1617
1615
  next unless claimed_file
@@ -1624,6 +1622,13 @@ module Samagotchi
1624
1622
  next
1625
1623
  end
1626
1624
  begin
1625
+ # A participant's line gets the checks their queued turn gets at
1626
+ # its start (#participant_refusal: the link, the cap): refused,
1627
+ # it is taken off as that turn would be, and never merged.
1628
+ if (author = input.origin.participant_author) && (reason = participant_refusal(author))
1629
+ refuse_participant_turn(input.origin, reason)
1630
+ next
1631
+ end
1627
1632
  prompt = input.prompt.to_s.strip
1628
1633
  # Which turn took it, on disk before its file goes (a wait for
1629
1634
  # it reads this, not an inference: ReplyWait::Poll#follow_merge).
@@ -1660,10 +1665,10 @@ module Samagotchi
1660
1665
  end
1661
1666
 
1662
1667
  # Whether a person's message waits that the next boundary merges
1663
- # (SessionInbox.steer_waiting?): a running task_wait ends on it. Never
1664
- # in a participant's turn, whose drain merges none.
1668
+ # (SessionInbox.steer_waiting?): a running task_wait ends on it. In a
1669
+ # participant's turn only their own.
1665
1670
  def input_waiting?
1666
- !@participant_turn && SessionInbox.steer_waiting?(@session_dir)
1671
+ SessionInbox.steer_waiting?(@session_dir, running_author: @turn_author)
1667
1672
  end
1668
1673
 
1669
1674
  # A participant's author fields (Engine#author_fields), {} for the owner.