samagotchi 0.12.0 → 0.13.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +62 -1
  3. data/README.md +20 -7
  4. data/bin/chi +5 -2
  5. data/docs/architecture.md +4 -2
  6. data/docs/cli.md +34 -10
  7. data/docs/configuration.md +29 -8
  8. data/docs/desktop.md +10 -6
  9. data/docs/guardrails.md +73 -7
  10. data/docs/hooks.md +13 -9
  11. data/docs/memory.md +5 -3
  12. data/docs/plugins.md +4 -4
  13. data/docs/sessions.md +9 -6
  14. data/docs/sub-agent.md +128 -0
  15. data/lib/samagotchi/answer_command.rb +220 -0
  16. data/lib/samagotchi/bridge.rb +11 -1
  17. data/lib/samagotchi/bridge_client.rb +8 -3
  18. data/lib/samagotchi/bundle_command.rb +18 -10
  19. data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +19 -0
  20. data/lib/samagotchi/bundles/guardrails/manifest.yml +2 -2
  21. data/lib/samagotchi/bundles/system/config_modification_protocol.md +5 -5
  22. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  23. data/lib/samagotchi/cli/command.rb +3 -2
  24. data/lib/samagotchi/cli/exit.rb +23 -0
  25. data/lib/samagotchi/cli/parent_wait.rb +82 -0
  26. data/lib/samagotchi/config.rb +50 -18
  27. data/lib/samagotchi/desktop_command.rb +5 -3
  28. data/lib/samagotchi/engine.rb +40 -14
  29. data/lib/samagotchi/guardrails/parent_approvals.rb +155 -0
  30. data/lib/samagotchi/hooks/bundle_loader.rb +10 -3
  31. data/lib/samagotchi/host_registry.rb +0 -1
  32. data/lib/samagotchi/llm/openai_chat.rb +9 -1
  33. data/lib/samagotchi/memory_bundle/installer.rb +12 -0
  34. data/lib/samagotchi/memory_bundle/status.rb +9 -1
  35. data/lib/samagotchi/memory_paths.rb +8 -0
  36. data/lib/samagotchi/model_profile.rb +30 -16
  37. data/lib/samagotchi/parent_report.rb +213 -0
  38. data/lib/samagotchi/plugin/api.rb +15 -2
  39. data/lib/samagotchi/prompt_literal_guard.rb +4 -2
  40. data/lib/samagotchi/question_desk.rb +29 -2
  41. data/lib/samagotchi/recap_setup.rb +15 -5
  42. data/lib/samagotchi/reply_wait.rb +5 -1
  43. data/lib/samagotchi/send_command.rb +50 -75
  44. data/lib/samagotchi/session.rb +15 -0
  45. data/lib/samagotchi/session_manager.rb +34 -7
  46. data/lib/samagotchi/sessions_command.rb +13 -3
  47. data/lib/samagotchi/system_prompt.rb +10 -2
  48. data/lib/samagotchi/terminal_ui/attach_launcher.rb +33 -5
  49. data/lib/samagotchi/terminal_ui/attached_loop.rb +17 -5
  50. data/lib/samagotchi/terminal_ui.rb +15 -1
  51. data/lib/samagotchi/thought_stream_splitter.rb +4 -4
  52. data/lib/samagotchi/tool_call_parser.rb +5 -6
  53. data/lib/samagotchi/tools/builtin_calls.rb +14 -3
  54. data/lib/samagotchi/tools/builtins.rb +14 -2
  55. data/lib/samagotchi/tools/delegate_wait.rb +16 -4
  56. data/lib/samagotchi/tools/execute.rb +8 -8
  57. data/lib/samagotchi/tools/task_create.rb +3 -2
  58. data/lib/samagotchi/tools/task_runtime.rb +7 -2
  59. data/lib/samagotchi/version.rb +1 -1
  60. data/lib/samagotchi/web/app.rb +4 -3
  61. data/lib/samagotchi/web/message_parts.rb +2 -1
  62. data/lib/samagotchi/web/server.rb +2 -1
  63. data/lib/samagotchi/web/session_summary.rb +3 -11
  64. metadata +7 -1
@@ -15,6 +15,7 @@ require_relative "host_registry"
15
15
  require_relative "session"
