samagotchi 0.91.0 → 0.93.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +88 -1
- data/README.md +5 -2
- data/bin/chi +8 -8
- data/docs/architecture.md +20 -16
- data/docs/bundle-authoring.md +2 -1
- data/docs/cli.md +5 -5
- data/docs/configuration.md +3 -1
- data/docs/context.md +5 -3
- data/docs/guardrails.md +4 -5
- data/docs/hooks.md +3 -3
- data/docs/internals/background-tasks.md +2 -2
- data/docs/internals/bundles.md +4 -4
- data/docs/internals/engine.md +27 -8
- data/docs/internals/tool-guardrails.md +10 -2
- data/docs/memory.md +6 -0
- data/docs/plugins.md +30 -13
- data/docs/releasing.md +3 -0
- data/docs/sessions.md +4 -4
- data/docs/testing.md +2 -1
- data/lib/samagotchi/bootstrap/probe.rb +2 -2
- data/lib/samagotchi/bridge.rb +8 -9
- data/lib/samagotchi/bundle_command/check.rb +43 -12
- data/lib/samagotchi/bundle_command/scaffold.rb +12 -0
- data/lib/samagotchi/bundle_command/test_run.rb +12 -0
- data/lib/samagotchi/bundle_command/try/command.rb +164 -0
- data/lib/samagotchi/bundle_command/try/render.rb +80 -0
- data/lib/samagotchi/bundle_command/try/run.rb +35 -17
- data/lib/samagotchi/bundle_command/try/sandbox.rb +113 -34
- data/lib/samagotchi/bundle_command/try/trace.rb +20 -26
- data/lib/samagotchi/bundle_command.rb +11 -213
- data/lib/samagotchi/bundle_session_settings.rb +82 -45
- data/lib/samagotchi/bundles/core/manifest.yml +2 -2
- data/lib/samagotchi/bundles/cost-cap/manifest.yml +6 -5
- data/lib/samagotchi/bundles/cost-cap/plugin.rb +57 -12
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/child_env.rb +50 -0
- data/lib/samagotchi/child_reports.rb +12 -14
- data/lib/samagotchi/client_id.rb +18 -0
- data/lib/samagotchi/config/restart_watch.rb +112 -0
- data/lib/samagotchi/config.rb +18 -59
- data/lib/samagotchi/engine.rb +26 -26
- data/lib/samagotchi/guardrails/data_readers.rb +34 -30
- data/lib/samagotchi/guardrails/parent_levels.rb +31 -0
- data/lib/samagotchi/guardrails/parent_policy.rb +9 -20
- data/lib/samagotchi/guardrails/shell_lex.rb +28 -7
- data/lib/samagotchi/hook_question.rb +21 -0
- data/lib/samagotchi/hook_routes.rb +6 -6
- data/lib/samagotchi/hooks/registry.rb +7 -5
- data/lib/samagotchi/host_registry.rb +1 -1
- data/lib/samagotchi/kernel_loop.rb +14 -3
- data/lib/samagotchi/llm/anthropic_messages.rb +5 -0
- data/lib/samagotchi/llm/chat_loop.rb +3 -1
- data/lib/samagotchi/llm/errors.rb +4 -4
- data/lib/samagotchi/memory_bundle/builder.rb +3 -2
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +7 -2
- data/lib/samagotchi/memory_bundle/installer.rb +1 -0
- data/lib/samagotchi/memory_bundle/manifest.rb +42 -8
- data/lib/samagotchi/memory_bundle/provenance.rb +2 -1
- data/lib/samagotchi/parent_answers_command.rb +6 -6
- data/lib/samagotchi/parent_report.rb +12 -0
- data/lib/samagotchi/plugin/context.rb +6 -5
- data/lib/samagotchi/plugin/http_transport.rb +14 -3
- data/lib/samagotchi/plugin/sessions.rb +2 -3
- data/lib/samagotchi/question_desk.rb +12 -9
- data/lib/samagotchi/reply_wait/poll.rb +267 -0
- data/lib/samagotchi/reply_wait.rb +51 -190
- data/lib/samagotchi/sandbox_layout.rb +66 -0
- data/lib/samagotchi/send_command/parent_gate.rb +83 -0
- data/lib/samagotchi/send_command/wait_target.rb +55 -0
- data/lib/samagotchi/send_command.rb +34 -103
- data/lib/samagotchi/session.rb +10 -9
- data/lib/samagotchi/session_inbox.rb +31 -8
- data/lib/samagotchi/session_manager.rb +60 -35
- data/lib/samagotchi/session_metrics.rb +22 -3
- data/lib/samagotchi/sessions_command.rb +4 -4
- data/lib/samagotchi/task_watch.rb +64 -21
- data/lib/samagotchi/terminal_ui/attached_loop.rb +3 -3
- data/lib/samagotchi/terminal_ui/formatting.rb +8 -5
- data/lib/samagotchi/testing/bundle.rb +33 -20
- data/lib/samagotchi/testing/host.rb +2 -8
- data/lib/samagotchi/testing/install.rb +26 -17
- data/lib/samagotchi/testing/recorder.rb +6 -9
- data/lib/samagotchi/testing/sandbox.rb +45 -37
- data/lib/samagotchi/testing/waiting.rb +5 -7
- data/lib/samagotchi/tool_response.rb +28 -6
- data/lib/samagotchi/tools/delegate.rb +10 -10
- data/lib/samagotchi/tools/delegate_cursor.rb +32 -8
- data/lib/samagotchi/tools/delegate_relay.rb +31 -16
- data/lib/samagotchi/tools/delegate_wait.rb +4 -4
- data/lib/samagotchi/tools/task_runtime.rb +2 -2
- data/lib/samagotchi/turn_checkpoint.rb +23 -10
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +46 -63
- data/lib/samagotchi/web/public/app.js +130 -554
- data/lib/samagotchi/web/public/cards_place.js +4 -1
- data/lib/samagotchi/web/public/images.js +6 -0
- data/lib/samagotchi/web/public/index.html +1 -1
- data/lib/samagotchi/web/public/model_pick.js +10 -0
- data/lib/samagotchi/web/public/send_turn.js +173 -0
- data/lib/samagotchi/web/public/session_actions.js +203 -0
- data/lib/samagotchi/web/public/start_page.js +236 -0
- data/lib/samagotchi/web/public/toasts.js +17 -9
- data/lib/samagotchi/web/public/unqueue.js +20 -4
- data/lib/samagotchi/worker.rb +77 -185
- data/lib/samagotchi/worker_idle_exit.rb +20 -5
- data/lib/samagotchi/worker_inbound.rb +5 -5
- data/lib/samagotchi/worker_task_wakes.rb +160 -0
- data/lib/samagotchi/worker_wakes.rb +0 -3
- metadata +15 -1
|
@@ -28,20 +28,42 @@ module Samagotchi
|
|
|
28
28
|
|
|
29
29
|
# Runs +calls+ in order (ToolRunner#run each), yielding each run and its
|
|
30
30
|
# index as it finishes. +emit+ takes the dispatch events, the runner's
|
|
31
|
-
# own go to +on_stream_event+.
|
|
31
|
+
# own go to +on_stream_event+. An Interrupt (Ctrl-C) mid-batch yields an
|
|
32
|
+
# #interrupted run for every call left (the one it cut short too), so
|
|
33
|
+
# the loop's conversation answers each call, then goes on up: the turn
|
|
34
|
+
# keeps its progress (Engine#ctrl_c_messages).
|
|
32
35
|
# @return [Array<ToolRun>] the runs
|
|
33
36
|
def run_batch(runner, calls, iteration:, emit:, on_stream_event:, cap:)
|
|
37
|
+
# Before anything can be interrupted: the rescue counts it.
|
|
38
|
+
runs = []
|
|
34
39
|
emit.call({ type: :tool_dispatch_started, iteration: iteration, call_count: calls.length })
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
yield
|
|
39
|
-
run
|
|
40
|
+
calls.each_with_index do |call, index|
|
|
41
|
+
runs << runner.run(call, iteration: iteration, call_index: index + 1, call_count: calls.length,
|
|
42
|
+
on_stream_event: on_stream_event, max_tool_output_chars: cap)
|
|
43
|
+
yield runs.last, index if block_given?
|
|
40
44
|
end
|
|
41
45
|
emit.call({ type: :tool_dispatch_completed, iteration: iteration, call_count: calls.length })
|
|
42
46
|
runs
|
|
47
|
+
rescue Interrupt
|
|
48
|
+
calls.each_with_index.drop(runs.size).each { |call, index| yield interrupted(call[:name]), index } if block_given?
|
|
49
|
+
raise
|
|
43
50
|
end
|
|
44
51
|
|
|
52
|
+
# The result of a call an Interrupt cut short (or never started): the
|
|
53
|
+
# model reads that it may have partly run.
|
|
54
|
+
def interrupted(name)
|
|
55
|
+
text = interrupted_text(name)
|
|
56
|
+
ToolRun.new(output: text, capped_output: text)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
INTERRUPTED = "Interrupted (Ctrl-C) before it returned: it may have partly run."
|
|
60
|
+
|
|
61
|
+
def interrupted_text(name) = "[#{name}] #{INTERRUPTED}"
|
|
62
|
+
|
|
63
|
+
# Whether +content+ is an #interrupted result, whatever tool it names
|
|
64
|
+
# (an aliased call's saved name is the model's, the text the tool's).
|
|
65
|
+
def interrupted_text?(content) = content.to_s.match?(/\A\[[^\]\s]+\] #{Regexp.escape(INTERRUPTED)}\z/)
|
|
66
|
+
|
|
45
67
|
# The runs' activity, for the turn's tool list (none for a call whose
|
|
46
68
|
# dispatcher raised).
|
|
47
69
|
def activities(runs)
|
|
@@ -234,11 +234,11 @@ module Samagotchi
|
|
|
234
234
|
DelegateWait.mark_seen(parent.id, child, state_dir: state_dir)
|
|
235
235
|
delivered = SessionManager.deliver_turn(id, prompt: task, client_id: "#{ClientId::DELEGATE_PREFIX}#{parent.id[0, 8]}",
|
|
236
236
|
state_dir: state_dir)
|
|
237
|
-
case delivered
|
|
237
|
+
case delivered.status
|
|
238
238
|
when :accepted
|
|
239
|
-
DelegateWait.mark_turn(parent.id, id, follow_up_turn(id, delivered
|
|
239
|
+
DelegateWait.mark_turn(parent.id, id, follow_up_turn(id, delivered.enqueued_id, state_dir: state_dir), state_dir: state_dir)
|
|
240
240
|
id
|
|
241
|
-
when :refused then "Error: session #{id[0, 8]} refused the message (#{delivered.
|
|
241
|
+
when :refused then "Error: session #{id[0, 8]} refused the message (#{delivered.error})"
|
|
242
242
|
when :timeout then "Error: the worker of session #{id[0, 8]} did not answer in time, so the message was not sent"
|
|
243
243
|
else "Error: the message to session #{id[0, 8]} could not be written"
|
|
244
244
|
end
|
|
@@ -251,24 +251,24 @@ module Samagotchi
|
|
|
251
251
|
|
|
252
252
|
# The child's turn a delivered follow-up joins, for the wait
|
|
253
253
|
# (DelegateCursor#turn): the running turn's (its checkpoint mark), the
|
|
254
|
-
# follow-up merging into it; else the
|
|
255
|
-
# running as a turn of its own. Only
|
|
256
|
-
# turn_ids; nil otherwise, or when the
|
|
257
|
-
# delegate's first turn): the wait takes the
|
|
254
|
+
# follow-up merging into it; else the delivery's +enqueued_id+, the
|
|
255
|
+
# follow-up running as a turn of its own. Only
|
|
256
|
+
# from a worker that names turn_ids; nil otherwise, or when the
|
|
257
|
+
# running turn has no id (a delegate's first turn): the wait takes the
|
|
258
|
+
# next reply.
|
|
258
259
|
# A follow-up steered into a turn that ended before it merged runs as
|
|
259
260
|
# its own turn, yet the wait is on the ended one (LOW, as before).
|
|
260
261
|
# One the child's idle look missed being merged into a turn starting
|
|
261
262
|
# then never runs as its own: ReplyWait falls back to that turn's
|
|
262
263
|
# reply (merged_elsewhere?).
|
|
263
264
|
# @return [String, nil]
|
|
264
|
-
def self.follow_up_turn(id,
|
|
265
|
+
def self.follow_up_turn(id, enqueued_id, state_dir:)
|
|
265
266
|
return nil unless ReplyWait.turn_ids?(Session.session_dir(id, state_dir: state_dir))
|
|
266
267
|
|
|
267
268
|
child = Session.load(id, state_dir: state_dir)
|
|
268
269
|
return child.interrupted_turn&.enqueued_id if child.status == Session::STATUS_RUNNING
|
|
269
270
|
|
|
270
|
-
enqueued_id
|
|
271
|
-
enqueued_id.to_s.empty? ? nil : enqueued_id.to_s
|
|
271
|
+
enqueued_id
|
|
272
272
|
rescue ArgumentError
|
|
273
273
|
nil
|
|
274
274
|
end
|
|
@@ -4,6 +4,7 @@ require "json"
|
|
|
4
4
|
require "fileutils"
|
|
5
5
|
require_relative "../atomic_file"
|
|
6
6
|
require_relative "../session"
|
|
7
|
+
require_relative "../reply_wait"
|
|
7
8
|
|
|
8
9
|
module Samagotchi
|
|
9
10
|
module Tools
|
|
@@ -14,32 +15,54 @@ module Samagotchi
|
|
|
14
15
|
# of the child's turn the last follow-up went to (Delegate.follow_up),
|
|
15
16
|
# whose own reply a wait takes (ReplyWait's turn:); nil once that turn
|
|
16
17
|
# was reported, or when unknown (a new child, an older worker).
|
|
17
|
-
# +report_claim+: a report reserved until the parent's turn is settled
|
|
18
|
+
# +report_claim+: a report reserved until the parent's turn is settled
|
|
19
|
+
# (ReportClaim).
|
|
18
20
|
DelegateCursor = Data.define(:reply_file, :last_turn, :question_id, :messages, :turn, :report_claim) do
|
|
19
21
|
def initialize(reply_file: nil, last_turn: nil, question_id: nil, messages: nil, turn: nil, report_claim: nil) = super
|
|
20
22
|
|
|
21
23
|
def self.empty = new
|
|
22
24
|
|
|
23
|
-
#
|
|
25
|
+
# The cursor with no report reserved: what it is once the reserving
|
|
26
|
+
# turn is settled, and how two cursors compare apart from a claim.
|
|
27
|
+
def without_claim = with(report_claim: nil)
|
|
28
|
+
|
|
29
|
+
# @param baseline [ReplyWait::Baseline, nil] ReplyWait.baseline_of's
|
|
24
30
|
def with_baseline(baseline)
|
|
25
|
-
|
|
26
|
-
with(messages: b[:messages], question_id: b[:question_id], last_turn: b[:last_turn])
|
|
31
|
+
with(messages: baseline&.messages, question_id: baseline&.question_id, last_turn: baseline&.last_turn)
|
|
27
32
|
end
|
|
28
33
|
|
|
29
|
-
# @return [
|
|
34
|
+
# @return [ReplyWait::Baseline, nil] ReplyWait's baseline:, nil when
|
|
35
|
+
# none was taken
|
|
30
36
|
def baseline
|
|
31
37
|
return nil if messages.nil?
|
|
32
38
|
|
|
33
|
-
|
|
39
|
+
ReplyWait::Baseline.new(messages: messages, question_id: question_id, last_turn: last_turn)
|
|
34
40
|
end
|
|
35
41
|
|
|
36
|
-
def to_json_hash = to_h.transform_keys(&:to_s).compact
|
|
42
|
+
def to_json_hash = to_h.merge(report_claim: report_claim&.to_json_hash).transform_keys(&:to_s).compact
|
|
37
43
|
|
|
38
44
|
def self.from_json_hash(data)
|
|
39
45
|
return empty unless data.is_a?(Hash)
|
|
40
46
|
|
|
41
47
|
new(reply_file: data["reply_file"], last_turn: data["last_turn"], question_id: data["question_id"],
|
|
42
|
-
messages: data["messages"], turn: data["turn"], report_claim: data["report_claim"])
|
|
48
|
+
messages: data["messages"], turn: data["turn"], report_claim: ReportClaim.from_json_hash(data["report_claim"]))
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# A delegate report reserved in the parent's cursor (ChildReports) until
|
|
53
|
+
# the parent's turn that took it is kept or rolled back: +id+ the
|
|
54
|
+
# claim's id (the taking turn's Report#claim_id), +file+ the reply file
|
|
55
|
+
# reported (nil for a question), +cursor+ the DelegateCursor the report
|
|
56
|
+
# moves the parent to once kept (its own claim empty). In delegates.json
|
|
57
|
+
# it is {"id", "file", "cursor"}, the file's key kept even when nil.
|
|
58
|
+
ReportClaim = Data.define(:id, :file, :cursor) do
|
|
59
|
+
def to_json_hash = { "id" => id, "file" => file, "cursor" => cursor.to_json_hash }
|
|
60
|
+
|
|
61
|
+
# @return [ReportClaim, nil] nil for no claim (or not a Hash)
|
|
62
|
+
def self.from_json_hash(data)
|
|
63
|
+
return nil unless data.is_a?(Hash)
|
|
64
|
+
|
|
65
|
+
new(id: data["id"], file: data["file"], cursor: DelegateCursor.from_json_hash(data["cursor"]))
|
|
43
66
|
end
|
|
44
67
|
end
|
|
45
68
|
|
|
@@ -77,6 +100,7 @@ module Samagotchi
|
|
|
77
100
|
end
|
|
78
101
|
|
|
79
102
|
# Reserve a report only if the cursor read to build it is still current.
|
|
103
|
+
# @param claim [ReportClaim]
|
|
80
104
|
# @return [DelegateCursor, nil] the reserved cursor, or nil if another path moved it
|
|
81
105
|
def claim_report(parent_id, child_id, expected:, claim:, state_dir:)
|
|
82
106
|
locked(parent_id, state_dir: state_dir) do
|
|
@@ -85,7 +85,7 @@ module Samagotchi
|
|
|
85
85
|
|
|
86
86
|
fields = Relay.card(child, question, relay_id: relay_id, more: more)
|
|
87
87
|
answer = relay.open_question(fields, watch: watch(child_id, qid, state_dir))
|
|
88
|
-
settle(answer, fields, client: client, desk: desk, relay_id: relay_id, qid: qid)
|
|
88
|
+
settle(answer, fields, label: label(question), client: client, desk: desk, relay_id: relay_id, qid: qid)
|
|
89
89
|
ensure
|
|
90
90
|
# A relay that failed before it settled closes, so the desk (which
|
|
91
91
|
# prunes only settled ones) keeps none open; a settled one stays.
|
|
@@ -108,12 +108,11 @@ module Samagotchi
|
|
|
108
108
|
end
|
|
109
109
|
|
|
110
110
|
# Record the parent's answer, tell the child, and say what came of it.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
hook = fields[:kind] == HOOK_KIND
|
|
111
|
+
# @param label [String] the outcome line's start (#label)
|
|
112
|
+
def settle(answer, fields, label:, client:, desk:, relay_id:, qid:)
|
|
114
113
|
if answer.is_a?(Hash) && answer[:error] == "cancelled"
|
|
115
114
|
desk.close(relay_id)
|
|
116
|
-
return closed(answer[:reason].to_s,
|
|
115
|
+
return closed(answer[:reason].to_s, label, client: client, relay_id: relay_id, qid: qid)
|
|
117
116
|
end
|
|
118
117
|
|
|
119
118
|
dismissed = !answer.is_a?(QuestionAnswer)
|
|
@@ -121,18 +120,18 @@ module Samagotchi
|
|
|
121
120
|
desk.record(relay_id, selected_indices: dismissed ? [] : answer.selected_indices,
|
|
122
121
|
freeform: dismissed ? nil : answer.freeform, dismissed: dismissed, by: by)
|
|
123
122
|
response = post(client, "answered", relay_id, qid)
|
|
124
|
-
Outcome.new(line: line(
|
|
123
|
+
Outcome.new(line: line(label, delivered(response, answer, fields, dismissed)), stopped: false)
|
|
125
124
|
end
|
|
126
125
|
|
|
127
|
-
def closed(reason,
|
|
126
|
+
def closed(reason, label, client:, relay_id:, qid:)
|
|
128
127
|
case reason
|
|
129
|
-
when "answered_on_child" then Outcome.new(line: line(
|
|
128
|
+
when "answered_on_child" then Outcome.new(line: line(label, "answered on the child"), stopped: false)
|
|
130
129
|
when "child_gone"
|
|
131
|
-
Outcome.new(line: line(
|
|
130
|
+
Outcome.new(line: line(label, "not delivered (the child's worker is gone)"), stopped: false)
|
|
132
131
|
else
|
|
133
132
|
# A Stop: the child's question stays open there.
|
|
134
133
|
post(client, "closed", relay_id, qid, reason: "stopped")
|
|
135
|
-
Outcome.new(line: line(
|
|
134
|
+
Outcome.new(line: line(label, "still open (your turn was stopped)"), stopped: true)
|
|
136
135
|
end
|
|
137
136
|
end
|
|
138
137
|
|
|
@@ -172,14 +171,30 @@ module Samagotchi
|
|
|
172
171
|
why.empty? ? "denied" : "denied (#{why.inspect})"
|
|
173
172
|
end
|
|
174
173
|
|
|
175
|
-
|
|
176
|
-
|
|
174
|
+
# One relayed question's outcome line: #label → +outcome+.
|
|
175
|
+
def line(label, outcome) = "#{label} → #{outcome}"
|
|
176
|
+
|
|
177
|
+
# What an outcome line starts with: "hook question relayed to your
|
|
178
|
+
# user: Cost cap", "approval relayed to your user: execute: git push".
|
|
179
|
+
# @param question [PendingQuestion] the child's
|
|
180
|
+
def label(question)
|
|
181
|
+
"#{question.kind.to_s == HOOK_KIND ? "hook question" : "approval"} relayed to your user: #{what(question)}"
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# What a relayed question is about, as its card has it (Relay.card): a
|
|
185
|
+
# hook's header as the asking hook gave it (kept down a chain: a relay
|
|
186
|
+
# of a relay), else its hook; an approval's call (#what_text).
|
|
187
|
+
# @param question [PendingQuestion] the child's
|
|
188
|
+
def what(question)
|
|
189
|
+
return what_text(symbolized(question.approval)) unless question.kind.to_s == HOOK_KIND
|
|
190
|
+
|
|
191
|
+
inner = symbolized(question.relay, nil)
|
|
192
|
+
header = (inner ? inner[:header] : question.header).to_s
|
|
193
|
+
header.empty? ? question.hook.to_s : header
|
|
177
194
|
end
|
|
178
195
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
header = fields.dig(:relay, :header).to_s
|
|
182
|
-
header.empty? ? fields[:hook].to_s : header
|
|
196
|
+
def symbolized(value, none = {})
|
|
197
|
+
value.is_a?(Hash) ? Relay.deep_symbolize(value) : none
|
|
183
198
|
end
|
|
184
199
|
|
|
185
200
|
# execute: git push origin main
|
|
@@ -69,7 +69,7 @@ module Samagotchi
|
|
|
69
69
|
cursor = DelegateCursors.get(peers.session_id, child_id, state_dir: sd)
|
|
70
70
|
report_claim = cursor.report_claim
|
|
71
71
|
if report_claim
|
|
72
|
-
cursor =
|
|
72
|
+
cursor = report_claim.cursor.with(report_claim: report_claim)
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
baseline = cursor.baseline
|
|
@@ -88,10 +88,10 @@ module Samagotchi
|
|
|
88
88
|
outcomes.concat(relayed_outcomes.delete(key) || [])
|
|
89
89
|
movement = advance(peers.session_id, child_id, wait, state_dir: sd)
|
|
90
90
|
if movement&.already_reported
|
|
91
|
-
file = movement.cursor.report_claim&.
|
|
91
|
+
file = movement.cursor.report_claim&.file || wait.file
|
|
92
92
|
return already_reported_result(child_id, file, state_dir: sd)
|
|
93
93
|
end
|
|
94
|
-
return already_reported_result(child_id, report_claim
|
|
94
|
+
return already_reported_result(child_id, report_claim.file, state_dir: sd) if report_claim && wait.status == :timeout
|
|
95
95
|
|
|
96
96
|
text = with_outcomes(finish(wait, child_id, timeout: timeout, reports: reports), outcomes)
|
|
97
97
|
return RefText.wrap(text, reply_path(child_id, wait, state_dir: sd))
|
|
@@ -179,7 +179,7 @@ module Samagotchi
|
|
|
179
179
|
|
|
180
180
|
already_reported = false
|
|
181
181
|
cursor = DelegateCursors.update(parent_id, child_id, state_dir: state_dir) do |current|
|
|
182
|
-
if (current.report_claim && current.report_claim
|
|
182
|
+
if (current.report_claim && current.report_claim.file == wait.file) ||
|
|
183
183
|
(wait.status == :done && current.reply_file == wait.file)
|
|
184
184
|
already_reported = true
|
|
185
185
|
next current
|
|
@@ -6,6 +6,7 @@ require "securerandom"
|
|
|
6
6
|
require "shellwords"
|
|
7
7
|
require "time"
|
|
8
8
|
require_relative "../atomic_file"
|
|
9
|
+
require_relative "../child_env"
|
|
9
10
|
require_relative "../log"
|
|
10
11
|
require_relative "../process_group"
|
|
11
12
|
require_relative "shell"
|
|
@@ -29,8 +30,7 @@ module Samagotchi
|
|
|
29
30
|
OUTPUT_TAIL_READ_BYTES = 16 * 1024
|
|
30
31
|
SANITIZED_ENV_KEYS = %w[RUBYOPT RUBYLIB BUNDLE_GEMFILE BUNDLE_BIN_PATH BUNDLER_VERSION].freeze
|
|
31
32
|
# Set by chi (Builtins.parent_env); the model's env can't change it.
|
|
32
|
-
MARKER_ENV_KEYS =
|
|
33
|
-
SAMAGOTCHI_SESSION_REASONING_HISTORY].freeze
|
|
33
|
+
MARKER_ENV_KEYS = ChildEnv::SESSION_MARKERS
|
|
34
34
|
# task_wait and task_get add it to a task the user stopped (the web's
|
|
35
35
|
# stop-task button), so the model doesn't rerun the job.
|
|
36
36
|
# A record whose pid isn't a process group chi started (missing, 0, 1,
|
|
@@ -91,16 +91,25 @@ module Samagotchi
|
|
|
91
91
|
end
|
|
92
92
|
private_class_method :settled_record
|
|
93
93
|
|
|
94
|
+
# A command's status (its command_ran's) as a settled outcome; any
|
|
95
|
+
# other status (busy, dropped) is a cancel for that reason.
|
|
96
|
+
COMMAND_OUTCOMES = { "ok" => "completed", "error" => "failed" }.freeze
|
|
97
|
+
# Its record's origin: no turn ran (ParentReport says "the command").
|
|
98
|
+
COMMAND_ORIGIN = "command"
|
|
99
|
+
|
|
94
100
|
# A file-delivered command ends its sender's wait without running a
|
|
95
|
-
# turn. Keep a terminal outcome under the same per-input settled path
|
|
96
|
-
|
|
101
|
+
# turn. Keep a terminal outcome under the same per-input settled path:
|
|
102
|
+
# +status+ is its command_ran's ("ok", "error", "busy", "dropped" at a
|
|
103
|
+
# teardown), so the wait reads completed, failed, or canceled with that
|
|
104
|
+
# reason.
|
|
105
|
+
def self.record_command_outcome(session_dir, enqueued_id, status:)
|
|
97
106
|
id = enqueued_id.to_s
|
|
98
107
|
return if id.empty? || !SessionInbox::ENQUEUED_ID_NAME.match?(id)
|
|
99
108
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
109
|
+
outcome = COMMAND_OUTCOMES.fetch(status.to_s, "canceled")
|
|
110
|
+
record = LastTurn.new(outcome: outcome, cancel_reason: (status.to_s if outcome == "canceled"),
|
|
111
|
+
ended_at: Time.now.iso8601(3), origin: COMMAND_ORIGIN, enqueued_id: id)
|
|
112
|
+
write_settled(session_dir, id, record)
|
|
104
113
|
rescue SystemCallError, IOError => e
|
|
105
114
|
Log.warn(:worker, "command_outcome_write_failed", error: e.class.name, msg: e.message, enqueued_id: id)
|
|
106
115
|
end
|
|
@@ -109,14 +118,18 @@ module Samagotchi
|
|
|
109
118
|
id = mark.enqueued_id.to_s
|
|
110
119
|
return unless SessionInbox::ENQUEUED_ID_NAME.match?(id)
|
|
111
120
|
|
|
112
|
-
|
|
113
|
-
FileUtils.mkdir_p(dir)
|
|
114
|
-
record = settled_record(id, ended_at: Time.now.iso8601(3), seconds: seconds)
|
|
115
|
-
AtomicFile.write(File.join(dir, id), JSON.generate(record.to_file))
|
|
121
|
+
write_settled(session_dir, id, settled_record(id, ended_at: Time.now.iso8601(3), seconds: seconds))
|
|
116
122
|
rescue SystemCallError => e
|
|
117
123
|
# The settle goes on; a wait for that turn runs to its timeout.
|
|
118
124
|
Log.warn(:worker, "settled_turn_write_failed", error: e.class.name, msg: e.message)
|
|
119
125
|
end
|
|
126
|
+
|
|
127
|
+
def self.write_settled(session_dir, id, record)
|
|
128
|
+
dir = File.join(session_dir, SessionInbox::INPUT_DIR, SETTLED_DIR)
|
|
129
|
+
FileUtils.mkdir_p(dir)
|
|
130
|
+
AtomicFile.write(File.join(dir, id), JSON.generate(record.to_file))
|
|
131
|
+
end
|
|
132
|
+
private_class_method :write_settled
|
|
120
133
|
private_class_method :record_settled
|
|
121
134
|
|
|
122
135
|
# @param session [Session] the worker's session (saved as a whole)
|