samagotchi 0.45.0 → 0.46.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 +51 -1
- data/docs/architecture.md +2 -1
- data/docs/cli.md +14 -5
- data/docs/configuration.md +102 -5
- data/docs/internals/background-tasks.md +1 -0
- data/docs/internals/engine.md +1 -1
- data/docs/internals/llm-context-live.md +111 -0
- data/docs/sessions.md +2 -2
- data/lib/samagotchi/bridge/turn_accumulator.rb +5 -0
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/config.rb +57 -4
- data/lib/samagotchi/desktop/macos/Panel.swift +7 -3
- data/lib/samagotchi/engine.rb +39 -6
- data/lib/samagotchi/host_model.rb +116 -0
- data/lib/samagotchi/host_registry.rb +15 -9
- data/lib/samagotchi/kernel_loop.rb +2 -1
- data/lib/samagotchi/llm/chat_loop.rb +89 -7
- data/lib/samagotchi/llm/turn_settings.rb +5 -3
- data/lib/samagotchi/model_catalog.rb +17 -12
- data/lib/samagotchi/model_price.rb +54 -0
- data/lib/samagotchi/model_profile.rb +3 -1
- data/lib/samagotchi/process_group.rb +56 -9
- data/lib/samagotchi/recap_setup.rb +1 -1
- data/lib/samagotchi/self_report.rb +2 -1
- data/lib/samagotchi/session_commands.rb +23 -14
- data/lib/samagotchi/session_metrics.rb +40 -5
- data/lib/samagotchi/terminal_ui/attached_view.rb +2 -0
- data/lib/samagotchi/terminal_ui/formatting.rb +26 -9
- data/lib/samagotchi/terminal_ui/status_row.rb +12 -6
- data/lib/samagotchi/terminal_ui.rb +13 -0
- data/lib/samagotchi/tools/delegate_wait.rb +5 -4
- data/lib/samagotchi/tools/task_runtime.rb +11 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +5 -2
- data/lib/samagotchi/web/public/app.js +7 -3
- data/lib/samagotchi/web/public/ctx.js +20 -7
- data/lib/samagotchi/web/public/format.js +7 -2
- data/lib/samagotchi/web/public/model_pick.js +23 -3
- data/lib/samagotchi/web/public/model_picker.js +5 -3
- data/lib/samagotchi/web/public/stage_view.js +1 -0
- data/lib/samagotchi/web/public/turn_events.js +3 -2
- data/lib/samagotchi/web/public/turn_model.js +16 -0
- data/lib/samagotchi/web/public/turn_view.js +19 -0
- data/lib/samagotchi/worker.rb +16 -1
- metadata +4 -1
|
@@ -84,10 +84,11 @@ module Samagotchi
|
|
|
84
84
|
outcomes << outcome.line
|
|
85
85
|
return with_outcomes(canceled_result(child_id, reports: reports), outcomes) if outcome.stopped
|
|
86
86
|
|
|
87
|
-
# Wait on
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
|
|
87
|
+
# Wait on from the child as it was before the relay (read when it
|
|
88
|
+
# asked), the relayed question no longer reported: a turn that
|
|
89
|
+
# ends with no reply during the relay's POST or after it still
|
|
90
|
+
# ends the wait.
|
|
91
|
+
baseline = ReplyWait.baseline_of(wait.session, question_id: wait.question[:id])
|
|
91
92
|
end
|
|
92
93
|
rescue ArgumentError => e
|
|
93
94
|
"Error: #{e.message}"
|
|
@@ -66,6 +66,8 @@ module Samagotchi
|
|
|
66
66
|
err: output_io
|
|
67
67
|
)
|
|
68
68
|
Process.detach(pid)
|
|
69
|
+
# Read now, compared before each signal: a pid reused later is not this task.
|
|
70
|
+
pid_started = ProcessGroup.start_time(pid)
|
|
69
71
|
|
|
70
72
|
now = timestamp
|
|
71
73
|
record = {
|
|
@@ -76,6 +78,7 @@ module Samagotchi
|
|
|
76
78
|
"workspace_root" => Dir.pwd,
|
|
77
79
|
"status" => "running",
|
|
78
80
|
"pid" => pid,
|
|
81
|
+
"pid_started" => pid_started,
|
|
79
82
|
"created_at" => now,
|
|
80
83
|
"started_at" => now,
|
|
81
84
|
"finished_at" => nil,
|
|
@@ -190,7 +193,7 @@ module Samagotchi
|
|
|
190
193
|
|
|
191
194
|
pid = refreshed["pid"]
|
|
192
195
|
# Checked again just before the signal: never a group chi didn't start.
|
|
193
|
-
return [mark_not_chis(refreshed), nil] unless
|
|
196
|
+
return [mark_not_chis(refreshed), nil] unless chis_group?(refreshed, started: refreshed["pid_started"])
|
|
194
197
|
|
|
195
198
|
refreshed["stop_requested_by"] = by.to_s
|
|
196
199
|
write_record(refreshed)
|
|
@@ -210,13 +213,19 @@ module Samagotchi
|
|
|
210
213
|
return record unless record["status"] == "running"
|
|
211
214
|
|
|
212
215
|
pid = record["pid"]
|
|
213
|
-
return record if
|
|
216
|
+
return record if chis_group?(record)
|
|
214
217
|
# Gone: its exit code says how it ended.
|
|
215
218
|
return mark_finished_without_exit_code(record) if ProcessGroup.signalable?(pid) && !ProcessGroup.alive?(pid)
|
|
216
219
|
|
|
217
220
|
mark_not_chis(record)
|
|
218
221
|
end
|
|
219
222
|
|
|
223
|
+
# Whether the record's pid still leads its own group. With +started+
|
|
224
|
+
# (the start time recorded at task_create), also the same process: a
|
|
225
|
+
# ps per call, so only task_stop asks it, just before the signal;
|
|
226
|
+
# a refresh (each task_wait poll, task_list) checks the group only.
|
|
227
|
+
def chis_group?(record, started: nil) = ProcessGroup.leader?(record["pid"], started: started)
|
|
228
|
+
|
|
220
229
|
# A running record whose pid isn't a group chi started (NOT_CHIS_PROCESS)
|
|
221
230
|
# ends as failed, unless its exit code or a stop on disk says more.
|
|
222
231
|
def mark_not_chis(record)
|
data/lib/samagotchi/version.rb
CHANGED
data/lib/samagotchi/web/app.rb
CHANGED
|
@@ -443,7 +443,9 @@ module Samagotchi
|
|
|
443
443
|
warnings.concat(listing.warnings)
|
|
444
444
|
# the web's own spelling, kept as it was: bare on the default host
|
|
445
445
|
listing.rows.each do |row|
|
|
446
|
-
|
|
446
|
+
model = { name: row.host == default_host ? row.id : "#{row.host}:#{row.id}", host: row.host, id: row.id }
|
|
447
|
+
model[:configured] = true if row.configured
|
|
448
|
+
models << model
|
|
447
449
|
end
|
|
448
450
|
end
|
|
449
451
|
rescue StandardError => e
|
|
@@ -657,7 +659,8 @@ module Samagotchi
|
|
|
657
659
|
session_json = session_to_json(session, status: displayed_status(session, snapshot, owner: owner), owner: owner)
|
|
658
660
|
# What the worker's server said it served for that model (after a turn).
|
|
659
661
|
if snapshot
|
|
660
|
-
session_json = session_json.merge(served_model: snapshot["served_model"], served_model_for: snapshot["served_model_for"]
|
|
662
|
+
session_json = session_json.merge(served_model: snapshot["served_model"], served_model_for: snapshot["served_model_for"],
|
|
663
|
+
served_expected_by: snapshot["served_expected_by"])
|
|
661
664
|
# The model notes its prompt carries now (a /model there changes them).
|
|
662
665
|
session_json = session_json.merge(prompt_notes: snapshot["prompt_notes"]) if snapshot["prompt_notes"].is_a?(Array)
|
|
663
666
|
end
|
|
@@ -1156,7 +1156,7 @@ function renderSessionView(data) {
|
|
|
1156
1156
|
setCapabilities(data.capabilities);
|
|
1157
1157
|
current.status = s.status;
|
|
1158
1158
|
current.model = s.model_name;
|
|
1159
|
-
current.served = s.served_model ? [s.served_model, s.served_model_for] : null;
|
|
1159
|
+
current.served = s.served_model ? [s.served_model, s.served_model_for, s.served_expected_by || null] : null;
|
|
1160
1160
|
current.dir = s.working_directory;
|
|
1161
1161
|
current.owner = s.owner || null;
|
|
1162
1162
|
current.timing = normalizeTiming(data.timing);
|
|
@@ -2007,7 +2007,7 @@ const streamHandlers = {
|
|
|
2007
2007
|
},
|
|
2008
2008
|
generation_completed: (data) => {
|
|
2009
2009
|
liveTurn.timing.setNote("");
|
|
2010
|
-
if (data?.served_model) current.served = [data.served_model, data.requested_model];
|
|
2010
|
+
if (data?.served_model) current.served = [data.served_model, data.requested_model, data.served_expected_by || null];
|
|
2011
2011
|
// The generation's speed and the session's token totals, from the server.
|
|
2012
2012
|
current.timing.tokens = generationTokens(current.timing.tokens, data);
|
|
2013
2013
|
if (data?.served_model || data?.tokens) updateInfoBar();
|
|
@@ -2212,7 +2212,11 @@ const streamHandlers = {
|
|
|
2212
2212
|
llm_context_edited: (data) => { turnDom.notice(llmContextNotice(data)); },
|
|
2213
2213
|
// The provider is asked again after an error, or the turn waits for
|
|
2214
2214
|
// plugins' setup: a note on the live timing line until it streams.
|
|
2215
|
-
generation_retrying: (data) =>
|
|
2215
|
+
generation_retrying: (data) => {
|
|
2216
|
+
// A dropped stream's step asked again: what it streamed is void.
|
|
2217
|
+
if (data?.restarted) turnDom.generationRestarted(data);
|
|
2218
|
+
liveTurn.timing.setNote(retryStatusLine(data));
|
|
2219
|
+
},
|
|
2216
2220
|
plugin_init_wait: (data) => liveTurn.timing.setNote(initWaitLine(data)),
|
|
2217
2221
|
plugin_init_started: (data) => onInitEvent("plugin_init_started", data),
|
|
2218
2222
|
plugin_init_finished: (data) => onInitEvent("plugin_init_finished", data),
|
|
@@ -77,12 +77,24 @@ export function speedText(tps, source) {
|
|
|
77
77
|
return `${source === "estimate" ? "~" : ""}${number} tok/s`;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
// A cost in USD: "$0.42"; under a cent, four decimals ("$0.0012"); ""
|
|
81
|
-
// none or zero. As
|
|
82
|
-
|
|
80
|
+
// A cost in USD: "$0.42"; under a cent, four decimals ("$0.0012"); "~$0.12"
|
|
81
|
+
// for an estimate (hosts.<name>.models prices); "" for none or zero. As
|
|
82
|
+
// Formatting#cost_text.
|
|
83
|
+
export function costText(cost, { estimate = false } = {}) {
|
|
83
84
|
const value = num(cost);
|
|
84
85
|
if (value === null || value <= 0) return "";
|
|
85
|
-
return value >= 0.01 ? `$${value.toFixed(2)}` : `$${value.toFixed(4)}`;
|
|
86
|
+
return `${estimate ? "~" : ""}${value >= 0.01 ? `$${value.toFixed(2)}` : `$${value.toFixed(4)}`}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// The ctx tooltip's cost: the reported cost as is; with an estimate (an
|
|
90
|
+
// older worker or saved file has none) the total with "~" and both parts.
|
|
91
|
+
function costLine(tokens) {
|
|
92
|
+
const reported = num(tokens.cost_sum) || 0;
|
|
93
|
+
const estimated = num(tokens.cost_estimate_sum) || 0;
|
|
94
|
+
if (estimated <= 0) return reported > 0 ? `cost: ${costText(reported)}` : "";
|
|
95
|
+
const total = costText(reported + estimated, { estimate: true });
|
|
96
|
+
const parts = reported > 0 ? `${costText(reported)} reported, ${costText(estimated, { estimate: true })} estimated` : "estimated";
|
|
97
|
+
return `cost: ${total} (${parts})`;
|
|
86
98
|
}
|
|
87
99
|
|
|
88
100
|
function count(n) {
|
|
@@ -142,7 +154,8 @@ export function ctxWindowText(window) {
|
|
|
142
154
|
|
|
143
155
|
// The ctx tooltip (info bar and card): +title+, then the session's tokens
|
|
144
156
|
// (in summed over every request, cached with its share, out with the
|
|
145
|
-
// reasoning part) and its cost, when it has any
|
|
157
|
+
// reasoning part) and its cost, when it has any (an estimated part with
|
|
158
|
+
// "~", costLine), and the memory indexes
|
|
146
159
|
// its prompt holds (+memoryIndex+, the snapshot's memory_index); with
|
|
147
160
|
// +window+ (ctxWindowText's) the window line under the title. A
|
|
148
161
|
// delegated session keeps its own metrics, so these are this session's only.
|
|
@@ -160,8 +173,8 @@ export function tokensTipText(tokens, title = "Context used after the last turn"
|
|
|
160
173
|
if (cached > 0 && prompt > 0) parts.push(`cached ${count(cached)} (${Math.round((cached * 100) / prompt)}%)`);
|
|
161
174
|
parts.push(`out ${count(completion)}${reasoning > 0 ? ` (reasoning ${count(reasoning)})` : ""}`);
|
|
162
175
|
const lines = [title, `tokens: ${parts.join(" · ")}`];
|
|
163
|
-
const cost =
|
|
164
|
-
if (cost) lines.push(
|
|
176
|
+
const cost = costLine(tokens);
|
|
177
|
+
if (cost) lines.push(cost);
|
|
165
178
|
lines.push("this session only, all requests");
|
|
166
179
|
return `${lines.join("\n")}${memoryLine}`;
|
|
167
180
|
}
|
|
@@ -255,8 +255,13 @@ export function servedModelDiffers(asked, served) {
|
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
// The session's model for the info bar: the served model with a marker when
|
|
258
|
-
// the server serves another one than +servedFor+ (the name asked)
|
|
259
|
-
|
|
258
|
+
// the server serves another one than +servedFor+ (the name asked), unless
|
|
259
|
+
// +expectedBy+ names the host whose hosts.<name>.models served: lists it.
|
|
260
|
+
export function modelLabel(model, served, servedFor, expectedBy = null) {
|
|
261
|
+
if (expectedBy && servedModelDiffers(servedFor, served)) {
|
|
262
|
+
const name = model || "";
|
|
263
|
+
return { text: name.slice(0, 40), title: `${name}; served: ${served} (expected: hosts.${expectedBy}.models)`, mismatch: false };
|
|
264
|
+
}
|
|
260
265
|
if (servedModelDiffers(servedFor, served)) {
|
|
261
266
|
return { text: `${served.slice(0, 40)} ⚠`, title: `served: ${served}; asked for ${model}`, mismatch: true };
|
|
262
267
|
}
|
|
@@ -20,7 +20,8 @@ export const RECENT_KEY = "chi_model_recent";
|
|
|
20
20
|
// The payload's models as rows: {name, host, id, isDefaultHost}, and
|
|
21
21
|
// sampling (the configured request parameters as /model words them) when
|
|
22
22
|
// the model has some, and llm_context (the LLM context it starts under,
|
|
23
|
-
// LLMContextStrategy::Explained#summary) when the server sent one
|
|
23
|
+
// LLMContextStrategy::Explained#summary) when the server sent one, and
|
|
24
|
+
// configured when the host declares the id (hosts.<name>.models). The
|
|
24
25
|
// default host's names are bare (name == id); the server's own default,
|
|
25
26
|
// unshifted when no host lists it, has no host and goes with them, shown
|
|
26
27
|
// by its name.
|
|
@@ -34,15 +35,18 @@ export function modelRows(payload) {
|
|
|
34
35
|
const id = host ? String(m.id || name) : name;
|
|
35
36
|
const row = { name, host, id, isDefaultHost: !host || name === id };
|
|
36
37
|
if (m.sampling) row.sampling = String(m.sampling);
|
|
38
|
+
if (m.configured === true) row.configured = true;
|
|
37
39
|
if (m.llm_context && typeof m.llm_context === "object") row.llm_context = m.llm_context;
|
|
38
40
|
return row;
|
|
39
41
|
});
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
const byId = (a, b) =>
|
|
44
|
+
const byId = (a, b) => (b.configured === true) - (a.configured === true) ||
|
|
45
|
+
a.id.localeCompare(b.id, "en", { sensitivity: "base" });
|
|
43
46
|
|
|
44
47
|
// The rows under their hosts, [{host, isDefault, rows}]: the default host
|
|
45
|
-
// first, the others in the server's order
|
|
48
|
+
// first, the others in the server's order; inside a host the configured
|
|
49
|
+
// ids first, each part A-Z.
|
|
46
50
|
export function groupRows(rows) {
|
|
47
51
|
const groups = [];
|
|
48
52
|
const find = (key) => groups.find((g) => g.key === key);
|
|
@@ -61,6 +65,22 @@ export function groupRows(rows) {
|
|
|
61
65
|
}));
|
|
62
66
|
}
|
|
63
67
|
|
|
68
|
+
// The word after a row's id: "default" for the default model, else
|
|
69
|
+
// "config" for an id the host declares (hosts.<name>.models), else "".
|
|
70
|
+
export function rowNote(row, defaultName) {
|
|
71
|
+
if (row.name === defaultName) return "default";
|
|
72
|
+
return row.configured ? "config" : "";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// A row's tooltip: its configured sampling, and where a configured id
|
|
76
|
+
// comes from; "" when neither.
|
|
77
|
+
export function rowTitle(row) {
|
|
78
|
+
const parts = [];
|
|
79
|
+
if (row.sampling) parts.push(`sampling: ${row.sampling}`);
|
|
80
|
+
if (row.configured) parts.push(`served by config (hosts.${row.host}.models)`);
|
|
81
|
+
return parts.join("; ");
|
|
82
|
+
}
|
|
83
|
+
|
|
64
84
|
const SEPARATORS = "/-:._ ";
|
|
65
85
|
const atSegmentStart = (text, i) => i === 0 || SEPARATORS.includes(text[i - 1]);
|
|
66
86
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { escapeHtml } from "./format.js";
|
|
13
13
|
import { moveSelection } from "./command_complete.js";
|
|
14
14
|
import {
|
|
15
|
-
MODEL_KEY, RECENT_KEY, groupRows, matchModels, modelRows, pickModel, pushRecent, recentRows,
|
|
15
|
+
MODEL_KEY, RECENT_KEY, groupRows, matchModels, modelRows, pickModel, pushRecent, recentRows, rowNote, rowTitle,
|
|
16
16
|
} from "./model_pick.js";
|
|
17
17
|
|
|
18
18
|
const PAGE = 8;
|
|
@@ -57,9 +57,11 @@ export function createModelPicker({ button, panel, input, list, composer, center
|
|
|
57
57
|
|
|
58
58
|
function rowHtml(row, i, { hostMarks = [], idMarks = [], showHost = !row.isDefaultHost } = {}) {
|
|
59
59
|
const host = showHost ? `<span class="model-host">${markedHtml(row.host, hostMarks)}</span><span class="model-dot"> · </span>` : "";
|
|
60
|
-
const
|
|
60
|
+
const noteText = rowNote(row, defaultName);
|
|
61
|
+
const note = noteText ? `<span class="model-note">${noteText}</span>` : "";
|
|
61
62
|
const cls = `model-option${i === active ? " active" : ""}${row.name === chosen ? " chosen" : ""}`;
|
|
62
|
-
const
|
|
63
|
+
const titleText = rowTitle(row);
|
|
64
|
+
const title = titleText ? ` title="${escapeHtml(titleText)}"` : "";
|
|
63
65
|
return `<div class="${cls}" role="option" id="model-option-${i}" aria-selected="${i === active}" data-index="${i}"${title}>` +
|
|
64
66
|
`<span class="model-id">${host}${markedHtml(row.id, idMarks)}</span>${note}</div>`;
|
|
65
67
|
}
|
|
@@ -560,6 +560,7 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
560
560
|
},
|
|
561
561
|
generationStarted: (data) => view.generationStarted(data),
|
|
562
562
|
chunk: (data) => view.chunk(data),
|
|
563
|
+
generationRestarted: (data) => view.generationRestarted(data),
|
|
563
564
|
generationCompleted: () => view.generationCompleted(),
|
|
564
565
|
toolStarted: (data) => view.toolStarted(data),
|
|
565
566
|
toolCompleted: (data) => view.toolCompleted(data),
|
|
@@ -209,8 +209,9 @@ export function noticeLine(entry = {}) {
|
|
|
209
209
|
|
|
210
210
|
// The live turn's status while the provider is asked again after an error
|
|
211
211
|
// (generation_retrying): "↻ retrying (503) in 4 s, 2/5" (retry 2 of 5).
|
|
212
|
-
export function retryStatusLine({ attempt, max_retries, next_delay, status, error_class } = {}) {
|
|
213
|
-
|
|
212
|
+
export function retryStatusLine({ attempt, max_retries, next_delay, status, error_class, restarted } = {}) {
|
|
213
|
+
// restarted: a dropped stream's step asked again (the chat loop's).
|
|
214
|
+
const why = restarted ? "stream dropped" : status || String(error_class || "").split("::").pop();
|
|
214
215
|
const delay = Number(next_delay);
|
|
215
216
|
const when = Number.isFinite(delay) ? ` in ${delay < 1 ? delay.toFixed(1) : Math.round(delay)} s` : "";
|
|
216
217
|
const count = attempt ? `, ${attempt}${max_retries ? `/${max_retries}` : ""}` : "";
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
// a chunk with no iteration goes to the current gen, unless that one is
|
|
13
13
|
// textDone or already has tool rows (an older server's replay), which
|
|
14
14
|
// opens a new gen.
|
|
15
|
+
// - generation_retrying with restarted (a dropped stream's step asked
|
|
16
|
+
// again) empties its gen's thinking and text.
|
|
15
17
|
// - generation_completed marks the current gen textDone; its tool rows
|
|
16
18
|
// follow it (the kernel emits them after).
|
|
17
19
|
// - tool events go to the gen of their iteration, else to the current gen,
|
|
@@ -105,6 +107,20 @@ export function chunk(turn, { text = "", thinking = "", iteration = null } = {})
|
|
|
105
107
|
return r;
|
|
106
108
|
}
|
|
107
109
|
|
|
110
|
+
// A step asked again after its stream dropped (generation_retrying with
|
|
111
|
+
// restarted): the open gen of +iteration+ (else the current one) loses what
|
|
112
|
+
// it streamed; null when there is none.
|
|
113
|
+
export function generationRestarted(turn, iteration = null) {
|
|
114
|
+
const current = currentGen(turn);
|
|
115
|
+
const gen = iteration == null ? current
|
|
116
|
+
: [...turn.gens].reverse().find((g) => !g.closed && g.iteration === iteration) ?? null;
|
|
117
|
+
if (!gen) return null;
|
|
118
|
+
gen.thinking = "";
|
|
119
|
+
gen.text = "";
|
|
120
|
+
gen.textDone = false;
|
|
121
|
+
return { gen, opened: false, closed: null };
|
|
122
|
+
}
|
|
123
|
+
|
|
108
124
|
export function generationCompleted(turn) {
|
|
109
125
|
const gen = currentGen(turn);
|
|
110
126
|
if (gen) gen.textDone = true;
|
|
@@ -615,6 +615,25 @@ export function createTurnView({
|
|
|
615
615
|
apply(r);
|
|
616
616
|
scheduleFlush(r.gen);
|
|
617
617
|
},
|
|
618
|
+
// A dropped stream's step asked again (generation_retrying with
|
|
619
|
+
// restarted): its gen's thinking and narration so far go, the step
|
|
620
|
+
// shows "…" until the new stream's text comes.
|
|
621
|
+
generationRestarted(data) {
|
|
622
|
+
if (!turn) return;
|
|
623
|
+
const r = model.generationRestarted(turn, data?.iteration ?? null);
|
|
624
|
+
const entry = r && genEls.get(r.gen);
|
|
625
|
+
if (!entry) return;
|
|
626
|
+
dirty.delete(r.gen);
|
|
627
|
+
if (entry.thinkingWrap) {
|
|
628
|
+
if (entry.tickerTimer) { clearTimeout(entry.tickerTimer); entry.tickerTimer = null; }
|
|
629
|
+
entry.ticker.close();
|
|
630
|
+
entry.thinkingWrap.remove();
|
|
631
|
+
}
|
|
632
|
+
Object.assign(entry, { thinking: null, thinkingWrap: null, thinkingLine: null, ticker: null, thinkingWritten: 0,
|
|
633
|
+
box: null, feed: createSentenceFeed() });
|
|
634
|
+
entry.text.textContent = "…";
|
|
635
|
+
onChange();
|
|
636
|
+
},
|
|
618
637
|
generationCompleted() {
|
|
619
638
|
if (!turn) return;
|
|
620
639
|
const r = model.generationCompleted(turn);
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -16,6 +16,7 @@ require_relative "session_manager"
|
|
|
16
16
|
require_relative "archive_store"
|
|
17
17
|
require_relative "log"
|
|
18
18
|
require_relative "turn_flow"
|
|
19
|
+
require_relative "llm/errors"
|
|
19
20
|
require_relative "continue_offer"
|
|
20
21
|
require_relative "iteration_limit"
|
|
21
22
|
require_relative "session_commands"
|
|
@@ -830,7 +831,7 @@ module Samagotchi
|
|
|
830
831
|
# failed prompt or wake turn that got nowhere, a canceled continue
|
|
831
832
|
# turn). A failed turn whose work stayed, and a reminder or continue
|
|
832
833
|
# turn that failed (kept with its failed note), read them.
|
|
833
|
-
settle_child_reports(rolled_back: @turn_flow.restores != restores)
|
|
834
|
+
settle_child_reports(rolled_back: @turn_flow.restores != restores || merges_lost?(error))
|
|
834
835
|
ring_parent_after_turn
|
|
835
836
|
end
|
|
836
837
|
|
|
@@ -843,6 +844,20 @@ module Samagotchi
|
|
|
843
844
|
rolled_back ? @child_reports.release : @child_reports.commit
|
|
844
845
|
end
|
|
845
846
|
|
|
847
|
+
# Whether +error+ ended the turn without its loop's conversation: a
|
|
848
|
+
# failure the loops didn't mark (LLM::FailedTurn), one past their end
|
|
849
|
+
# (the Engine's own end of the turn, say). The Engine kept the turn's
|
|
850
|
+
# start only, so the reports its loop merged are not in the
|
|
851
|
+
# conversation: their rings stay for the next turn, even when the turn
|
|
852
|
+
# itself stays (a reminder or continue turn). Not one raised after the
|
|
853
|
+
# turn ended (the Engine's post-turn work): the turn was kept as
|
|
854
|
+
# completed, the reports in it.
|
|
855
|
+
def merges_lost?(error)
|
|
856
|
+
return false if error.nil? || error.is_a?(LLM::FailedTurn)
|
|
857
|
+
|
|
858
|
+
@session.last_turn&.dig("outcome") != "completed"
|
|
859
|
+
end
|
|
860
|
+
|
|
846
861
|
# A delegate child's turn that was its parent's (#reports_to_parent?)
|
|
847
862
|
# rings the parent, after the save: the parent reads a settled child.
|
|
848
863
|
# A question it rang about that still waits makes the next turn ring
|
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.46.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -136,6 +136,7 @@ files:
|
|
|
136
136
|
- docs/internals/gemma4-contract.md
|
|
137
137
|
- docs/internals/llm-context-bench.md
|
|
138
138
|
- docs/internals/llm-context-forget.md
|
|
139
|
+
- docs/internals/llm-context-live.md
|
|
139
140
|
- docs/internals/prompt-caching.md
|
|
140
141
|
- docs/internals/tool-guardrails.md
|
|
141
142
|
- docs/memory.md
|
|
@@ -285,6 +286,7 @@ files:
|
|
|
285
286
|
- lib/samagotchi/hooks/loader.rb
|
|
286
287
|
- lib/samagotchi/hooks/registry.rb
|
|
287
288
|
- lib/samagotchi/hooks/stream_watch.rb
|
|
289
|
+
- lib/samagotchi/host_model.rb
|
|
288
290
|
- lib/samagotchi/host_registry.rb
|
|
289
291
|
- lib/samagotchi/idle_client.rb
|
|
290
292
|
- lib/samagotchi/idle_recap.rb
|
|
@@ -348,6 +350,7 @@ files:
|
|
|
348
350
|
- lib/samagotchi/model_match.rb
|
|
349
351
|
- lib/samagotchi/model_notes.rb
|
|
350
352
|
- lib/samagotchi/model_overlay.rb
|
|
353
|
+
- lib/samagotchi/model_price.rb
|
|
351
354
|
- lib/samagotchi/model_profile.rb
|
|
352
355
|
- lib/samagotchi/model_ref.rb
|
|
353
356
|
- lib/samagotchi/models_command.rb
|