16
16
  require_relative "owner_lock"
17
17
  require_relative "engine"
18
+ require_relative "guardrails/parent_approvals"
18
19
  require_relative "tools/memory"
19
20
  require_relative "output_formatter"
20
21
  require_relative "turn_flow"
@@ -1138,9 +1139,16 @@ module Samagotchi
1138
1139
  end
1139
1140
 
1140
1141
  begin
1141
- @engine.answer_question(id: prompt.id, selected: answer.selected, freeform: answer.freeform)
1142
+ parent = parent_answers? ? { client_id: Guardrails::ParentApprovals::CLIENT_ID } : {}
1143
+ @engine.answer_question(id: prompt.id, selected: answer.selected, freeform: answer.freeform, **parent)
1142
1144
  close_question_widget(prompt, prompt.answer_text(answer))
1143
1145
  return true
1146
+ rescue QuestionDesk::Refused => e
1147
+ # A parent's allow guardrails.parent_approvals doesn't let through:
1148
+ # the question stays open for a deny (or the end of the input).
1149
+ @surface.commit("#{paint(QUESTION_PROMPT, 33)}#{raw}")
1150
+ @surface.commit(Guardrails::ParentApprovals.message(e.reason, typed: true))
1151
+ next
1144
1152
  rescue ArgumentError => e
1145
1153
  @surface.commit("Invalid: #{e.message}. Try again.")
1146
1154
  next
@@ -1151,6 +1159,12 @@ module Samagotchi
1151
1159
  end
1152
1160
  end
1153
1161
 
1162
+ # Whether the answers typed here are a parent agent's (piped stdin, an
1163
+ # agent marker): QuestionDesk holds them to guardrails.parent_approvals.
1164
+ def parent_answers?
1165
+ Guardrails::ParentApprovals.parent_process?
1166
+ end
1167
+
1154
1168
  def close_question_widget(prompt, outcome)
1155
1169
  @surface.clear_slot(:notes)
1156
1170
  @surface.commit(prompt.summary(outcome, paint: method(:paint)))
@@ -25,15 +25,15 @@ module Samagotchi
25
25
  # not a block and stays in `:text` (the model doesn't emit it; the system
26
26
  # prompt carries it).
27
27
  class ThoughtStreamSplitter
28
- # Gemma 4's thought channel (ToolCallParser::Gemma strips the same pair).
29
- GEMMA_THOUGHT_CHANNEL = { open: "<|channel>thought", close: "<channel|>" }.freeze
30
-
31
28
  # @return [Array<Hash>] each entry { open: String, close: String, lane: :thinking|:drop }
32
29
  def self.for_profile(profile)
33
30
  blocks = []
34
31
  # A thinking block needs a close marker to be bounded.
35
32
  blocks << { open: profile.thought_open, close: profile.thought_close, lane: :thinking } if profile.thought_close
36
- blocks << GEMMA_THOUGHT_CHANNEL.merge(lane: :thinking) if profile.name == "gemma4"
33
+ # Gemma 4's thought channel (ToolCallParser::Gemma strips the same pair).
34
+ if profile.thought_channel_open
35
+ blocks << { open: profile.thought_channel_open, close: profile.thought_channel_close, lane: :thinking }
36
+ end
37
37
  blocks << { open: profile.tool_call_open, close: profile.tool_call_close, lane: :drop }
38
38
  new(blocks)
39
39
  end
@@ -38,8 +38,8 @@ module Samagotchi
38
38
  # Digits after the first character: plugin tools may have them
39
39
  # (Plugin::Api::TOOL_NAME).
