samagotchi 0.86.0 → 0.88.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 +32 -1
- data/bin/chi +39 -2
- data/docs/architecture.md +5 -3
- data/docs/cli.md +18 -5
- data/docs/configuration.md +1 -1
- data/docs/guardrails.md +53 -3
- data/docs/hooks.md +1 -1
- data/docs/plugins.md +1 -1
- data/docs/sessions.md +19 -7
- data/docs/sub-agent.md +15 -3
- data/lib/samagotchi/answer_command.rb +6 -6
- data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +3 -3
- data/lib/samagotchi/bundles/known-names/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/child_reports.rb +100 -7
- data/lib/samagotchi/cli/parent_wait.rb +3 -1
- data/lib/samagotchi/guardrails/parent_policy.rb +94 -5
- data/lib/samagotchi/parent_answers_command.rb +80 -0
- data/lib/samagotchi/parent_report.rb +53 -34
- data/lib/samagotchi/question_desk.rb +3 -2
- data/lib/samagotchi/relay.rb +34 -2
- data/lib/samagotchi/relay_desk.rb +13 -0
- data/lib/samagotchi/send_command.rb +75 -10
- data/lib/samagotchi/session.rb +26 -2
- data/lib/samagotchi/session_commands.rb +35 -9
- data/lib/samagotchi/session_setup.rb +10 -8
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +18 -8
- data/lib/samagotchi/terminal_ui/attached_loop.rb +16 -5
- data/lib/samagotchi/terminal_ui/question_prompt.rb +2 -1
- data/lib/samagotchi/terminal_ui.rb +12 -6
- data/lib/samagotchi/tools/delegate.rb +4 -1
- data/lib/samagotchi/tools/delegate_relay.rb +69 -15
- data/lib/samagotchi/tools/delegate_wait.rb +14 -8
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/public/notify.js +6 -2
- data/lib/samagotchi/web/public/question_card.js +2 -0
- data/lib/samagotchi/web/public/question_cards.js +25 -18
- data/lib/samagotchi/worker.rb +60 -10
- metadata +2 -1
|
@@ -134,7 +134,10 @@ module Samagotchi
|
|
|
134
134
|
"(the most is #{max}, #{MAX_CHILDREN_KEY}): #{ids}. #{full_hint(peers)}"
|
|
135
135
|
end
|
|
136
136
|
|
|
137
|
-
|
|
137
|
+
# The parent's own parent_answers, as it is now (a snapshot): the
|
|
138
|
+
# child's answers are held at least as tight.
|
|
139
|
+
setup = SessionSetup.new(toolkit: toolkit.empty? ? nil : Toolkit.new(name: toolkit),
|
|
140
|
+
parent_answers: parent.parent_answers)
|
|
138
141
|
child = SessionManager.spawn_session(prompt: task, working_directory: folder,
|
|
139
142
|
model_name: child_model(model, parent), memories: CHILD_MEMORIES,
|
|
140
143
|
parent_id: parent.id, delegate: true, setup: setup, state_dir: state_dir)
|
|
@@ -4,6 +4,7 @@ require_relative "../session"
|
|
|
4
4
|
require_relative "../bridge_client"
|
|
5
5
|
require_relative "../relay"
|
|
6
6
|
require_relative "../guardrails/approval"
|
|
7
|
+
require_relative "../guardrails/parent_policy"
|
|
7
8
|
require_relative "../log"
|
|
8
9
|
require_relative "../question_answer"
|
|
9
10
|
|
|
@@ -35,10 +36,39 @@ module Samagotchi
|
|
|
35
36
|
|
|
36
37
|
module_function
|
|
37
38
|
|
|
39
|
+
# Kinds a relay may carry.
|
|
40
|
+
HOOK_KIND = "hook"
|
|
41
|
+
|
|
38
42
|
# @param relay [#open_question, #relay_desk] Peers#relay
|
|
43
|
+
# @param strategy [Guardrails::ParentPolicy::Strategy] the child's
|
|
44
|
+
# (ParentPolicy.for_session(child))
|
|
39
45
|
# @return [Boolean] a relay can take this question
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
# @param child_id [String, nil] the asking child: a hook's question
|
|
47
|
+
# already answered on one of this parent's relays
|
|
48
|
+
# (RelayDesk#settled?) isn't relayed again; an approval is (as before
|
|
49
|
+
# P4: a refused or undelivered answer is offered again)
|
|
50
|
+
def relayable?(relay, question, strategy:, child_id: nil)
|
|
51
|
+
return false if relay.nil? || !relay?(question, strategy: strategy)
|
|
52
|
+
return true unless child_id && question.kind.to_s == HOOK_KIND
|
|
53
|
+
|
|
54
|
+
!relay.relay_desk.settled?(child_id, question.id)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Whether a delegate's question goes to the parent's user rather than
|
|
58
|
+
# its model: every approval, and a hook's question the parent can't
|
|
59
|
+
# usefully answer under the child's strategy
|
|
60
|
+
# (Guardrails::ParentPolicy.parent_decides?). The one place that says
|
|
61
|
+
# which kinds the relay carries: the wait (DelegateWait.call), the
|
|
62
|
+
# look at the other children (Others#waiting) and the delegate
|
|
63
|
+
# reports (ChildReports) all ask it.
|
|
64
|
+
def relay?(question, strategy:)
|
|
65
|
+
return false unless question.is_a?(PendingQuestion)
|
|
66
|
+
|
|
67
|
+
case question.kind.to_s
|
|
68
|
+
when Guardrails::Approval::KIND then true
|
|
69
|
+
when HOOK_KIND then !Guardrails::ParentPolicy.parent_decides?(question, strategy: strategy)
|
|
70
|
+
else false
|
|
71
|
+
end
|
|
42
72
|
end
|
|
43
73
|
|
|
44
74
|
# Relay the child's pending approval and wait until it settles.
|
|
@@ -79,10 +109,11 @@ module Samagotchi
|
|
|
79
109
|
|
|
80
110
|
# Record the parent's answer, tell the child, and say what came of it.
|
|
81
111
|
def settle(answer, fields, client:, desk:, relay_id:, qid:)
|
|
82
|
-
what = what_text(fields[:approval])
|
|
112
|
+
what = fields[:kind] == HOOK_KIND ? hook_what(fields) : what_text(fields[:approval])
|
|
113
|
+
hook = fields[:kind] == HOOK_KIND
|
|
83
114
|
if answer.is_a?(Hash) && answer[:error] == "cancelled"
|
|
84
115
|
desk.close(relay_id)
|
|
85
|
-
return closed(answer[:reason].to_s, what, client: client, relay_id: relay_id, qid: qid)
|
|
116
|
+
return closed(answer[:reason].to_s, what, client: client, relay_id: relay_id, qid: qid, hook: hook)
|
|
86
117
|
end
|
|
87
118
|
|
|
88
119
|
dismissed = !answer.is_a?(QuestionAnswer)
|
|
@@ -90,17 +121,18 @@ module Samagotchi
|
|
|
90
121
|
desk.record(relay_id, selected_indices: dismissed ? [] : answer.selected_indices,
|
|
91
122
|
freeform: dismissed ? nil : answer.freeform, dismissed: dismissed, by: by)
|
|
92
123
|
response = post(client, "answered", relay_id, qid)
|
|
93
|
-
Outcome.new(line: line(what, delivered(response, answer, fields, dismissed)), stopped: false)
|
|
124
|
+
Outcome.new(line: line(what, delivered(response, answer, fields, dismissed), hook: hook), stopped: false)
|
|
94
125
|
end
|
|
95
126
|
|
|
96
|
-
def closed(reason, what, client:, relay_id:, qid:)
|
|
127
|
+
def closed(reason, what, client:, relay_id:, qid:, hook: false)
|
|
97
128
|
case reason
|
|
98
|
-
when "answered_on_child" then Outcome.new(line: line(what, "answered on the child"), stopped: false)
|
|
99
|
-
when "child_gone"
|
|
129
|
+
when "answered_on_child" then Outcome.new(line: line(what, "answered on the child", hook: hook), stopped: false)
|
|
130
|
+
when "child_gone"
|
|
131
|
+
Outcome.new(line: line(what, "not delivered (the child's worker is gone)", hook: hook), stopped: false)
|
|
100
132
|
else
|
|
101
133
|
# A Stop: the child's question stays open there.
|
|
102
134
|
post(client, "closed", relay_id, qid, reason: "stopped")
|
|
103
|
-
Outcome.new(line: line(what, "still open (your turn was stopped)"), stopped: true)
|
|
135
|
+
Outcome.new(line: line(what, "still open (your turn was stopped)", hook: hook), stopped: true)
|
|
104
136
|
end
|
|
105
137
|
end
|
|
106
138
|
|
|
@@ -108,10 +140,16 @@ module Samagotchi
|
|
|
108
140
|
def delivered(response, answer, fields, dismissed)
|
|
109
141
|
return "not delivered (the child's worker is gone)" unless response
|
|
110
142
|
|
|
143
|
+
hook = fields[:kind] == HOOK_KIND
|
|
111
144
|
case response.status
|
|
112
|
-
when 200
|
|
145
|
+
when 200
|
|
146
|
+
# A dismissed hook question: the hook decides what no answer means.
|
|
147
|
+
return dismissed ? "dismissed" : answer.selected.join(", ") if hook
|
|
148
|
+
|
|
149
|
+
dismissed ? "dismissed (denied)" : verdict(answer, fields)
|
|
113
150
|
when 409 then "answered on the child"
|
|
114
|
-
when 403
|
|
151
|
+
when 403
|
|
152
|
+
"refused by the child (a parent agent may not #{hook ? "pick it" : "allow it"}); it waits for the user there"
|
|
115
153
|
else "not delivered (#{response.json&.fetch("error", nil) || response.status})"
|
|
116
154
|
end
|
|
117
155
|
end
|
|
@@ -134,7 +172,15 @@ module Samagotchi
|
|
|
134
172
|
why.empty? ? "denied" : "denied (#{why.inspect})"
|
|
135
173
|
end
|
|
136
174
|
|
|
137
|
-
def line(what, outcome
|
|
175
|
+
def line(what, outcome, hook: false)
|
|
176
|
+
"#{hook ? "hook question" : "approval"} relayed to your user: #{what} → #{outcome}"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# A hook card's header as the child asked it (Cost cap), or its hook.
|
|
180
|
+
def hook_what(fields)
|
|
181
|
+
header = fields.dig(:relay, :header).to_s
|
|
182
|
+
header.empty? ? fields[:hook].to_s : header
|
|
183
|
+
end
|
|
138
184
|
|
|
139
185
|
# execute: git push origin main
|
|
140
186
|
def what_text(facts)
|
|
@@ -151,6 +197,9 @@ module Samagotchi
|
|
|
151
197
|
# (DelegateWait.relayed_outcomes). Every question is relayed once per
|
|
152
198
|
# wait.
|
|
153
199
|
class Others
|
|
200
|
+
# The kinds of question a relay may carry (DelegateRelay.relay? decides).
|
|
201
|
+
KINDS = [Guardrails::Approval::KIND, HOOK_KIND].freeze
|
|
202
|
+
|
|
154
203
|
# @param parent_id [String]
|
|
155
204
|
# @param except [String] the child waited on (its own approvals come
|
|
156
205
|
# through ReplyWait)
|
|
@@ -191,11 +240,16 @@ module Samagotchi
|
|
|
191
240
|
# oldest question first
|
|
192
241
|
def waiting
|
|
193
242
|
rows = SessionManager.children_of(@parent_id, state_dir: @state_dir).select do |row|
|
|
194
|
-
row[:id] != @except && row[:waiting]
|
|
243
|
+
row[:id] != @except && KINDS.include?(row[:waiting].to_s) && !@done[row[:waiting_id].to_s]
|
|
195
244
|
end
|
|
196
245
|
rows.filter_map do |row|
|
|
197
|
-
|
|
198
|
-
|
|
246
|
+
child = Session.load(row[:id], state_dir: @state_dir)
|
|
247
|
+
pending = child.pending_question
|
|
248
|
+
next nil unless pending && pending.id.to_s == row[:waiting_id].to_s
|
|
249
|
+
next nil unless DelegateRelay.relayable?(@relay, pending, strategy: Guardrails::ParentPolicy.for_session(child),
|
|
250
|
+
child_id: row[:id])
|
|
251
|
+
|
|
252
|
+
[row[:id], pending]
|
|
199
253
|
rescue ArgumentError
|
|
200
254
|
nil
|
|
201
255
|
end.sort_by { |_, pending| pending.created_at.to_s }
|
|
@@ -82,7 +82,9 @@ module Samagotchi
|
|
|
82
82
|
owner_grace: owner_grace, interject: others && -> { others.poll },
|
|
83
83
|
turn: cursor.turn)
|
|
84
84
|
left -= monotonic - started
|
|
85
|
-
unless wait.status == :waiting_for_answer &&
|
|
85
|
+
unless wait.status == :waiting_for_answer &&
|
|
86
|
+
DelegateRelay.relayable?(relay, wait.question, strategy: Guardrails::ParentPolicy.for_session(wait.session),
|
|
87
|
+
child_id: child_id)
|
|
86
88
|
outcomes.concat(relayed_outcomes.delete(key) || [])
|
|
87
89
|
movement = advance(peers.session_id, child_id, wait, state_dir: sd)
|
|
88
90
|
if movement&.already_reported
|
|
@@ -213,6 +215,10 @@ module Samagotchi
|
|
|
213
215
|
# @return [String]
|
|
214
216
|
def finish(wait, child_id, timeout:, report: false, reports: "off")
|
|
215
217
|
status = ParentReport.status(wait)
|
|
218
|
+
# What the parent may pick on the child's question: config.yml
|
|
219
|
+
# tightened by the child session's own parent_answers (the session
|
|
220
|
+
# the wait read; none: config.yml's).
|
|
221
|
+
strategy = Guardrails::ParentPolicy.for_session(wait.session)
|
|
216
222
|
case wait.status
|
|
217
223
|
when :done
|
|
218
224
|
reply_result(child_id, status, wait.text)
|
|
@@ -221,11 +227,11 @@ module Samagotchi
|
|
|
221
227
|
when :stopped
|
|
222
228
|
result(child_id, status, "the child was stopped (chi sessions stop); delegate with session: #{child_id} starts it again with a message")
|
|
223
229
|
when :waiting_for_answer
|
|
224
|
-
result(child_id, status, waiting_text(child_id, wait.question, report: report, reports: reports))
|
|
230
|
+
result(child_id, status, waiting_text(child_id, wait.question, report: report, reports: reports, strategy: strategy))
|
|
225
231
|
when :canceled
|
|
226
232
|
canceled_result(child_id, reports: reports)
|
|
227
233
|
when :no_reply
|
|
228
|
-
result(child_id, status, "#{no_reply_text(wait, child_id)}; its session shows what happened")
|
|
234
|
+
result(child_id, status, "#{no_reply_text(wait, child_id, strategy: strategy)}; its session shows what happened")
|
|
229
235
|
when :worker_gone
|
|
230
236
|
result(child_id, status,
|
|
231
237
|
"the child's worker is gone (it stopped or crashed); delegate with session: #{child_id} starts it again with a message")
|
|
@@ -266,8 +272,8 @@ module Samagotchi
|
|
|
266
272
|
end
|
|
267
273
|
|
|
268
274
|
# ParentReport's line for a turn that left no reply, about the child.
|
|
269
|
-
def no_reply_text(wait, child_id)
|
|
270
|
-
ParentReport.no_reply_line(wait, child_id).sub(/\Athe turn/, "the child's turn")
|
|
275
|
+
def no_reply_text(wait, child_id, strategy: nil)
|
|
276
|
+
ParentReport.no_reply_line(wait, child_id, strategy: strategy).sub(/\Athe turn/, "the child's turn")
|
|
271
277
|
end
|
|
272
278
|
|
|
273
279
|
def result(child_id, status, text)
|
|
@@ -303,9 +309,9 @@ module Samagotchi
|
|
|
303
309
|
# A report's last line differs (+report+): "waits again" made models
|
|
304
310
|
# call delegate_result right after answering Continue.
|
|
305
311
|
# The tool's last line follows #reports_mode.
|
|
306
|
-
def waiting_text(child_id, pending, report: false, reports: "off")
|
|
307
|
-
text = "Child #{child_id} is #{ParentReport.question_text(pending, session_id: child_id)}"
|
|
308
|
-
if ParentReport.continue?(pending) && ParentReport.may_continue?(pending)
|
|
312
|
+
def waiting_text(child_id, pending, report: false, reports: "off", strategy: nil)
|
|
313
|
+
text = "Child #{child_id} is #{ParentReport.question_text(pending, session_id: child_id, strategy: strategy)}"
|
|
314
|
+
if ParentReport.continue?(pending) && ParentReport.may_continue?(pending, strategy: strategy)
|
|
309
315
|
text += "The child ran out of steps before it answered. Decide: continue it (run the chi answer command " \
|
|
310
316
|
"above with execute) if it is getting somewhere; send it a narrower follow-up with delegate " \
|
|
311
317
|
"session: #{child_id} (that drops the question); or stop it and report back to your user.\n"
|
data/lib/samagotchi/version.rb
CHANGED
|
@@ -44,8 +44,10 @@ export function waitingOn(summary) {
|
|
|
44
44
|
// waiting on directly is seen by its look at the others (DelegateRelay
|
|
45
45
|
// OTHERS_EVERY, 2 s), and the relay reaches the page with the hub's next
|
|
46
46
|
// scan (SessionHub SCAN_INTERVAL, 1 s): up to ~3 s. Only these approvals
|
|
47
|
-
// wait
|
|
47
|
+
// wait (and a child's hook question, which may be relayed too); one nobody
|
|
48
|
+
// relays rings this late.
|
|
48
49
|
export const RELAY_HOLD_MS = 3500;
|
|
50
|
+
const RELAYED_KINDS = new Set(["approval", "hook"]);
|
|
49
51
|
|
|
50
52
|
// What changed between two summaries of one session that needs the user.
|
|
51
53
|
// @param prev the summary held before (null for a session not seen yet)
|
|
@@ -60,7 +62,9 @@ export function attentionFor(prev, next) {
|
|
|
60
62
|
// Relayed to the parent already: the parent's card notifies.
|
|
61
63
|
if (q.relayed_to) return null;
|
|
62
64
|
const attention = { reason: questionReason(q), sessionId: next.id, key: `${next.id}:${q.id}` };
|
|
63
|
-
|
|
65
|
+
// A hook's question may be relayed too (when the parent may only deny it);
|
|
66
|
+
// one the parent's model answers rings as late, which does no harm.
|
|
67
|
+
return next.parent_id && RELAYED_KINDS.has(q.kind) ? { ...attention, holdMs: RELAY_HOLD_MS } : attention;
|
|
64
68
|
}
|
|
65
69
|
const card = next.pending_card;
|
|
66
70
|
if (card && card.id && card.id !== prev?.pending_card?.id) {
|
|
@@ -119,6 +119,8 @@ export function resultText(pq, { answer = null, cancelled = false, reason = "" }
|
|
|
119
119
|
const relay = relayView(pq);
|
|
120
120
|
if (cancelled && Object.hasOwn(CLOSED_PLAIN, reason)) return CLOSED_PLAIN[reason];
|
|
121
121
|
if (cancelled && relay && reason !== "dismissed") return relayClosedText(relay, reason);
|
|
122
|
+
// A relayed hook question's dismissal: the hook decides what it means.
|
|
123
|
+
if (cancelled && relay && !approval) return "Dismissed";
|
|
122
124
|
if (isContinue(pq)) return continueResultText({ answer, cancelled, reason });
|
|
123
125
|
if (cancelled) {
|
|
124
126
|
const label = approval ? "Denied" : "Cancelled";
|
|
@@ -17,7 +17,7 @@ import { diffTemplate } from "./diff_view.js";
|
|
|
17
17
|
import { render } from "./vendor/lit-html.js";
|
|
18
18
|
import {
|
|
19
19
|
answerErrorText, approvalAllowed, approvalView, CONTINUE_REASON_PLACEHOLDER, deniedReason, isContinue, keepsAskingText,
|
|
20
|
-
nextCardAction, relayedToView, resultText, summaryText,
|
|
20
|
+
nextCardAction, relayedToView, relayView, resultText, summaryText,
|
|
21
21
|
} from "./question_card.js";
|
|
22
22
|
import { sessionHash } from "./route.js";
|
|
23
23
|
import { clientLabel, isOwn } from "./turn_events.js";
|
|
@@ -92,9 +92,12 @@ export function createQuestionCards({
|
|
|
92
92
|
if (approval) {
|
|
93
93
|
card.appendChild(renderApprovalDetails(approval));
|
|
94
94
|
} else {
|
|
95
|
+
// A delegate's hook question relayed here: who asks, then its own text.
|
|
96
|
+
const relay = relayView(pq);
|
|
97
|
+
if (relay) card.appendChild(renderDelegateLine(relay));
|
|
95
98
|
const q = doc.createElement("div");
|
|
96
99
|
q.className = "question-text";
|
|
97
|
-
q.textContent = pq.question || "";
|
|
100
|
+
q.textContent = (relay && pq.relay.asked) || pq.question || "";
|
|
98
101
|
card.appendChild(q);
|
|
99
102
|
}
|
|
100
103
|
|
|
@@ -273,28 +276,32 @@ export function createQuestionCards({
|
|
|
273
276
|
card.appendChild(err);
|
|
274
277
|
}
|
|
275
278
|
|
|
279
|
+
// "delegate ab12 · its task", the name linked to its session (a relayed
|
|
280
|
+
// approval's or hook question's; relayView).
|
|
281
|
+
function renderDelegateLine(delegate) {
|
|
282
|
+
const who = doc.createElement("div");
|
|
283
|
+
who.className = "approval-delegate";
|
|
284
|
+
const label = doc.createElement("span");
|
|
285
|
+
label.className = "approval-label";
|
|
286
|
+
label.textContent = "delegate ";
|
|
287
|
+
who.appendChild(label);
|
|
288
|
+
const name = doc.createElement("a");
|
|
289
|
+
name.className = "approval-delegate-link";
|
|
290
|
+
name.href = sessionHash(delegate.childId);
|
|
291
|
+
name.title = "Open the delegate's session";
|
|
292
|
+
name.textContent = delegate.who;
|
|
293
|
+
who.appendChild(name);
|
|
294
|
+
if (delegate.task) who.appendChild(doc.createTextNode(` · ${delegate.task}`));
|
|
295
|
+
return who;
|
|
296
|
+
}
|
|
297
|
+
|
|
276
298
|
// The tool, the command (or paths, or args) as code, where and why; no
|
|
277
299
|
// code box when there is nothing to show.
|
|
278
300
|
function renderApprovalDetails(view) {
|
|
279
301
|
const box = doc.createElement("div");
|
|
280
302
|
box.className = "approval-details";
|
|
281
303
|
// A delegate's approval: who asks, and its task.
|
|
282
|
-
if (view.delegate)
|
|
283
|
-
const who = doc.createElement("div");
|
|
284
|
-
who.className = "approval-delegate";
|
|
285
|
-
const label = doc.createElement("span");
|
|
286
|
-
label.className = "approval-label";
|
|
287
|
-
label.textContent = "delegate ";
|
|
288
|
-
who.appendChild(label);
|
|
289
|
-
const name = doc.createElement("a");
|
|
290
|
-
name.className = "approval-delegate-link";
|
|
291
|
-
name.href = sessionHash(view.delegate.childId);
|
|
292
|
-
name.title = "Open the delegate's session";
|
|
293
|
-
name.textContent = view.delegate.who;
|
|
294
|
-
who.appendChild(name);
|
|
295
|
-
if (view.delegate.task) who.appendChild(doc.createTextNode(` · ${view.delegate.task}`));
|
|
296
|
-
box.appendChild(who);
|
|
297
|
-
}
|
|
304
|
+
if (view.delegate) box.appendChild(renderDelegateLine(view.delegate));
|
|
298
305
|
const tool = doc.createElement("div");
|
|
299
306
|
tool.className = "approval-tool";
|
|
300
307
|
tool.textContent = view.tool;
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "fileutils"
|
|
|
5
5
|
require_relative "client_id"
|
|
6
6
|
require_relative "config"
|
|
7
7
|
require_relative "engine"
|
|
8
|
+
require_relative "guardrails/parent_policy"
|
|
8
9
|
require_relative "events"
|
|
9
10
|
require_relative "session"
|
|
10
11
|
require_relative "session_inbox"
|
|
@@ -308,6 +309,7 @@ module Samagotchi
|
|
|
308
309
|
# What failed to load and what plugins showed as they loaded, now that
|
|
309
310
|
# a UI can be there (a late one gets them in the snapshot).
|
|
310
311
|
@engine.announce_load_events!
|
|
312
|
+
announce_session_warnings
|
|
311
313
|
@idle_exit = WorkerIdleExit.new(
|
|
312
314
|
engine: @engine, bridge: @bridge,
|
|
313
315
|
timeout_minutes: @idle_exit_minutes || SessionManager.config_idle_exit_minutes,
|
|
@@ -468,6 +470,16 @@ module Samagotchi
|
|
|
468
470
|
save_or_log(:interrupted_turn) { save_session }
|
|
469
471
|
end
|
|
470
472
|
|
|
473
|
+
# A saved parent_answers that isn't a level reads as hard on every
|
|
474
|
+
# answer without a word; said once here, as the session loads.
|
|
475
|
+
def announce_session_warnings
|
|
476
|
+
value = @session.parent_answers
|
|
477
|
+
return unless Guardrails::ParentPolicy.unknown_level?(value)
|
|
478
|
+
|
|
479
|
+
@engine.announce({ type: :guardrail_warning, label: "parent_answers",
|
|
480
|
+
message: "parent_answers #{value.inspect} in session #{@session_id} isn't hard/standard/soft; hard applies" })
|
|
481
|
+
end
|
|
482
|
+
|
|
471
483
|
# A question saved by a worker that died while it waited (an
|
|
472
484
|
# ask_user_question, an approval, a hook's question): the turn that
|
|
473
485
|
# asked is gone, so nobody can answer it, and left in the file it
|
|
@@ -600,7 +612,9 @@ module Samagotchi
|
|
|
600
612
|
budget_notice if state == :budget
|
|
601
613
|
return false unless state == :due
|
|
602
614
|
|
|
603
|
-
|
|
615
|
+
# Unresolved: a relay among them opens its card at the turn's first
|
|
616
|
+
# boundary, on the turn thread (a Stop closes it).
|
|
617
|
+
reports, = take_child_reports(resolve: false)
|
|
604
618
|
# The turn must not generate with nothing to say: the drain gets this
|
|
605
619
|
# exact list at its first boundary.
|
|
606
620
|
return false if reports.empty?
|
|
@@ -794,7 +808,9 @@ module Samagotchi
|
|
|
794
808
|
# as its output
|
|
795
809
|
def run_command_line(command)
|
|
796
810
|
@engine.holding_announcements do
|
|
797
|
-
command_result(command)
|
|
811
|
+
command_result(command) do
|
|
812
|
+
@commands.run(command.line, confirm: confirm?(command), as_parent: as_parent?(command), user_ui: user_ui?(command))
|
|
813
|
+
end
|
|
798
814
|
end
|
|
799
815
|
end
|
|
800
816
|
|
|
@@ -808,6 +824,12 @@ module Samagotchi
|
|
|
808
824
|
# guardrails.parent_answers (SessionCommands#continue_answer).
|
|
809
825
|
def as_parent?(command) = command.client_id.to_s == ClientId::CLI_ANSWER
|
|
810
826
|
|
|
827
|
+
# Whether +command+ is a user's at a UI (a web tab, a chi TUI, a card's
|
|
828
|
+
# click), not a parent agent's: only such a line loosens
|
|
829
|
+
# /parent-answers. chi send, a delegate's follow-up, a plugin and an
|
|
830
|
+
# input with no client never do (ParentAnswersCommand).
|
|
831
|
+
def user_ui?(command) = ClientId.interactive?(command.client_id) && !as_parent?(command)
|
|
832
|
+
|
|
811
833
|
# The block's Result for +command+; one that raised, or found no
|
|
812
834
|
# command for the line, is an error Result saying so.
|
|
813
835
|
# @return [SessionCommands::Result]
|
|
@@ -1003,19 +1025,26 @@ module Samagotchi
|
|
|
1003
1025
|
end
|
|
1004
1026
|
|
|
1005
1027
|
# The Engine published a question (after the question desk saved it).
|
|
1006
|
-
# In a turn that reports to the parent, the model's own question
|
|
1007
|
-
#
|
|
1008
|
-
#
|
|
1028
|
+
# In a turn that reports to the parent, the model's own question, the
|
|
1029
|
+
# step-limit continue and a hook's question ring it (the parent reports
|
|
1030
|
+
# or relays a hook's: ChildReports). Approvals don't: the relay takes
|
|
1031
|
+
# them while the parent waits. Nor does a card relayed from this
|
|
1032
|
+
# session's own child (relay set): the hop is the parent's wait.
|
|
1009
1033
|
def ring_question(event)
|
|
1010
1034
|
return unless @reporting_turn
|
|
1011
1035
|
|
|
1012
|
-
|
|
1013
|
-
|
|
1036
|
+
pending = event[:pending_question] || {}
|
|
1037
|
+
kind = pending[:kind].to_s
|
|
1038
|
+
return unless RINGING_KINDS.include?(kind)
|
|
1039
|
+
return if pending[:relay]
|
|
1014
1040
|
|
|
1015
1041
|
ChildRing.ring(@session, why: "question", state_dir: @state_dir)
|
|
1016
1042
|
@asked_parent = true
|
|
1017
1043
|
end
|
|
1018
1044
|
|
|
1045
|
+
# The kinds of question that ring a delegate child's parent (#ring_question).
|
|
1046
|
+
RINGING_KINDS = ["", ContinueOffer::KIND, "hook"].freeze
|
|
1047
|
+
|
|
1019
1048
|
# WorkerInbound.save_or_log: a failed save between turns is logged.
|
|
1020
1049
|
def save_or_log(at, &) = WorkerInbound.save_or_log(at, &)
|
|
1021
1050
|
|
|
@@ -1105,17 +1134,37 @@ module Samagotchi
|
|
|
1105
1134
|
# the turn as its own).
|
|
1106
1135
|
# @return [Array(Array<ChildReports::Report>, Hash|nil)] the reports
|
|
1107
1136
|
# and the mark of their message
|
|
1108
|
-
|
|
1137
|
+
# A report whose question goes to the parent's user is relayed here
|
|
1138
|
+
# (ChildReports#resolve), before the boundary announces or merges
|
|
1139
|
+
# anything; one whose card a Stop closed is held back until a human's
|
|
1140
|
+
# input (no wake turn re-offers it at once; other wakes go on).
|
|
1141
|
+
# +resolve+ false: a wake turn's take before the turn starts.
|
|
1142
|
+
def take_child_reports(resolve: true)
|
|
1109
1143
|
if (reports = @wakes.take_handed)
|
|
1110
|
-
return [reports, { turn_start: true, turn_id: @turn_id }.compact]
|
|
1144
|
+
return [resolve_relays(reports), { turn_start: true, turn_id: @turn_id }.compact]
|
|
1111
1145
|
end
|
|
1112
1146
|
|
|
1113
|
-
|
|
1147
|
+
taken = @child_reports.take
|
|
1148
|
+
[resolve ? resolve_relays(taken) : taken, nil]
|
|
1114
1149
|
rescue StandardError => e
|
|
1115
1150
|
Log.warn(:worker, "delegate_reports_failed", error: e.class.name, msg: e.message)
|
|
1116
1151
|
[[], nil]
|
|
1117
1152
|
end
|
|
1118
1153
|
|
|
1154
|
+
def resolve_relays(reports)
|
|
1155
|
+
return reports if reports.none?(&:relay)
|
|
1156
|
+
|
|
1157
|
+
relay = @engine.relay_peer
|
|
1158
|
+
stopped = false
|
|
1159
|
+
reports.filter_map do |report|
|
|
1160
|
+
next report unless report.relay
|
|
1161
|
+
# The turn stopped on an earlier card: no more cards in it.
|
|
1162
|
+
next @child_reports.hold_back(report) if stopped
|
|
1163
|
+
|
|
1164
|
+
@child_reports.resolve(report, relay: relay).tap { |resolved| stopped = resolved.nil? }
|
|
1165
|
+
end
|
|
1166
|
+
end
|
|
1167
|
+
|
|
1119
1168
|
# Check again with the event log held, which the Bridge holds while it
|
|
1120
1169
|
# queues a POST /turn, then close the Bridge's intake so no client can
|
|
1121
1170
|
# queue one after the check, and stop the idle jobs (reminder callback,
|
|
@@ -1237,6 +1286,7 @@ module Samagotchi
|
|
|
1237
1286
|
|
|
1238
1287
|
ArchiveStore.user_input(@session_id, state_dir: @state_dir)
|
|
1239
1288
|
@wakes.human_input!(interactive: ClientId.interactive?(client_id))
|
|
1289
|
+
@child_reports&.release_holds
|
|
1240
1290
|
end
|
|
1241
1291
|
|
|
1242
1292
|
def stopped_on_disk?
|
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.
|
|
4
|
+
version: 0.88.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -422,6 +422,7 @@ files:
|
|
|
422
422
|
- lib/samagotchi/note_delivery.rb
|
|
423
423
|
- lib/samagotchi/output_formatter.rb
|
|
424
424
|
- lib/samagotchi/owner_lock.rb
|
|
425
|
+
- lib/samagotchi/parent_answers_command.rb
|
|
425
426
|
- lib/samagotchi/parent_report.rb
|
|
426
427
|
- lib/samagotchi/paths.rb
|
|
427
428
|
- lib/samagotchi/peer_sources.rb
|