samagotchi 0.44.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 +120 -1
- data/docs/architecture.md +2 -1
- data/docs/cli.md +43 -12
- data/docs/configuration.md +102 -5
- data/docs/context.md +2 -1
- 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/plugins.md +4 -2
- data/docs/sessions.md +9 -5
- data/lib/samagotchi/bridge/turn_accumulator.rb +19 -0
- data/lib/samagotchi/bridge.rb +22 -10
- data/lib/samagotchi/bundles/github-pr/manifest.yml +3 -3
- data/lib/samagotchi/bundles/github-pr/plugin.rb +22 -4
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/self_map.md +2 -1
- data/lib/samagotchi/commands/registry.rb +54 -9
- data/lib/samagotchi/config.rb +57 -4
- data/lib/samagotchi/desktop/macos/Panel.swift +7 -3
- data/lib/samagotchi/engine.rb +47 -9
- data/lib/samagotchi/host_model.rb +116 -0
- data/lib/samagotchi/host_registry.rb +43 -12
- 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/llm_context_strategy.rb +37 -0
- data/lib/samagotchi/model_catalog.rb +17 -12
- data/lib/samagotchi/model_notes.rb +8 -6
- data/lib/samagotchi/model_overlay.rb +3 -2
- data/lib/samagotchi/model_price.rb +54 -0
- data/lib/samagotchi/model_profile.rb +74 -3
- data/lib/samagotchi/plugin/context.rb +19 -7
- data/lib/samagotchi/process_group.rb +56 -9
- data/lib/samagotchi/recap_setup.rb +1 -1
- data/lib/samagotchi/self_report.rb +33 -3
- data/lib/samagotchi/session_commands.rb +47 -21
- data/lib/samagotchi/session_manager.rb +19 -1
- data/lib/samagotchi/session_metrics.rb +79 -11
- data/lib/samagotchi/sessions_command.rb +15 -5
- data/lib/samagotchi/terminal_ui/attached_loop.rb +85 -3
- data/lib/samagotchi/terminal_ui/attached_view.rb +2 -0
- data/lib/samagotchi/terminal_ui/formatting.rb +45 -10
- data/lib/samagotchi/terminal_ui/status_row.rb +12 -6
- data/lib/samagotchi/terminal_ui.rb +97 -12
- 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 +12 -11
- data/lib/samagotchi/web/public/app.js +30 -18
- data/lib/samagotchi/web/public/command_complete.js +3 -1
- data/lib/samagotchi/web/public/ctx.js +62 -22
- data/lib/samagotchi/web/public/format.js +7 -2
- data/lib/samagotchi/web/public/index.html +3 -1
- data/lib/samagotchi/web/public/llm_context_chip.js +2 -4
- 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_model.js +18 -4
- data/lib/samagotchi/web/public/stage_view.js +4 -3
- data/lib/samagotchi/web/public/turn_events.js +19 -6
- data/lib/samagotchi/web/public/turn_model.js +16 -0
- data/lib/samagotchi/web/public/turn_view.js +19 -0
- data/lib/samagotchi/web/session_hub.rb +9 -1
- data/lib/samagotchi/web/session_summary.rb +19 -2
- data/lib/samagotchi/worker.rb +172 -21
- data/lib/samagotchi/worker_inbound.rb +6 -5
- metadata +4 -1
|
@@ -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
|
}
|
|
@@ -302,6 +302,8 @@ button.task-stop:hover:not(:disabled){background:var(--danger-bg-dim);filter:non
|
|
|
302
302
|
.note-text{white-space:pre-wrap;max-height:8em;overflow:auto}
|
|
303
303
|
.bubble.command.busy{border-left-color:var(--warn)}
|
|
304
304
|
.bubble.command.failed{border-left-color:var(--danger)}
|
|
305
|
+
.bubble.command.queued{border-left-style:dashed}
|
|
306
|
+
.bubble.command.queued .command-output{color:var(--text-faint)}
|
|
305
307
|
.command-line{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--text-3)}
|
|
306
308
|
.command-output{margin:6px 0 0;white-space:pre-wrap;font-size:12px;color:var(--text-2)}
|
|
307
309
|
.bubble.continue{border-left:3px solid var(--accent);background:var(--bg-panel);padding:10px 12px}
|
|
@@ -612,7 +614,7 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
612
614
|
.ts-trail-item:is(.error,.blocked){color:var(--danger-text)}
|
|
613
615
|
.ts-trail-item.warn{color:var(--warn-text)}
|
|
614
616
|
.ts-trail-item.stopped{color:var(--warn-text);opacity:.8}
|
|
615
|
-
.ts-trail-item.info,.ts-trail-item.steer{color:var(--accent)}
|
|
617
|
+
.ts-trail-item.info,.ts-trail-item.steer,.ts-trail-item.edit{color:var(--accent)}
|
|
616
618
|
.ts-trail-item.none{font-style:italic}
|
|
617
619
|
/* A card waits: the tool row and trail give it their room. */
|
|
618
620
|
.turn-stage.waiting .ts-now,.turn-stage.waiting .ts-trail{display:none}
|
|
@@ -179,9 +179,9 @@ export function formHtml(summary, escapeHtml, { running = false, kind = SESSION,
|
|
|
179
179
|
`<label class="llmctx-field"><span>Budget</span><input name="budget" type="text" inputmode="numeric" autocomplete="off" ` +
|
|
180
180
|
`placeholder="follow the model (64000, 64k or off)" value="${escapeHtml(own.budget)}"></label>` +
|
|
181
181
|
`<div class="ctx-pop-error llmctx-error" hidden></div>` +
|
|
182
|
-
`<div class="ctx-pop-meta">${when}${running ? " A turn is running:
|
|
182
|
+
`<div class="ctx-pop-meta">${when}${running ? " A turn is running: it applies after this turn." : ""}</div>` +
|
|
183
183
|
`<div class="ctx-pop-actions"><button type="button" class="ctx-pop-detach" data-act="reset">follow the model</button>` +
|
|
184
|
-
`<button type="submit" class="llmctx-set"
|
|
184
|
+
`<button type="submit" class="llmctx-set">Set</button></div></form>`;
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
const EDGE = 16;
|
|
@@ -263,8 +263,6 @@ export function createLlmContextControl({ el, escapeHtml, summary, session = ()
|
|
|
263
263
|
if (!data || session() !== openFor) return close();
|
|
264
264
|
const now = pop.querySelector(".llmctx-now");
|
|
265
265
|
if (now) now.textContent = nowText(data);
|
|
266
|
-
const set = pop.querySelector(".llmctx-set");
|
|
267
|
-
if (set) set.disabled = running();
|
|
268
266
|
}
|
|
269
267
|
|
|
270
268
|
el.addEventListener("click", (e) => {
|
|
@@ -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
|
}
|
|
@@ -198,15 +198,29 @@ export function handOffOrder({ prompt = null, block = null, extras = [], answer
|
|
|
198
198
|
// history (it is idle-only).
|
|
199
199
|
// What flashes in the trail for a few seconds while the turn runs (the
|
|
200
200
|
// rows themselves go into their step inside the closed cloud): a hook's
|
|
201
|
-
// notice, warn or info, as its row reads,
|
|
202
|
-
//
|
|
203
|
-
//
|
|
201
|
+
// notice, warn or info, as its row reads, the ✂ row of an applied LLM
|
|
202
|
+
// context edit (kind "edit"), a steer-cut row (a message cut in on a
|
|
203
|
+
// thinking-only generation; kind "steer"), and a plugin's nudge (steer).
|
|
204
|
+
// The row's leading mark ("✂ ", "↪ ") goes: FLASH_MARKS's stands in.
|
|
205
|
+
// chi's own asking-again row (a notice with a line and no hook or cls)
|
|
206
|
+
// does not: the status line tells that already. null: nothing flashes.
|
|
207
|
+
// A hookless notice's cls names its flash: [cls, trail kind, the leading
|
|
208
|
+
// mark the line loses].
|
|
209
|
+
const HOOKLESS_FLASH = [
|
|
210
|
+
["llm-context", "edit", /^✂ /],
|
|
211
|
+
["steer-cut", "steer", /^↪ /],
|
|
212
|
+
];
|
|
204
213
|
export function flashOf(what, data = {}) {
|
|
205
214
|
if (what === "steer") {
|
|
206
215
|
const sender = steerSender(data.source);
|
|
207
216
|
return { kind: "steer", text: sender ? `${sender} nudged the model` : "nudged the model" };
|
|
208
217
|
}
|
|
209
|
-
if (what !== "notice"
|
|
218
|
+
if (what !== "notice") return null;
|
|
219
|
+
if (!data.hook) {
|
|
220
|
+
const flash = HOOKLESS_FLASH.find(([cls]) => cls === data.cls);
|
|
221
|
+
if (!flash || !data.line) return null;
|
|
222
|
+
return { kind: flash[1], text: data.line.replace(flash[2], "") };
|
|
223
|
+
}
|
|
210
224
|
return { kind: data.level === "warn" ? "warn" : "info", text: data.line || `${hookNoticeLabel(data.hook)}: ${data.text || ""}` };
|
|
211
225
|
}
|
|
212
226
|
|
|
@@ -28,9 +28,9 @@ const PENDING = ".bubble.question:not(.answered):not(.cancelled), .bubble.contin
|
|
|
28
28
|
// and never holds the hand-off (a turn cut short can leave it open).
|
|
29
29
|
const ASKS = ".plugin-card[data-in-turn] .card-action";
|
|
30
30
|
const MARKS = { answered: "✓", "no answer": "∅", canceled: "■", failed: "✕" };
|
|
31
|
-
// The mark of a flash in the trail: a warn notice, a hook's info notice,
|
|
32
|
-
// plugin's nudge (
|
|
33
|
-
const FLASH_MARKS = { warn: "⚠", info: "ℹ", steer: "↪" };
|
|
31
|
+
// The mark of a flash in the trail: a warn notice, a hook's info notice,
|
|
32
|
+
// an applied LLM context edit (✂), a steer-cut row and a plugin's nudge (↪).
|
|
33
|
+
const FLASH_MARKS = { warn: "⚠", info: "ℹ", edit: "✂", steer: "↪" };
|
|
34
34
|
|
|
35
35
|
function node(tag, cls, parent = null) {
|
|
36
36
|
const el = document.createElement(tag);
|
|
@@ -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}` : ""}` : "";
|
|
@@ -339,18 +340,26 @@ export function continueLine(choice, reason = "") {
|
|
|
339
340
|
// tab's own), `resync` says the conversation changed (!rollback, !cmd, a
|
|
340
341
|
// continue answered no) and must be read again. An anytime command's
|
|
341
342
|
// (`anytime`, D8) bubble is drawn at its command_queued, before the cards
|
|
342
|
-
// it shows; its command_ran fills that bubble in (`commandId`).
|
|
343
|
+
// it shows; its command_ran fills that bubble in (`commandId`). So is a
|
|
344
|
+
// command queued for after the running turn (`queued`): its command_queued
|
|
345
|
+
// (waits: "turn_end") draws it `waiting`, saying so, until it ran or was
|
|
346
|
+
// dropped (styled as failed).
|
|
347
|
+
export const QUEUED_TEXT = "queued: runs after this turn";
|
|
348
|
+
|
|
343
349
|
export function commandView(event, myId) {
|
|
344
350
|
const own = isOwn(event.client_id, myId);
|
|
351
|
+
const waiting = event.waits === "turn_end";
|
|
345
352
|
return {
|
|
346
353
|
label: own ? null : clientLabel(event.client_id),
|
|
347
354
|
line: event.line || "",
|
|
348
|
-
text: event.output || "",
|
|
355
|
+
text: event.output || (waiting ? QUEUED_TEXT : ""),
|
|
349
356
|
busy: event.status === "busy",
|
|
350
|
-
failed: event.status === "error",
|
|
357
|
+
failed: event.status === "error" || event.status === "dropped",
|
|
351
358
|
resync: (event.changed || []).includes("messages"),
|
|
352
359
|
modelName: event.model_name || null,
|
|
353
360
|
anytime: event.anytime === true,
|
|
361
|
+
queued: waiting || event.queued === true,
|
|
362
|
+
waiting,
|
|
354
363
|
commandId: event.command_id || null,
|
|
355
364
|
// A card's action (Nudge…): no bubble unless it says something back.
|
|
356
365
|
hidden: event.card === true && !event.output && event.status !== "error",
|
|
@@ -498,7 +507,7 @@ export function replaysDrawnTurn(event, turnRecords = []) {
|
|
|
498
507
|
// A `notice` part carries its event as it came (TurnNotice). The TUI replays
|
|
499
508
|
// a turn the same way (TurnAccumulator.replay_events); spec/shared/
|
|
500
509
|
// turn_snapshot.json pins both.
|
|
501
|
-
export function snapshotEvents({ current_turn: turn = null, queued = [], started_at = undefined } = {}) {
|
|
510
|
+
export function snapshotEvents({ current_turn: turn = null, queued = [], queued_commands: commands = [], started_at = undefined } = {}) {
|
|
502
511
|
const events = [];
|
|
503
512
|
if (turn) {
|
|
504
513
|
events.push(withImages({ type: "turn_started", prompt: turn.prompt, origin: turn.origin, continue: !!turn.continue, started_at }, turn.images));
|
|
@@ -576,6 +585,10 @@ export function snapshotEvents({ current_turn: turn = null, queued = [], started
|
|
|
576
585
|
for (const entry of queued || []) {
|
|
577
586
|
events.push(withImages({ type: "turn_enqueued", enqueued_id: entry.enqueued_id, client_id: entry.client_id, prompt: entry.prompt }, entry.images));
|
|
578
587
|
}
|
|
588
|
+
// Commands waiting for the turn's end: their bubbles, as live.
|
|
589
|
+
for (const entry of commands || []) {
|
|
590
|
+
events.push({ type: "command_queued", command_id: entry.command_id, client_id: entry.client_id, line: entry.line, waits: "turn_end" });
|
|
591
|
+
}
|
|
579
592
|
return events;
|
|
580
593
|
}
|
|
581
594
|
|
|
@@ -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);
|
|
@@ -297,7 +297,7 @@ module Samagotchi
|
|
|
297
297
|
|
|
298
298
|
entry.owner = owner
|
|
299
299
|
entry.live = live
|
|
300
|
-
summary = SessionSummary.build(entry.session, owner: owner, session_dir: dir)
|
|
300
|
+
summary = SessionSummary.build(entry.session, owner: owner, session_dir: dir, registry: hub_registry)
|
|
301
301
|
# A new worker (pid) or a new Bridge for the same session is an
|
|
302
302
|
# event for the page even when the summary reads the same: it
|
|
303
303
|
# reconnects its stream on it.
|
|
@@ -307,6 +307,14 @@ module Samagotchi
|
|
|
307
307
|
emit("session", session: summary)
|
|
308
308
|
end
|
|
309
309
|
|
|
310
|
+
# One HostRegistry for the hub's lifetime: every session's ctx_pct may
|
|
311
|
+
# need its llm_context budget resolved, and the registry's config read
|
|
312
|
+
# is what would otherwise repeat per scan and per session. Built on
|
|
313
|
+
# the first summary that needs it.
|
|
314
|
+
def hub_registry
|
|
315
|
+
@hub_registry ||= HostRegistry.new
|
|
316
|
+
end
|
|
317
|
+
|
|
310
318
|
def drop(id)
|
|
311
319
|
entry = @sessions.delete(id)
|
|
312
320
|
emit("session_gone", id: id) if entry&.summary
|
|
@@ -4,6 +4,8 @@ require_relative "../session"
|
|
|
4
4
|
require_relative "../recap_store"
|
|
5
5
|
require_relative "../archive_store"
|
|
6
6
|
require_relative "../session_metrics"
|
|
7
|
+
require_relative "../llm_context_strategy"
|
|
8
|
+
require_relative "../host_registry"
|
|
7
9
|
require_relative "../bridge_client"
|
|
8
10
|
require_relative "../worker_sidecar"
|
|
9
11
|
require_relative "../bridge/pending_card"
|
|
@@ -26,12 +28,18 @@ module Samagotchi
|
|
|
26
28
|
# @param root_cache [Hash, nil] ProjectScope.root_for's cache, shared
|
|
27
29
|
# across one listing (a session saved before project_root existed
|
|
28
30
|
# looks its project up)
|
|
31
|
+
# @param registry [HostRegistry, nil] the hosts to resolve the
|
|
32
|
+
# session's llm_context budget against (its ctx_pct counts against
|
|
33
|
+
# the smaller of that budget and the window, as the live meter
|
|
34
|
+
# does). nil: one is built here; a caller that builds many summaries
|
|
35
|
+
# (the session hub) keeps one and passes it.
|
|
29
36
|
# @return [Hash] symbol keys
|
|
30
|
-
def build(session, owner:, session_dir:, status: displayed_status(session, owner: owner), root_cache: nil
|
|
37
|
+
def build(session, owner:, session_dir:, status: displayed_status(session, owner: owner), root_cache: nil,
|
|
38
|
+
registry: nil)
|
|
31
39
|
used = Array(session.used_memory_names)
|
|
32
40
|
up = bridge_up?(session_dir, owner)
|
|
33
41
|
sidecar = up ? WorkerSidecar.read(session_dir) : nil
|
|
34
|
-
saved = SessionMetrics.saved_summary(session_dir)
|
|
42
|
+
saved = SessionMetrics.saved_summary(session_dir, budget_tokens: budget_resolver(session, registry))
|
|
35
43
|
{
|
|
36
44
|
id: session.id,
|
|
37
45
|
status: status,
|
|
@@ -86,6 +94,15 @@ module Samagotchi
|
|
|
86
94
|
}
|
|
87
95
|
end
|
|
88
96
|
|
|
97
|
+
# The budget resolver SessionMetrics.saved_summary takes: a callable,
|
|
98
|
+
# so the session's llm_context budget (config + the registry's index,
|
|
99
|
+
# no network) is resolved only for a session that has a saved context
|
|
100
|
+
# to count. One registry per caller (the hub) or one built here.
|
|
101
|
+
def budget_resolver(session, registry)
|
|
102
|
+
-> { LLMContextStrategy.session_budget(session, registry: registry ||= HostRegistry.new) }
|
|
103
|
+
end
|
|
104
|
+
private_class_method :budget_resolver
|
|
105
|
+
|
|
89
106
|
# status is turn state (idle/running). The live worker's snapshot is the
|
|
90
107
|
# truth; on disk, a "running" with no live owner was left by a worker
|
|
91
108
|
# that died mid-turn.
|