40
40
  TOOL_CALL_BODY_RE = /\Acall:([a-z_][a-z0-9_]{0,49})\{/
41
- THOUGHT_CHANNEL_OPEN = "<|channel>thought"
42
- THOUGHT_CHANNEL_CLOSE = "<channel|>"
41
+ THOUGHT_CHANNEL_OPEN = ModelProfile::GEMMA_THOUGHT_CHANNEL_OPEN
42
+ THOUGHT_CHANNEL_CLOSE = ModelProfile::GEMMA_THOUGHT_CHANNEL_CLOSE
43
43
  CONTROL_TOKEN_START = "<|"
44
44
 
45
45
  def initialize(profile)
@@ -305,10 +305,10 @@ module Samagotchi
305
305
  end
306
306
 
307
307
  def parse(text)
308
- read(text).map { |call| Tools::BuiltinCalls.build(call[:name], call[:args], raw: call[:raw]) }
308
+ read(text).map { |call| Tools::BuiltinCalls.build(call[:name], call[:args]) }
309
309
  end
310
310
 
311
- # Each <tool_call> block as {name:, args:, raw:}: its <parameter=…>
311
+ # Each <tool_call> block as {name:, args:}: its <parameter=…>
312
312
  # (or <arg_key>/<arg_value>) text by lowercased key, unbuilt.
313
313
  def read(text)
314
314
  results = []
@@ -324,8 +324,7 @@ module Samagotchi
324
324
 
325
325
  body = text[body_start...close_pos]
326
326
  if (name = qwen_function_name(body))
327
- params = qwen_params(body)
328
- results << { name: name, args: params, raw: params.to_s }
327
+ results << { name: name, args: qwen_params(body) }
329
328
  end
330
329
  pos = close_pos + tool_close.length
331
330
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
3
4
  require_relative "../tool_declarations"
4
5
  require_relative "ask_user_question"
5
6
 
@@ -18,7 +19,7 @@ module Samagotchi
18
19
  # and the handler reading it.
19
20
  #
20
21
  # A tool that is not built in keeps its arguments whole on args: (typed
21
- # by its schema at dispatch, Tools::Args) with +raw+ as its content.
22
+ # by its schema at dispatch, Tools::Args) with their JSON as its content.
22
23
  module BuiltinCalls
23
24
  # Per tool, what the schema alone doesn't say:
24
25
  # content: the property (or ordered list of keys to try) that fills
@@ -103,7 +104,7 @@ module Samagotchi
103
104
  # @param name [String]
104
105
  # @param args [Hash] string keys, as the model gave them
105
106
  # @param raw [String, nil] the call's text, the content of a tool
106
- # that isn't built in
107
+ # that isn't built in when no argument could be read from it
107
108
  # @return [Hash]
108
109
  def build(name, args, raw: nil)
109
110
  name = name.to_s
@@ -139,7 +140,17 @@ module Samagotchi
139
140
  end
140
141
 
141
142
  def passthrough(name, args, raw)
142
- { name: name, content: raw.nil? ? args.values.join(" ") : raw.to_s, path: nil, scope: nil, args: args }
143
+ { name: name, content: passthrough_content(args, raw), path: nil, scope: nil, args: args }
144
+ end
145
+
146
+ # The arguments as JSON, one form for every model format and Ruby
147
+ # version; the raw text when none could be read from it.
148
+ def passthrough_content(args, raw)
149
+ return raw.to_s if args.empty? && !raw.to_s.empty?
150
+
151
+ JSON.generate(args)
152
+ rescue JSON::GeneratorError
153
+ args.values.join(" ")
143
154
  end
144
155
  end
145
156
  end
@@ -21,6 +21,7 @@ require_relative "send_note"
21
21
  require_relative "delegate"
22
22
  require_relative "delegate_result"
23
23
  require_relative "ask_user_question"
24
+ require_relative "../guardrails/parent_approvals"
24
25
 
25
26
  module Samagotchi
26
27
  module Tools
@@ -72,13 +73,15 @@ module Samagotchi
72
73
  TaskCreate::NAME => lambda do |call, kctx|
73
74
  next Execute::NOT_RUN_ON_STOP if cancelled_proc(kctx).call
74
75
 
75
- TaskCreate.call(call[:content], cwd: call[:cwd], env: call[:env])
76
+ TaskCreate.call(call[:content], cwd: call[:cwd], env: call[:env], marker_env: parent_env(kctx))
76
77
  end,
77
78
  TaskWait::NAME => lambda do |call, kctx|
78
79
  TaskWait.call(call[:content], timeout: call[:timeout], tail_lines: call[:tail_lines],
79
80
  done_pattern: call[:done_pattern], cancelled: cancelled_proc(kctx))
80
81
  end,
81
- Execute::NAME => ->(call, kctx) { Execute.call(call[:content], cwd: call[:cwd], cancelled: cancelled_proc(kctx)) },
82
+ Execute::NAME => lambda do |call, kctx|
83
+ Execute.call(call[:content], cwd: call[:cwd], cancelled: cancelled_proc(kctx), env: parent_env(kctx))
84
+ end,
82
85
  RegisterReminder::NAME => lambda do |call, kctx|
83
86
  RegisterReminder.call(call[:content], reminder_store: kctx.reminder_store, description: call[:description],
84
87
  interval_minutes: call[:interval_minutes])
@@ -99,6 +102,15 @@ module Samagotchi
99
102
 
100
103
  module_function
101
104
 
105
+ # What execute and task_create export into the commands they run: a
106
+ # chi started there answers as a parent agent
107
+ # (Guardrails::ParentApprovals.parent_process?), even under a PTY
108
+ # wrapper. The session's id, "chi" without one.
109
+ def parent_env(kctx)
110
+ id = kctx.peers.respond_to?(:session_id) ? kctx.peers.session_id.to_s : ""
111
+ { Guardrails::ParentApprovals::PARENT_SESSION_ENV => id.empty? ? "chi" : id }
112
+ end
113
+
102
114
  # Stop flips the turn's controller, seen through the Engine's PeerView
103
115
  # (as DelegateWait does); a bare kernel has no peers: never cancelled.
104
116
  def cancelled_proc(kctx)
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../session"
4
4
  require_relative "../reply_wait"
5
+ require_relative "../parent_report"
5
6
  require_relative "output_guardrails"
6
7
  require_relative "peers"
7
8
 
@@ -18,6 +19,10 @@ module Samagotchi
18
19
  module DelegateWait
19
20
  TIMEOUT_DEFAULT = 600
20
21
  POLL_INTERVAL = 0.5
22
+ # Seconds with no worker before the child counts as gone: as long as
23
+ # Delegate gives a starting child (its worker takes the owner lock
24
+ # only once it has booted).
25
+ OWNER_GRACE = 15
21
26
  # Cut like an execute result: full up to TRUNCATE_AT bytes, else a
22
27
  # head-and-tail preview of PREVIEW bytes.
23
28
  TRUNCATE_AT_BYTES = OutputGuardrails::DEFAULT_TRUNCATE_AT_BYTES
@@ -39,13 +44,16 @@ module Samagotchi
39
44
  # @param peers [Peers] the parent (session_id, state_dir, cancelled?)
40
45
  # @param timeout [Integer] seconds
41
46
  # @param poll_interval [Float]
47
+ # @param owner_grace [Numeric] seconds with no live worker before
48
+ # the child's worker counts as gone
42
49
  # @return [String] the tool result
43
- def call(child_id, peers:, timeout: TIMEOUT_DEFAULT, poll_interval: POLL_INTERVAL)
50
+ def call(child_id, peers:, timeout: TIMEOUT_DEFAULT, poll_interval: POLL_INTERVAL, owner_grace: OWNER_GRACE)
44
51
  sd = peers.state_dir || Session.default_state_dir
45
52
  key = [peers.session_id, child_id]
46
53
  cancelled = -> { peers.cancelled? }
47
54
  wait = ReplyWait.call(child_id, state_dir: sd, cursor: seen[key], timeout: timeout.to_i,
48
- poll_interval: poll_interval, cancelled: cancelled, baseline: baselines[key])
55
+ poll_interval: poll_interval, cancelled: cancelled, baseline: baselines[key],
56
+ owner_grace: owner_grace)
49
57
  # The turn sent to is handed over: a later wait looks for a later one.
