samagotchi 0.63.0 → 0.64.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 +46 -1
- data/docs/architecture.md +12 -2
- data/docs/cli.md +16 -8
- data/docs/configuration.md +15 -8
- data/docs/internals/engine.md +10 -3
- data/docs/plugins.md +7 -0
- data/docs/sessions.md +11 -8
- data/docs/sub-agent.md +21 -4
- data/docs/testing.md +6 -0
- data/lib/samagotchi/answer_command.rb +5 -1
- data/lib/samagotchi/bridge/turn_accumulator.rb +1 -0
- data/lib/samagotchi/bridge.rb +18 -4
- data/lib/samagotchi/bundles/slow-commands/manifest.yml +2 -2
- data/lib/samagotchi/bundles/slow-commands/plugin.rb +5 -3
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/child_reports.rb +7 -2
- data/lib/samagotchi/cli/parent_wait.rb +9 -2
- data/lib/samagotchi/composer_route.rb +90 -0
- data/lib/samagotchi/config.rb +6 -3
- data/lib/samagotchi/continue_offer.rb +17 -3
- data/lib/samagotchi/delivery.rb +13 -8
- data/lib/samagotchi/engine.rb +2 -0
- data/lib/samagotchi/host_config.rb +6 -6
- data/lib/samagotchi/host_model.rb +2 -2
- data/lib/samagotchi/interrupted_turn.rb +7 -3
- data/lib/samagotchi/last_turn.rb +5 -2
- data/lib/samagotchi/pending_input_queue.rb +10 -3
- data/lib/samagotchi/plugin/sessions.rb +27 -5
- data/lib/samagotchi/reply_wait.rb +171 -6
- data/lib/samagotchi/send_command.rb +63 -19
- data/lib/samagotchi/session_commands.rb +1 -1
- data/lib/samagotchi/session_inbox.rb +63 -7
- data/lib/samagotchi/session_manager.rb +49 -20
- data/lib/samagotchi/terminal_ui/attached_loop.rb +47 -26
- data/lib/samagotchi/terminal_ui/formatting.rb +3 -19
- data/lib/samagotchi/terminal_ui.rb +72 -57
- data/lib/samagotchi/tool_activity.rb +5 -5
- data/lib/samagotchi/tools/delegate.rb +28 -1
- data/lib/samagotchi/tools/delegate_cursor.rb +9 -4
- data/lib/samagotchi/tools/delegate_wait.rb +17 -5
- data/lib/samagotchi/tools/execute.rb +11 -4
- data/lib/samagotchi/turn_checkpoint.rb +9 -3
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/public/app.js +6 -0
- data/lib/samagotchi/web/public/commands.js +8 -8
- data/lib/samagotchi/web/public/composer.js +3 -3
- data/lib/samagotchi/web/public/timing.js +2 -1
- data/lib/samagotchi/web/public/turn_model.js +13 -3
- data/lib/samagotchi/worker.rb +7 -5
- metadata +2 -1
|
@@ -10,9 +10,14 @@ module Samagotchi
|
|
|
10
10
|
# What a parent was already told about one delegate child: the newest
|
|
11
11
|
# reply it was given (+reply_file+, ReplyWait's cursor) and the child as
|
|
12
12
|
# it was then (+messages+, +question_id+, +last_turn+: ReplyWait's
|
|
13
|
-
# baseline). nil +messages+ means no baseline.
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
# baseline). nil +messages+ means no baseline. +turn+: the enqueued_id
|
|
14
|
+
# of the child's turn the last follow-up went to (Delegate.follow_up),
|
|
15
|
+
# whose own reply a wait takes (ReplyWait's turn:); nil once that turn
|
|
16
|
+
# was reported, or when unknown (a new child, an older worker).
|
|
17
|
+
DelegateCursor = Data.define(:reply_file, :last_turn, :question_id, :messages, :turn) do
|
|
18
|
+
def initialize(reply_file: nil, last_turn: nil, question_id: nil, messages: nil, turn: nil) = super
|
|
19
|
+
|
|
20
|
+
def self.empty = new
|
|
16
21
|
|
|
17
22
|
# @param baseline [Hash, nil] ReplyWait.baseline_of's
|
|
18
23
|
def with_baseline(baseline)
|
|
@@ -93,7 +98,7 @@ module Samagotchi
|
|
|
93
98
|
return DelegateCursor.empty unless data.is_a?(Hash)
|
|
94
99
|
|
|
95
100
|
DelegateCursor.new(reply_file: data["reply_file"], last_turn: data["last_turn"],
|
|
96
|
-
question_id: data["question_id"], messages: data["messages"])
|
|
101
|
+
question_id: data["question_id"], messages: data["messages"], turn: data["turn"])
|
|
97
102
|
end
|
|
98
103
|
|
|
99
104
|
def path(parent_id, state_dir:)
|
|
@@ -72,7 +72,8 @@ module Samagotchi
|
|
|
72
72
|
started = monotonic
|
|
73
73
|
wait = ReplyWait.call(child_id, state_dir: sd, cursor: cursor.reply_file, timeout: [left, 0].max,
|
|
74
74
|
poll_interval: poll_interval, cancelled: cancelled, baseline: baseline,
|
|
75
|
-
owner_grace: owner_grace, interject: others && -> { others.poll }
|
|
75
|
+
owner_grace: owner_grace, interject: others && -> { others.poll },
|
|
76
|
+
turn: cursor.turn)
|
|
76
77
|
left -= monotonic - started
|
|
77
78
|
unless wait.status == :waiting_for_answer && DelegateRelay.relayable?(relay, wait.question)
|
|
78
79
|
outcomes.concat(relayed_outcomes.delete(key) || [])
|
|
@@ -143,7 +144,9 @@ module Samagotchi
|
|
|
143
144
|
# gave, and the child as that same look loaded it (a fresh load could
|
|
144
145
|
# swallow a turn that ended in between), so a later wait looks for a
|
|
145
146
|
# later turn and a question already reported isn't reported again.
|
|
146
|
-
#
|
|
147
|
+
# The follow-up's turn (DelegateCursor#turn) is done with once its end
|
|
148
|
+
# was reported; a question keeps it. Nothing moves on a timeout or a
|
|
149
|
+
# cancel.
|
|
147
150
|
# @param wait [ReplyWait::Result]
|
|
148
151
|
# @return [DelegateCursor, nil] the new cursor
|
|
149
152
|
def advance(parent_id, child_id, wait, state_dir:)
|
|
@@ -151,6 +154,7 @@ module Samagotchi
|
|
|
151
154
|
|
|
152
155
|
DelegateCursors.update(parent_id, child_id, state_dir: state_dir) do |cursor|
|
|
153
156
|
cursor = cursor.with(reply_file: wait.file) if wait.status == :done
|
|
157
|
+
cursor = cursor.with(turn: nil) unless wait.status == :waiting_for_answer
|
|
154
158
|
cursor.with_baseline(ReplyWait.baseline_of(wait.session))
|
|
155
159
|
end
|
|
156
160
|
end
|
|
@@ -187,20 +191,28 @@ module Samagotchi
|
|
|
187
191
|
|
|
188
192
|
# Point the cursor at the newest reply now, so only a later one counts
|
|
189
193
|
# (a follow-up sent to a child that already answered), and take the
|
|
190
|
-
# child's baseline before the follow-up goes in.
|
|
194
|
+
# child's baseline before the follow-up goes in. No turn yet
|
|
195
|
+
# (#mark_turn, once it went in).
|
|
191
196
|
# @param child [Session] the child as loaded before sending
|
|
192
197
|
def mark_seen(parent_id, child, state_dir:)
|
|
193
198
|
newest = ReplyWait.newest_reply(child.id, state_dir: state_dir)
|
|
194
199
|
DelegateCursors.update(parent_id, child.id, state_dir: state_dir) do |cursor|
|
|
195
|
-
cursor.with(reply_file: newest).with_baseline(ReplyWait.baseline_of(child))
|
|
200
|
+
cursor.with(reply_file: newest, turn: nil).with_baseline(ReplyWait.baseline_of(child))
|
|
196
201
|
end
|
|
197
202
|
end
|
|
198
203
|
|
|
204
|
+
# The follow-up went in: the child's turn it joins, whose reply the
|
|
205
|
+
# wait takes (DelegateCursor#turn).
|
|
206
|
+
# @param turn [String, nil]
|
|
207
|
+
def mark_turn(parent_id, child_id, turn, state_dir:)
|
|
208
|
+
DelegateCursors.update(parent_id, child_id, state_dir: state_dir) { |cursor| cursor.with(turn: turn) }
|
|
209
|
+
end
|
|
210
|
+
|
|
199
211
|
# A new child's baseline: as spawned, before its first turn ran.
|
|
200
212
|
# @param child [Session] the session spawn_session returned
|
|
201
213
|
def mark_started(parent_id, child, state_dir:)
|
|
202
214
|
DelegateCursors.update(parent_id, child.id, state_dir: state_dir) do |cursor|
|
|
203
|
-
cursor.with_baseline(ReplyWait.baseline_of(child, question_id: nil))
|
|
215
|
+
cursor.with(turn: nil).with_baseline(ReplyWait.baseline_of(child, question_id: nil))
|
|
204
216
|
end
|
|
205
217
|
end
|
|
206
218
|
|
|
@@ -32,6 +32,10 @@ module Samagotchi
|
|
|
32
32
|
# stopped the turn: its cancel reason is :signal.
|
|
33
33
|
NOT_RUN_ON_SIGNAL = "Error: not run, a signal to chi stopped the turn"
|
|
34
34
|
STOPPED_BY_SIGNAL = "Error: command stopped by a signal to chi"
|
|
35
|
+
# The same two when a session stop (chi sessions stop, the web's stop,
|
|
36
|
+
# a parent stopping its delegate) canceled the turn: :session_stopped.
|
|
37
|
+
NOT_RUN_ON_SESSION_STOP = "Error: not run, the session was stopped"
|
|
38
|
+
STOPPED_BY_SESSION_STOP = "Error: command stopped as the session was stopped"
|
|
35
39
|
# The first words of a command killed at the limit in kill mode (hooks
|
|
36
40
|
# match on it).
|
|
37
41
|
TIMEOUT_PREFIX = "Error: command timed out after"
|
|
@@ -79,7 +83,8 @@ module Samagotchi
|
|
|
79
83
|
# @param cancelled [#call] true once the user stopped the turn: the
|
|
80
84
|
# command is killed (or not started) and its output so far returned
|
|
81
85
|
# @param stop_reason [#call] the stop's reason (:signal: a signal to
|
|
82
|
-
# chi, which the result then names
|
|
86
|
+
# chi, :session_stopped: a session stop, which the result then names
|
|
87
|
+
# instead of the user)
|
|
83
88
|
# @param env [Hash, nil] added to the command's environment
|
|
84
89
|
# @param on_timeout [Symbol, String, nil] :task or :kill (ON_TIMEOUT);
|
|
85
90
|
# nil reads execute.on_timeout
|
|
@@ -116,16 +121,18 @@ module Samagotchi
|
|
|
116
121
|
# a long command, and return whatever it printed before it was killed.
|
|
117
122
|
stopped_result("#{TIMEOUT_PREFIX} #{timeout_sec}s\n#{TIMEOUT_HINT}", e)
|
|
118
123
|
rescue CommandCancelled => e
|
|
119
|
-
first =
|
|
124
|
+
first = STOPPED_BY.fetch(stop_reason.call, STOPPED_BY_USER)
|
|
120
125
|
stopped_result("#{first} after #{e.elapsed.round}s (killed; rerun it if still needed)", e)
|
|
121
126
|
rescue StandardError => e
|
|
122
127
|
"Error: #{e.message}"
|
|
123
128
|
end
|
|
124
129
|
|
|
125
130
|
# What a command the turn's stop kept from starting returns.
|
|
126
|
-
def self.not_run(stop_reason) =
|
|
131
|
+
def self.not_run(stop_reason) = NOT_RUN.fetch(stop_reason.call, NOT_RUN_ON_STOP)
|
|
127
132
|
|
|
128
|
-
|
|
133
|
+
# The texts above by the stop's reason; any other is the user's Stop.
|
|
134
|
+
NOT_RUN = { signal: NOT_RUN_ON_SIGNAL, session_stopped: NOT_RUN_ON_SESSION_STOP }.freeze
|
|
135
|
+
STOPPED_BY = { signal: STOPPED_BY_SIGNAL, session_stopped: STOPPED_BY_SESSION_STOP }.freeze
|
|
129
136
|
|
|
130
137
|
# execute.on_timeout, or the caller's: :task (the default) or :kill.
|
|
131
138
|
def self.on_timeout_mode(value)
|
|
@@ -67,6 +67,7 @@ module Samagotchi
|
|
|
67
67
|
@boundaries = 0
|
|
68
68
|
@written_at = nil
|
|
69
69
|
@started_at = nil
|
|
70
|
+
@enqueued_id = nil
|
|
70
71
|
end
|
|
71
72
|
|
|
72
73
|
# A turn begins (before Engine#run_turn and the worker's running save):
|
|
@@ -74,14 +75,18 @@ module Samagotchi
|
|
|
74
75
|
# images, no turn id), marked at iteration 0, so a worker killed before
|
|
75
76
|
# the loop's first boundary leaves the prompt and a mark the next one
|
|
76
77
|
# settles. A continue turn (no prompt) is the history, marked.
|
|
78
|
+
# Every mark of the turn carries +enqueued_id+ (InterruptedTurn's).
|
|
77
79
|
# @param prompt [String, nil]
|
|
78
|
-
|
|
80
|
+
# @param enqueued_id [String, nil] the input the turn runs
|
|
81
|
+
def begin_turn(prompt: nil, enqueued_id: nil)
|
|
79
82
|
@boundaries = 0
|
|
83
|
+
@enqueued_id = enqueued_id.to_s.empty? ? nil : enqueued_id.to_s
|
|
80
84
|
@written_at = nil
|
|
81
85
|
@started_at = Time.now.iso8601(3)
|
|
82
86
|
messages = Array(@session.messages).dup
|
|
83
87
|
messages << { role: "user", content: prompt } unless prompt.to_s.empty?
|
|
84
|
-
@session.turn_checkpoint = { messages: messages, marker: InterruptedTurn.new(started_at: @started_at, at: @started_at, iteration: 0
|
|
88
|
+
@session.turn_checkpoint = { messages: messages, marker: InterruptedTurn.new(started_at: @started_at, at: @started_at, iteration: 0,
|
|
89
|
+
enqueued_id: @enqueued_id) }
|
|
85
90
|
end
|
|
86
91
|
|
|
87
92
|
# The turn ended (or never began): the session saves its own messages.
|
|
@@ -104,7 +109,8 @@ module Samagotchi
|
|
|
104
109
|
# One level deep: the loop may edit an entry in place later (a
|
|
105
110
|
# forget stub) while a save on another thread serializes this one.
|
|
106
111
|
messages = conversation.map { |message| message.is_a?(Hash) ? message.dup : message } + merged
|
|
107
|
-
marker = InterruptedTurn.new(id: turn_id, started_at: @started_at, at: Time.now.iso8601(3), iteration: @boundaries
|
|
112
|
+
marker = InterruptedTurn.new(id: turn_id, started_at: @started_at, at: Time.now.iso8601(3), iteration: @boundaries,
|
|
113
|
+
enqueued_id: @enqueued_id)
|
|
108
114
|
@session.turn_checkpoint = { messages: messages, marker: marker }
|
|
109
115
|
return false unless due? || !merged.empty?
|
|
110
116
|
|
data/lib/samagotchi/version.rb
CHANGED
|
@@ -1741,6 +1741,7 @@ async function handleSendTurn() {
|
|
|
1741
1741
|
updateInfoBar();
|
|
1742
1742
|
}
|
|
1743
1743
|
actionBtn.disabled = true;
|
|
1744
|
+
const sentTo = selected;
|
|
1744
1745
|
try {
|
|
1745
1746
|
for (const chip of sending) {
|
|
1746
1747
|
if (!chip.ref) chip.ref = await uploadImage(selected, chip.file, chip.name);
|
|
@@ -1787,6 +1788,11 @@ async function handleSendTurn() {
|
|
|
1787
1788
|
updateComposerMode();
|
|
1788
1789
|
return;
|
|
1789
1790
|
}
|
|
1791
|
+
// A stale tab sent into a session deleted meanwhile: the start page
|
|
1792
|
+
// says so, and the line stays in the composer.
|
|
1793
|
+
if (e.code === "not_found") return leaveGoneSession(sentTo, "not_found");
|
|
1794
|
+
// A /queue to a worker that can't queue (it would steer): its words, the text kept.
|
|
1795
|
+
if (e.code === "delivery_unavailable") return addCommandBubble({ label: null, line, text: e.body?.detail || e.message, failed: true });
|
|
1790
1796
|
alert(e.message);
|
|
1791
1797
|
} finally {
|
|
1792
1798
|
actionBtn.disabled = current.owner === "tui";
|
|
@@ -23,16 +23,17 @@ export function sessionCommandLine(text, commands) {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
// /cut TEXT and /queue TEXT: the UIs' own words for a delivery (listed
|
|
26
|
-
// for Tab and /help, never run as commands;
|
|
27
|
-
//
|
|
26
|
+
// for Tab and /help, never run as commands; ComposerRoute::STEER_CHOICE
|
|
27
|
+
// (spec/fixtures/composer_routes.json): the word, then any whitespace and the text). During a
|
|
28
28
|
// turn /cut TEXT is the text sent as a cut (POST /turn, delivery "cut"),
|
|
29
|
-
// as chi send --cut does
|
|
30
|
-
//
|
|
31
|
-
//
|
|
29
|
+
// as chi send --cut does, and /queue TEXT the text queued to run as a turn
|
|
30
|
+
// of its own after this one (delivery "queue"; a worker that can't queue
|
|
31
|
+
// refuses it, and the page shows the refusal's detail). With no running
|
|
32
|
+
// turn both are the text as a normal turn. The bare word gets the usage
|
|
33
|
+
// line.
|
|
32
34
|
// @return {{send: string, delivery?: string} | {reply: string} | null}
|
|
33
35
|
// null: not a delivery line
|
|
34
36
|
export const STEER_CHOICE_USAGE = "(/cut and /queue need a message: /cut TEXT, /queue TEXT)";
|
|
35
|
-
export const QUEUE_UNAVAILABLE_WEB = "(/queue isn't available in the web yet; a plain message goes in at the running turn's next step, and /cut TEXT cuts in)";
|
|
36
37
|
const STEER_CHOICE = /^\/(cut|queue)(?:\s+([\s\S]*))?$/;
|
|
37
38
|
|
|
38
39
|
export function steerChoiceRoute(text, { running = false } = {}) {
|
|
@@ -41,8 +42,7 @@ export function steerChoiceRoute(text, { running = false } = {}) {
|
|
|
41
42
|
const message = (match[2] || "").trim();
|
|
42
43
|
if (!message) return { reply: STEER_CHOICE_USAGE };
|
|
43
44
|
if (!running) return { send: message };
|
|
44
|
-
|
|
45
|
-
return { send: message, delivery: "cut" };
|
|
45
|
+
return { send: message, delivery: match[1] };
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
// A line that is one word: a slash and a name, no spaces, no second slash
|
|
@@ -7,8 +7,8 @@ import { sessionCommandLine, startPageReplyOrHint, steerChoiceRoute, unknownComm
|
|
|
7
7
|
|
|
8
8
|
// Where a composer line goes, in the order the page reads it:
|
|
9
9
|
// 1. /cut TEXT, /queue TEXT (steerChoiceRoute): during a turn in a session
|
|
10
|
-
// the text as a cut
|
|
11
|
-
// as the line.
|
|
10
|
+
// the text as a cut or queued for after the turn; else the text goes
|
|
11
|
+
// on as the line.
|
|
12
12
|
// 2. On the start page (+session+ false): the page's own reply or a typo
|
|
13
13
|
// hint (startPageReplyOrHint) needs no session; anything else makes one.
|
|
14
14
|
// 3. In a session (routeSessionLine): a session command, the page's own
|
|
@@ -21,7 +21,7 @@ import { sessionCommandLine, startPageReplyOrHint, steerChoiceRoute, unknownComm
|
|
|
21
21
|
// +send+ again in it (routeSessionLine, with its
|
|
22
22
|
// commands)
|
|
23
23
|
// {command} POST /command
|
|
24
|
-
// {send, delivery} POST /turn (+delivery+ "cut" or null)
|
|
24
|
+
// {send, delivery} POST /turn (+delivery+ "cut", "queue" or null)
|
|
25
25
|
export function routeComposerLine(text, { session = true, running = false, commands = [], images = 0 } = {}) {
|
|
26
26
|
const choice = steerChoiceRoute(text, { running: running && session });
|
|
27
27
|
if (choice?.reply) return { reply: choice.reply, line: text };
|
|
@@ -33,7 +33,8 @@ export function liveDurationText(durationMs) {
|
|
|
33
33
|
|
|
34
34
|
// A cancel's reason as the terminal UIs name it too (Formatting::
|
|
35
35
|
// CANCEL_REASONS); anything else as it is. Both sides: spec/shared/labels_matrix.json.
|
|
36
|
-
const CANCEL_REASONS = { ctrl_c: "Ctrl-C", user: "stopped", hook: "by a hook", worker_stopped: "worker stopped"
|
|
36
|
+
const CANCEL_REASONS = { ctrl_c: "Ctrl-C", user: "stopped", hook: "by a hook", worker_stopped: "worker stopped",
|
|
37
|
+
session_stopped: "session stopped" };
|
|
37
38
|
|
|
38
39
|
// The line a canceled turn ends with: "■ canceled (stopped)", live from
|
|
39
40
|
// turn_canceled's reason and after a re-render from the turn record's
|
|
@@ -222,13 +222,23 @@ export function genLabelParts(gen) {
|
|
|
222
222
|
if (line) return { head: cut(line), calls, repeats: true };
|
|
223
223
|
if (first?.title) return { head: cut(`${toolName(first)} ${first.title}`), calls, repeats: true };
|
|
224
224
|
if (gen.tools.length) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
225
|
+
// A plugin label that is a verb ("echoing", the default "calling
|
|
226
|
+
// tool") is the head itself when every call has one; among other
|
|
227
|
+
// tools it gives way to its tool's name ("working with read, echo").
|
|
228
|
+
const verbs = gen.tools.every(isVerbLabel);
|
|
229
|
+
const names = [...new Set(gen.tools.map((r) => (verbs || !isVerbLabel(r) ? toolName(r) : r.tool || "tool")))];
|
|
230
|
+
const list = `${names.slice(0, LABEL_TOOLS).join(", ")}${names.length > LABEL_TOOLS ? ", …" : ""}`;
|
|
231
|
+
return { head: verbs ? list : `working with ${list}`, calls, repeats: false };
|
|
228
232
|
}
|
|
229
233
|
return { head: "thinking", calls: "", repeats: false };
|
|
230
234
|
}
|
|
231
235
|
|
|
236
|
+
// A plugin tool's label that reads as a verb (its first word ends in
|
|
237
|
+
// "ing": "echoing", "calling tool"), not as a name ("chrome: screenshot").
|
|
238
|
+
function isVerbLabel(row) {
|
|
239
|
+
return /^[a-z]+ing\b/i.test(String(row?.label || ""));
|
|
240
|
+
}
|
|
241
|
+
|
|
232
242
|
// A step that said something (its text is not the turn's answer): a row of
|
|
233
243
|
// its own in a finished block's outline.
|
|
234
244
|
export function isNarrated(gen) {
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -900,7 +900,7 @@ module Samagotchi
|
|
|
900
900
|
@reporting_turn = reports_to_parent?(turn_args[:origin])
|
|
901
901
|
restores = @turn_flow.restores
|
|
902
902
|
begin
|
|
903
|
-
@checkpoint.begin_turn(prompt: prompt)
|
|
903
|
+
@checkpoint.begin_turn(prompt: prompt, enqueued_id: turn_args[:origin]&.dig(:enqueued_id))
|
|
904
904
|
@session.status = Session::STATUS_RUNNING
|
|
905
905
|
# A continue that moved this session to the chain's new link wrote
|
|
906
906
|
# the new parent beside it (Session::PARENT_FILE): this turn's
|
|
@@ -929,7 +929,7 @@ module Samagotchi
|
|
|
929
929
|
# no reply: its text is what came before its last tool calls, and a
|
|
930
930
|
# wait (chi send --wait) gets the question instead.
|
|
931
931
|
unless response.nil? || response.strip.empty? || @continue_offer.awaiting?
|
|
932
|
-
SessionInbox.write_output(@session_dir, response)
|
|
932
|
+
SessionInbox.write_output(@session_dir, response, enqueued_id: turn_args[:origin]&.dig(:enqueued_id))
|
|
933
933
|
end
|
|
934
934
|
save_or_log(:turn) { save_session }
|
|
935
935
|
# The reports the turn merged were read unless its end took the turn
|
|
@@ -1045,10 +1045,12 @@ module Samagotchi
|
|
|
1045
1045
|
@pending_input_drain ||= lambda do
|
|
1046
1046
|
# An iteration boundary: tell whoever sent a command now that it waits.
|
|
1047
1047
|
refuse_queued_commands(mid_turn: true)
|
|
1048
|
-
# A line with images runs as its own
|
|
1049
|
-
#
|
|
1048
|
+
# A queued line or one with images runs as its own turn after this
|
|
1049
|
+
# one (steering merges text only): skipped here, not a barrier, so a
|
|
1050
|
+
# line sent after it still goes in at this boundary. Only the files
|
|
1051
|
+
# claimed below reach @merged_this_turn and the checkpoint.
|
|
1050
1052
|
files = SessionInbox.find_new_input_files(@session_dir).sort
|
|
1051
|
-
.
|
|
1053
|
+
.reject { |input_file| SessionInbox.waits_for_turn_end?(input_file) }
|
|
1052
1054
|
merged = files.filter_map do |input_file|
|
|
1053
1055
|
claimed_file = SessionInbox.claim_input_file(input_file)
|
|
1054
1056
|
next unless claimed_file
|
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.64.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -235,6 +235,7 @@ files:
|
|
|
235
235
|
- lib/samagotchi/client_id.rb
|
|
236
236
|
- lib/samagotchi/command_steps.rb
|
|
237
237
|
- lib/samagotchi/commands/registry.rb
|
|
238
|
+
- lib/samagotchi/composer_route.rb
|
|
238
239
|
- lib/samagotchi/config.rb
|
|
239
240
|
- lib/samagotchi/config_text_edit.rb
|
|
240
241
|
- lib/samagotchi/context_absorber.rb
|