50
58
  baselines.delete(key) if %i[done no_reply].include?(wait.status)
51
59
  case wait.status
@@ -62,6 +70,9 @@ module Samagotchi
62
70
  result(child_id, "canceled", "wait canceled; the child keeps running; delegate_result #{child_id} waits again")
63
71
  when :no_reply
64
72
  result(child_id, "no_reply", "the child's turn ended without a reply (canceled, failed or empty); its session shows what happened")
73
+ when :worker_gone
74
+ result(child_id, "worker_gone",
75
+ "the child's worker is gone (it stopped or crashed); delegate with session: #{child_id} starts it again with a message")
65
76
  else
66
77
  timeout_result(child_id, timeout)
67
78
  end
@@ -98,9 +109,10 @@ module Samagotchi
98
109
  "chi --attach #{child_id} shows it.")
99
110
  end
100
111
 
112
+ # The whole question, as `chi send --wait` prints it (ParentReport):
113
+ # its text, options and the commands that answer it.
101
114
  def waiting_text(child_id, pending)
102
- what = pending[:kind].to_s == "approval" ? "an approval" : "a question"
103
- "Child #{child_id} is waiting for an answer (#{what}); attach with chi --attach #{child_id} or answer it in the web. " \
115
+ "Child #{child_id} is #{ParentReport.question_text(pending, session_id: child_id)}" \
104
116
  "delegate_result #{child_id} waits again once it is answered."
105
117
  end
106
118
 
@@ -25,7 +25,8 @@ module Samagotchi
25
25
 
26
26
  # @param cancelled [#call] true once the user stopped the turn: the
27
27
  # command is killed (or not started) and its output so far returned
28
- def self.call(command, cwd: nil, cancelled: -> { false })
28
+ # @param env [Hash, nil] added to the command's environment
29
+ def self.call(command, cwd: nil, cancelled: -> { false }, env: nil)
29
30
  return NOT_RUN_ON_STOP if cancelled.call
30
31
 
31
32
  command = command.strip
@@ -33,7 +34,8 @@ module Samagotchi
33
34
  return "Error: cwd not found: #{resolved_cwd}" unless resolved_cwd
34
35
 
35
36
  timeout_sec = timeout_seconds
36
- stdout, stderr, status = run_command(command, timeout_sec: timeout_sec, cwd: resolved_cwd, cancelled: cancelled)
37
+ stdout, stderr, status = run_command(command, timeout_sec: timeout_sec, cwd: resolved_cwd, cancelled: cancelled,
38
+ env: env)
37
39
 
38
40
  stdout_block = output_block("stdout", stdout)
39
41
  stderr_block = output_block("stderr", stderr)
@@ -66,7 +68,7 @@ module Samagotchi
66
68
  end
67
69
  private_class_method :stopped_result
68
70
 
69
- def self.run_command(command, timeout_sec:, cwd:, cancelled: -> { false })
71
+ def self.run_command(command, timeout_sec:, cwd:, cancelled: -> { false }, env: nil)
70
72
  stdout_text = ""
71
73
  stderr_text = ""
72
74
  status = nil
@@ -75,7 +77,7 @@ module Samagotchi
75
77
  started = monotonic_time
76
78
  deadline = started + timeout_sec
77
79
 
78
- Open3.popen3(command, chdir: cwd, pgroup: true) do |stdin, stdout, stderr, wait_thr|
80
+ Open3.popen3(env || {}, command, chdir: cwd, pgroup: true) do |stdin, stdout, stderr, wait_thr|
79
81
  stdin.close
80
82
  stdout_reader = reader_thread_for(stdout)
81
83
  stderr_reader = reader_thread_for(stderr)
@@ -173,11 +175,9 @@ module Samagotchi
173
175
  end
174
176
  private_class_method :monotonic_time
175
177
 
178
+ # execute.timeout_sec (SAMAGOTCHI_EXECUTE_TIMEOUT_SEC); 0 or less = the default.
176
179
  def self.timeout_seconds
177
- value = Integer(ENV["SAMAGOTCHI_EXECUTE_TIMEOUT_SEC"].to_s, exception: false)
178
- return TIMEOUT_SEC if value.nil? || value <= 0
179
-
180
- value
180
+ OutputGuardrails.config_positive_int("execute.timeout_sec", TIMEOUT_SEC)
181
181
  end
182
182
  private_class_method :timeout_seconds
183
183
 
@@ -9,8 +9,9 @@ module Samagotchi
9
9
 
10
10
  def self.name = NAME
11
11
 
12
- def self.call(command, cwd: nil, env: nil)
13
- record, error = TaskRuntime.create_task(command, cwd: cwd, env: env)
12
+ # @param marker_env [Hash] set after the model's env (Builtins.parent_env)
13
+ def self.call(command, cwd: nil, env: nil, marker_env: {})
14
+ record, error = TaskRuntime.create_task(command, cwd: cwd, env: env, marker_env: marker_env)
14
15
  return error if error
15
16
 
16
17
  [
@@ -19,10 +19,13 @@ module Samagotchi
19
19
  METADATA_VERSION = 1
20
20
  OUTPUT_TAIL_READ_BYTES = 16 * 1024
21
21
  SANITIZED_ENV_KEYS = %w[RUBYOPT RUBYLIB BUNDLE_GEMFILE BUNDLE_BIN_PATH BUNDLER_VERSION].freeze
22
+ # Set by chi (Builtins.parent_env); the model's env can't change it.
23
+ MARKER_ENV_KEYS = %w[SAMAGOTCHI_PARENT_SESSION].freeze
22
24
 
23
25
  module_function
24
26
 
25
- def create_task(command, cwd: nil, env: nil)
27
+ # @param marker_env [Hash] chi's own variables, set over the model's +env+
28
+ def create_task(command, cwd: nil, env: nil, marker_env: {})
26
29
  normalized_command = command.to_s.strip
27
30
  return [nil, "Error: command is required"] if normalized_command.empty?
28
31
 
@@ -32,6 +35,8 @@ module Samagotchi
32
35
  spawn_env, env_error = spawn_env_for(env)
33
36
  return [nil, env_error] if env_error
34
37
 
38
+ spawn_env = spawn_env.merge(marker_env || {})
39
+
35
40
  task_id = generate_task_id
36
41
  task_dir = task_dir_for(task_id)
37
42
  output_path = File.join(task_dir, OUTPUT_FILENAME)
@@ -258,7 +263,7 @@ module Samagotchi
258
263
  overrides.each do |key, value|
259
264
  return [nil, "Error: env keys and values must be strings"] unless key.is_a?(String) && value.is_a?(String)
260
265
  return [nil, "Error: env keys and values cannot contain NUL bytes"] if key.include?("\0") || value.include?("\0")
261
- return [nil, "Error: env key is reserved: #{key}"] if SANITIZED_ENV_KEYS.include?(key)
266
+ return [nil, "Error: env key is reserved: #{key}"] if SANITIZED_ENV_KEYS.include?(key) || MARKER_ENV_KEYS.include?(key)
262
267
 
263
268
  normalized[key] = value
264
269
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Samagotchi
4
- VERSION = "0.12.0"
4
+ VERSION = "0.13.0"
5
5
  # Sent as the User-Agent of every HTTP request chi makes.
6
6
  USER_AGENT = "chi/#{VERSION}"
7
7
  end
@@ -539,11 +539,12 @@ module Samagotchi
539
539
  last_event_seq = snapshot ? snapshot["event_seq"] : bridge_event_seq(id)
540
540
  current_turn = turn_snapshot && turn_snapshot["current_turn"]
541
541
  owner = session_owner(id)
542
- # The file's question only with a live owner: one a dead worker
543
- # saved can't be answered (its turn is gone).
542
+ # The file's question only while a worker owns the session: one a
543
+ # dead worker saved can't be answered (its turn is gone), nor a chi
544
+ # REPL's (it shares nothing with the web).
544
545
  pending = if turn_snapshot
545
546
  current_turn && current_turn["pending_question"]
546
- elsif owner
547
+ elsif owner&.worker?
547
548
  session.pending_question
548
549
  end
549
550
  # A /model in the worker changes it before the file catches up.
@@ -46,7 +46,8 @@ module Samagotchi
46
46
 
47
47
  QWEN_THINK = %r{<think>(.*?)</think>}m
48
48
  QWEN_OPEN_THINK = %r{\A(.*?)</think>}m # the template opened the block
49
- GEMMA_THOUGHT = /<\|channel>thought(.*?)(?:<channel\|>|\z)/m
49
+ GEMMA_THOUGHT = Regexp.new("#{Regexp.escape(ModelProfile::GEMMA_THOUGHT_CHANNEL_OPEN)}(.*?)" \
50
+ "(?:#{Regexp.escape(ModelProfile::GEMMA_THOUGHT_CHANNEL_CLOSE)}|\\z)", Regexp::MULTILINE)
50
51
 
51
52
  CallRef = Struct.new(:name, :arguments)
52
53
 
@@ -16,6 +16,7 @@ require_relative "../config"
16
16
  require_relative "../project_scope"
17
17
  require_relative "../log"
18
18
  require_relative "../version"
19
+ require_relative "../cli/exit"
19
20
 
20
21
  module Samagotchi
21
22
  module Web
@@ -99,7 +100,7 @@ module Samagotchi
99
100
  # Ctrl-C: the server has stopped (start's ensure); one line, and the
100
101
  # status a shell gives a command it interrupted.
101
102
  puts "#{"\n" if $stdout.tty?}Chi Web stopped."
102
- 130
103
+ CLI::Exit::INTERRUPTED
103
104
  end
104
105
 
105
106
  # @param hub [SessionHub, nil] the session projection the page streams
@@ -61,22 +61,14 @@ module Samagotchi
61
61
  bridge_up: up,
62
62
  # For the tab's notifications (notify.js): an open question, the
63
63
  # running turn's open card with actions, and how the last turn
64
- # ended. A question saved by a worker that died is not open.
65
- pending_question: owner ? pending_question_for(session) : nil,
64
+ # ended. A question saved by a worker that died is not open, nor a
65
+ # chi REPL's (SessionManager.worker_live?'s rule: only a worker shares it).
66
+ pending_question: session.waiting_question(live: !!owner&.worker?),
66
67
  pending_card: up ? Bridge::PendingCard.read(session_dir) : nil,
67
68
  last_turn: session.last_turn
68
69
  }
69
70
  end
70
71
 
71
- # The open question as {id:, kind:} ("approval" for a guardrail's,
72
- # else "question"); nil when there is none. The text stays out.
73
- def pending_question_for(session)
74
- pending = session.pending_question
75
- return nil unless pending.is_a?(Hash) && pending[:id]
76
-
77
- { id: pending[:id], kind: pending[:kind].to_s == "approval" ? "approval" : "question" }
78
- end
79
-
80
72
  # status is turn state (idle/running). The live worker's snapshot is the
81
73
  # truth; on disk, a "running" with no live owner was left by a worker
82
74
  # that died mid-turn.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: samagotchi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.0
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Dedov
@@ -136,7 +136,9 @@ files:
136
136
  - docs/plugins.md
137
137
  - docs/releasing.md
138
138
  - docs/sessions.md
139
+ - docs/sub-agent.md
139
140
  - lib/samagotchi.rb
141
+ - lib/samagotchi/answer_command.rb
140
142
  - lib/samagotchi/answer_display.rb
141
143
  - lib/samagotchi/archive_store.rb
142
144
  - lib/samagotchi/atomic_file.rb
@@ -188,7 +190,9 @@ files:
188
190
  - lib/samagotchi/bundles/system/self_map.md
189
191
  - lib/samagotchi/cancellation_controller.rb
190
192
  - lib/samagotchi/cli/command.rb
193
+ - lib/samagotchi/cli/exit.rb
191
194
  - lib/samagotchi/cli/flags.rb
195
+ - lib/samagotchi/cli/parent_wait.rb
192
196
  - lib/samagotchi/client.rb
193
197
  - lib/samagotchi/commands/registry.rb
194
198
  - lib/samagotchi/config.rb
@@ -224,6 +228,7 @@ files:
224
228
  - lib/samagotchi/guardrails/gate.rb
225
229
  - lib/samagotchi/guardrails/load_failures.rb
226
230
  - lib/samagotchi/guardrails/model_size.rb
231
+ - lib/samagotchi/guardrails/parent_approvals.rb
227
232
  - lib/samagotchi/guardrails/protected_paths.rb
228
233
  - lib/samagotchi/guardrails/rules.rb
229
234
  - lib/samagotchi/guardrails/scratch_writes.rb
@@ -286,6 +291,7 @@ files:
286
291
  - lib/samagotchi/note_command.rb
287
292
  - lib/samagotchi/output_formatter.rb
288
293
  - lib/samagotchi/owner_lock.rb
294
+ - lib/samagotchi/parent_report.rb
289
295
  - lib/samagotchi/paths.rb
290
296
  - lib/samagotchi/pending_input_queue.rb
291
297
  - lib/samagotchi/plugin/api.rb