samagotchi 0.15.0 → 0.17.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 +135 -1
- data/README.md +2 -0
- data/docs/architecture.md +23 -0
- data/docs/cli.md +7 -4
- data/docs/configuration.md +7 -5
- data/docs/guardrails.md +16 -2
- data/docs/internals/background-tasks.md +1 -0
- data/docs/memory.md +39 -0
- data/docs/plugins.md +2 -0
- data/docs/sessions.md +10 -4
- data/docs/sub-agent.md +16 -3
- data/docs/testing.md +47 -0
- data/lib/samagotchi/answer_display.rb +6 -2
- data/lib/samagotchi/bridge/card_store.rb +16 -1
- data/lib/samagotchi/bridge/turn_accumulator.rb +26 -5
- data/lib/samagotchi/bridge.rb +122 -7
- data/lib/samagotchi/bridge_client.rb +33 -6
- data/lib/samagotchi/bundle_command.rb +13 -2
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/memory_guide.md +5 -5
- data/lib/samagotchi/bundles/system/self_map.md +3 -1
- data/lib/samagotchi/empty_answer_retry.rb +2 -1
- data/lib/samagotchi/engine.rb +82 -18
- data/lib/samagotchi/kernel_loop.rb +16 -5
- data/lib/samagotchi/llm/chat_loop.rb +14 -8
- data/lib/samagotchi/llm/model_result.rb +12 -7
- data/lib/samagotchi/log_subscriber.rb +1 -0
- data/lib/samagotchi/memory_bundle/builder.rb +22 -0
- data/lib/samagotchi/memory_bundle/index_updater.rb +17 -6
- data/lib/samagotchi/memory_bundle/installer.rb +60 -21
- data/lib/samagotchi/memory_bundle/profile.rb +9 -4
- data/lib/samagotchi/memory_bundle/provenance.rb +14 -0
- data/lib/samagotchi/memory_bundle/shipped_update.rb +5 -3
- data/lib/samagotchi/memory_bundle/system_bundle.rb +5 -3
- data/lib/samagotchi/memory_bundle/trash.rb +57 -0
- data/lib/samagotchi/memory_bundle/uninstaller.rb +34 -5
- data/lib/samagotchi/parent_report.rb +8 -0
- data/lib/samagotchi/plugin/context.rb +14 -2
- data/lib/samagotchi/question_desk.rb +97 -18
- data/lib/samagotchi/relay.rb +88 -0
- data/lib/samagotchi/relay_desk.rb +73 -0
- data/lib/samagotchi/relay_verifier.rb +113 -0
- data/lib/samagotchi/relay_watcher.rb +46 -0
- data/lib/samagotchi/reply_wait.rb +10 -1
- data/lib/samagotchi/self_report.rb +49 -3
- data/lib/samagotchi/send_command.rb +45 -6
- data/lib/samagotchi/session.rb +15 -2
- data/lib/samagotchi/session_manager.rb +3 -2
- data/lib/samagotchi/session_metrics.rb +154 -48
- data/lib/samagotchi/sessions_command.rb +10 -4
- data/lib/samagotchi/system_prompt.rb +45 -4
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +11 -2
- data/lib/samagotchi/terminal_ui/attached_loop.rb +85 -21
- data/lib/samagotchi/terminal_ui/event_renderer.rb +8 -2
- data/lib/samagotchi/terminal_ui/formatting.rb +7 -0
- data/lib/samagotchi/terminal_ui/question_prompt.rb +48 -2
- data/lib/samagotchi/terminal_ui/question_slot.rb +5 -1
- data/lib/samagotchi/terminal_ui.rb +24 -0
- data/lib/samagotchi/tool_declarations.rb +2 -2
- data/lib/samagotchi/tools/builtins.rb +11 -3
- data/lib/samagotchi/tools/delegate_relay.rb +220 -0
- data/lib/samagotchi/tools/delegate_wait.rb +72 -6
- data/lib/samagotchi/tools/peers.rb +7 -4
- data/lib/samagotchi/tools/task_runtime.rb +1 -1
- data/lib/samagotchi/turn_note.rb +30 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +22 -1
- data/lib/samagotchi/web/public/app.js +60 -33
- data/lib/samagotchi/web/public/index.html +7 -0
- data/lib/samagotchi/web/public/notify.js +22 -5
- data/lib/samagotchi/web/public/question_card.js +45 -3
- data/lib/samagotchi/web/public/question_cards.js +49 -1
- data/lib/samagotchi/web/public/sessions_list.js +8 -1
- data/lib/samagotchi/web/public/stage_model.js +8 -3
- data/lib/samagotchi/web/public/stage_view.js +20 -3
- data/lib/samagotchi/web/public/strip.js +11 -0
- data/lib/samagotchi/web/public/timing.js +37 -15
- data/lib/samagotchi/web/public/turn_events.js +22 -3
- data/lib/samagotchi/web/public/turn_view.js +34 -12
- data/lib/samagotchi/worker.rb +28 -2
- data/lib/samagotchi/worker_sidecar.rb +4 -1
- metadata +8 -1
|
@@ -88,7 +88,9 @@ export function timedTurnIndexes(items) {
|
|
|
88
88
|
// the last, in order; a text-less iteration leaves no message, so a text
|
|
89
89
|
// can sit one step early: inspection, not truth) and its answer (the last
|
|
90
90
|
// assistant message). Assistant messages before any prompt are answers on
|
|
91
|
-
// their own.
|
|
91
|
+
// their own. A turn that ended with no answer (the server's empty_answer
|
|
92
|
+
// item) has no answer: its texts are steps, and `emptyAnswer` is that
|
|
93
|
+
// item's index. The turn index counts prompts, as timedTurnIndexes does. The
|
|
92
94
|
// user's lines merged into the running turn (`merged`, the server's
|
|
93
95
|
// Steer::INPUT_KIND) stay in it: the turn's `merged` list.
|
|
94
96
|
export function turnGroups(items, timing) {
|
|
@@ -110,13 +112,18 @@ export function turnGroups(items, timing) {
|
|
|
110
112
|
} else if (item?.role === "assistant") {
|
|
111
113
|
if (turn) turn.texts.push(i);
|
|
112
114
|
else groups.push({ kind: "answer", answer: i });
|
|
115
|
+
} else if (item?.role === "empty_answer" && turn) {
|
|
116
|
+
turn.emptyAnswer = i;
|
|
113
117
|
}
|
|
114
118
|
});
|
|
115
119
|
return groups.map((g) => {
|
|
116
120
|
if (g.kind !== "turn") return g;
|
|
117
121
|
const group = withSteers(turnGroup(g, items, timing), g.steers, items);
|
|
122
|
+
// A turn that ended with no answer: its notice (the item's index) in
|
|
123
|
+
// the answer's place.
|
|
124
|
+
const ended = g.emptyAnswer == null ? group : { ...group, emptyAnswer: g.emptyAnswer };
|
|
118
125
|
// A turn with no merged lines keeps its shape.
|
|
119
|
-
return g.merged.length ? { ...
|
|
126
|
+
return g.merged.length ? { ...ended, merged: g.merged } : ended;
|
|
120
127
|
});
|
|
121
128
|
}
|
|
122
129
|
|
|
@@ -150,7 +157,8 @@ function promptRecord(timing, g) {
|
|
|
150
157
|
function turnGroup(g, items, timing) {
|
|
151
158
|
const record = promptRecord(timing, g);
|
|
152
159
|
const tools = record ? (timing?.toolRecords || []).filter((r) => r.turn_id === record.id) : [];
|
|
153
|
-
|
|
160
|
+
// A turn with no answer has none of its texts as one: they are steps.
|
|
161
|
+
const stepsOnly = canceledSteps(g, record, tools) || g.emptyAnswer != null;
|
|
154
162
|
if (g.texts.some((i) => items[i].parts)) return partsTurn(g, items, record, tools, stepsOnly);
|
|
155
163
|
const maxIteration = tools.reduce((m, r) => Math.max(m, Number(r.iteration) || 0), 0);
|
|
156
164
|
const stepTexts = stepsOnly ? g.texts : g.texts.slice(0, -1);
|
|
@@ -177,37 +185,51 @@ function recordRows(tools, iteration) {
|
|
|
177
185
|
}
|
|
178
186
|
|
|
179
187
|
// A turn whose saved messages carry their parts (the server's ?parts=1):
|
|
180
|
-
// every assistant message is one
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
// it
|
|
188
|
+
// every assistant message is one step. Its calls take the turn's records by
|
|
189
|
+
// tool, in call order (each the next record of its tool, consumed with the
|
|
190
|
+
// ones it skipped), not by iteration: an empty retry spends an iteration
|
|
191
|
+
// that left no message (as the TUI's join, attached_loop.rb join_duration).
|
|
192
|
+
// The last message is the answer when it has text and made no call (and the
|
|
193
|
+
// turn is no canceled multi-step one); its thinking stays behind as a step,
|
|
194
|
+
// as it does live.
|
|
185
195
|
function partsTurn(g, items, record, tools, stepsOnly = false) {
|
|
186
196
|
const last = g.texts[g.texts.length - 1];
|
|
187
197
|
const lastItem = items[last];
|
|
188
198
|
const answer = !stepsOnly && String(lastItem.content ?? "") !== "" && !lastItem.parts?.tools?.length ? last : null;
|
|
199
|
+
const pool = [...tools].sort((a, b) =>
|
|
200
|
+
(Number(a.iteration) || 0) - (Number(b.iteration) || 0) || (Number(a.call_index) || 0) - (Number(b.call_index) || 0));
|
|
189
201
|
const steps = g.texts.map((i, k) => {
|
|
190
202
|
const iteration = k + 1;
|
|
191
203
|
const parts = items[i].parts || {};
|
|
192
|
-
const records = recordRows(tools, iteration);
|
|
193
204
|
const rows = Array.isArray(parts.tools)
|
|
194
205
|
? parts.tools.map((call, j) => {
|
|
195
|
-
const r =
|
|
206
|
+
const r = takeRecord(pool, call.tool);
|
|
196
207
|
return { key: `${iteration}:${j + 1}`, status: r?.status || "ok", duration_ms: r?.duration_ms, ...call };
|
|
197
208
|
})
|
|
198
|
-
:
|
|
209
|
+
: [];
|
|
199
210
|
const thinking = String(parts.thinking || "");
|
|
200
211
|
if (i === answer) return thinking ? { i: null, iteration, thinking, tools: [] } : null;
|
|
201
212
|
return { i, iteration, thinking, tools: rows };
|
|
202
213
|
}).filter(Boolean);
|
|
203
|
-
// Records
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
214
|
+
// Records no saved message took (a generation that saved nothing): a
|
|
215
|
+
// step per iteration of theirs, after the messages'.
|
|
216
|
+
let iteration = g.texts.length;
|
|
217
|
+
for (const n of [...new Set(pool.map((r) => Number(r.iteration) || 0))]) {
|
|
218
|
+
iteration += 1;
|
|
219
|
+
const rows = recordRows(pool, n).map((row) => ({ ...row, key: `${iteration}:${row.key.split(":")[1]}` }));
|
|
220
|
+
steps.push({ i: null, iteration, thinking: "", tools: rows });
|
|
207
221
|
}
|
|
208
222
|
return { kind: "turn", turnIndex: g.turnIndex, user: g.user, answer, record, steps };
|
|
209
223
|
}
|
|
210
224
|
|
|
225
|
+
// The next record of +tool+ in +pool+ (call order), removed with the ones
|
|
226
|
+
// before it; undefined when there is none (the pool stays as it was).
|
|
227
|
+
function takeRecord(pool, tool) {
|
|
228
|
+
const at = pool.findIndex((r) => String(r.tool) === String(tool));
|
|
229
|
+
if (at < 0) return undefined;
|
|
230
|
+
return pool.splice(0, at + 1)[at];
|
|
231
|
+
}
|
|
232
|
+
|
|
211
233
|
// Append +el+ to the history, above +timingEl+ while it is a running turn's
|
|
212
234
|
// live timing line, so that line stays under the turn's activity, thinking
|
|
213
235
|
// and answer.
|
|
@@ -64,6 +64,15 @@ export function emptyRetryLine({ attempt, of, stopped_by: stoppedBy } = {}) {
|
|
|
64
64
|
return `↻ ${what}, asking again (${attempt}/${of})`;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// The one muted line for a turn that ended with no answer (turn_completed's
|
|
68
|
+
// turn_summary.empty_answer, or a reloaded turn's marker): the TUI words
|
|
69
|
+
// it the same (TurnNote.empty_answer_line; spec/shared/labels_matrix.json).
|
|
70
|
+
export function emptyAnswerLine({ retries = 0 } = {}) {
|
|
71
|
+
const count = Number(retries) || 0;
|
|
72
|
+
const after = count > 0 ? ` (after ${count} ${count === 1 ? "retry" : "retries"})` : "";
|
|
73
|
+
return `no answer: the model returned nothing${after}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
67
76
|
// The text of a turn's row from a snapshot entry (CardStore): a hook's
|
|
68
77
|
// notice as "<label>: text", the loop's asking-again row as its line.
|
|
69
78
|
export function noticeLine(entry = {}) {
|
|
@@ -88,9 +97,19 @@ export function initWaitLine({ tasks } = {}) {
|
|
|
88
97
|
return `waiting for ${labels.length ? labels.join(" · ") : "plugins"}…`;
|
|
89
98
|
}
|
|
90
99
|
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
100
|
+
// Whether a composer line in an open session is one of its commands
|
|
101
|
+
// (+commands+: the session GET's listing), as the terminals decide
|
|
102
|
+
// (Commands::Registry#lookup): a "!" line with something after the "!", or
|
|
103
|
+
// a listed name alone or followed by a space; a terminal's own (/stats,
|
|
104
|
+
// /exit) too, which the page answers itself (webLocalReply). An unknown
|
|
105
|
+
// "/word" (a path, a typo) goes to the model as a prompt, as in a terminal.
|
|
106
|
+
export function sessionCommandLine(text, commands) {
|
|
107
|
+
const line = String(text || "").trim();
|
|
108
|
+
if (/^!\s*\S/.test(line)) return true;
|
|
109
|
+
if (!line.startsWith("/")) return false;
|
|
110
|
+
if (webLocalReply(line)) return true;
|
|
111
|
+
return (Array.isArray(commands) ? commands : [])
|
|
112
|
+
.some((c) => c && typeof c.name === "string" && (line === c.name || line.startsWith(`${c.name} `)));
|
|
94
113
|
}
|
|
95
114
|
|
|
96
115
|
// The terminal's own commands (a button in the web, or none yet): the
|
|
@@ -16,7 +16,7 @@ import { activityDiffHtml } from "./diff_view.js";
|
|
|
16
16
|
import { escapeHtml, messageBodyHtml, noteHtml, normalize, steerRowHtml } from "./format.js";
|
|
17
17
|
import { thumbsHtml } from "./images.js";
|
|
18
18
|
import { shouldFollowScroll } from "./scroll.js";
|
|
19
|
-
import { hookNoticeLabel } from "./turn_events.js";
|
|
19
|
+
import { emptyAnswerLine, hookNoticeLabel } from "./turn_events.js";
|
|
20
20
|
import { cancelLineHtml, formatDuration, turnGroups, turnTimingText } from "./timing.js";
|
|
21
21
|
import { createSentenceFeed } from "./sentences.js";
|
|
22
22
|
import { createTicker } from "./thinking_ticker.js";
|
|
@@ -465,11 +465,15 @@ export function createTurnView({
|
|
|
465
465
|
// +heldHeight+ (the live box's height, measured before the step closed) the
|
|
466
466
|
// bubble is held at that height, scrolled to its tail, until answerReady()
|
|
467
467
|
// (the rendered markdown landed) or 1.5 s reveals it.
|
|
468
|
-
|
|
468
|
+
// +endNote+: the notice of a turn that ended with no answer (app.js
|
|
469
|
+
// builds it), placed where the answer bubble would go.
|
|
470
|
+
function promote(gen, { heldHeight = 0, endNote = null } = {}) {
|
|
469
471
|
const entry = genEls.get(gen);
|
|
470
472
|
const bubble = entry.text;
|
|
471
|
-
|
|
472
|
-
|
|
473
|
+
// No text (an empty answer): no bubble, not even an empty one left in
|
|
474
|
+
// the step.
|
|
475
|
+
if (gen.text) bubble.className = "bubble output";
|
|
476
|
+
else bubble.remove();
|
|
473
477
|
// A hidden tab gets no pop: nobody sees it, and its timers are throttled.
|
|
474
478
|
const holding = useHold && heldHeight > 0 && !!gen.text && !document.hidden;
|
|
475
479
|
let holdHeight = 0;
|
|
@@ -512,11 +516,12 @@ export function createTurnView({
|
|
|
512
516
|
}
|
|
513
517
|
pieces.push(...cards);
|
|
514
518
|
if (gen.text) pieces.push(bubble);
|
|
519
|
+
else if (endNote) pieces.push(endNote);
|
|
515
520
|
placeAnswer(pieces, { plain: true, blockEl });
|
|
516
521
|
blockEl = null;
|
|
517
522
|
blockSummaryEl = null;
|
|
518
523
|
} else {
|
|
519
|
-
placeAnswer(gen.text ? [...cards, bubble] : cards, { plain: false, blockEl });
|
|
524
|
+
placeAnswer(gen.text ? [...cards, bubble] : [...cards, ...(endNote ? [endNote] : [])], { plain: false, blockEl });
|
|
520
525
|
// The last step, left with its thinking only, reads "thinking" once.
|
|
521
526
|
if (!gen.tools.length && gen.thinking) unwrapThinking(entry);
|
|
522
527
|
}
|
|
@@ -524,8 +529,10 @@ export function createTurnView({
|
|
|
524
529
|
}
|
|
525
530
|
|
|
526
531
|
// Returns the cards that left the block (card.js leavesBlock), detached:
|
|
527
|
-
// app.js puts them after the turn's end line.
|
|
528
|
-
|
|
532
|
+
// app.js puts them after the turn's end line. +endNote+ (a completed turn
|
|
533
|
+
// with no answer): its notice, in the answer's place; left unplaced
|
|
534
|
+
// with no turn to end (app.js then shows it itself).
|
|
535
|
+
function turnEnded({ kind, endNote = null }) {
|
|
529
536
|
// Once per turn: a second end (a re-dispatched event) would promote again.
|
|
530
537
|
if (!turn || turn.ended) return [];
|
|
531
538
|
const last = model.currentGen(turn);
|
|
@@ -541,7 +548,8 @@ export function createTurnView({
|
|
|
541
548
|
const heldHeight = box ? box.getBoundingClientRect().height : 0;
|
|
542
549
|
if (pendingSteers.length) flushSteers();
|
|
543
550
|
apply(model.turnEnded(turn));
|
|
544
|
-
if (promoting) promote(last, { heldHeight });
|
|
551
|
+
if (promoting) promote(last, { heldHeight, endNote });
|
|
552
|
+
else if (endNote) placeAnswer([endNote], { plain: false, blockEl });
|
|
545
553
|
const kept = [];
|
|
546
554
|
if (blockEl) {
|
|
547
555
|
updateBlockSummary();
|
|
@@ -678,6 +686,13 @@ function reloadRowHtml(r, thumbs) {
|
|
|
678
686
|
`${r.images?.length ? thumbs(r.images) : ""}${r.diff ? activityDiffHtml(r.diff) : ""}</div>`;
|
|
679
687
|
}
|
|
680
688
|
|
|
689
|
+
// The notice of a turn that ended with no answer: a muted row like the
|
|
690
|
+
// loop's retry row, where the answer would be (live: promote()).
|
|
691
|
+
export const EMPTY_ANSWER_CLASS = "bubble hook-notice empty-answer";
|
|
692
|
+
function emptyAnswerHtml(item) {
|
|
693
|
+
return `<div class="${EMPTY_ANSWER_CLASS}">${escapeHtml(emptyAnswerLine(item))}</div>`;
|
|
694
|
+
}
|
|
695
|
+
|
|
681
696
|
// A reloaded history in the turn view (renderHistory's object path): per
|
|
682
697
|
// turn the prompt, a collapsed block of its steps when it had any (texts,
|
|
683
698
|
// and with the server's parts the thinking, params and output, from the
|
|
@@ -708,9 +723,13 @@ export function turnHistoryHtml(items, timing, { thumbs }) {
|
|
|
708
723
|
const prompt = items[group.user];
|
|
709
724
|
const parts = [`<div class="bubble user"${copySourceAttr(prompt, false)}><div class="user-message">${messageBodyHtml(prompt).body}${thumbs(prompt.images)}</div></div>`];
|
|
710
725
|
const [only] = group.steps;
|
|
711
|
-
const
|
|
712
|
-
|
|
713
|
-
|
|
726
|
+
const ended = group.answer !== null || group.emptyAnswer != null;
|
|
727
|
+
const textless = only && (only.i === null || !String(items[only.i].content ?? ""));
|
|
728
|
+
// A lone thinking step before an answer (or the notice of a turn with
|
|
729
|
+
// none): no block, as live.
|
|
730
|
+
const plainThinking = group.steps.length === 1 && ended && textless && !only.tools.length && only.thinking;
|
|
731
|
+
const noBlock = !group.steps.length || plainThinking;
|
|
732
|
+
if (plainThinking) {
|
|
714
733
|
// A plain answer with thinking: the collapsed thinking block, as live.
|
|
715
734
|
parts.push(`<details class="bubble thinking"><summary>thinking</summary><div class="thinking-body">${escapeHtml(only.thinking)}</div></details>`);
|
|
716
735
|
} else if (group.steps.length) {
|
|
@@ -736,9 +755,12 @@ export function turnHistoryHtml(items, timing, { thumbs }) {
|
|
|
736
755
|
// after its steps and before its answer.
|
|
737
756
|
parts.push((group.merged || []).map((i) =>
|
|
738
757
|
`<div class="bubble user steered" data-user-state="steered"${items[i].step ? ` data-step="${Number(items[i].step)}"` : ""}${copySourceAttr(items[i], false)}><div class="user-message">${messageBodyHtml(items[i]).body}${thumbs(items[i].images)}</div><span class="state-badge">steered</span></div>`).join(""));
|
|
758
|
+
// A turn that ended with no answer: chi's muted notice where the answer
|
|
759
|
+
// would be, before the timing, as live.
|
|
760
|
+
if (group.emptyAnswer != null) parts.push(`${emptyAnswerHtml(items[group.emptyAnswer])}${timingHtml(group)}`);
|
|
739
761
|
// A turn with no answer (canceled, or running: no record yet) shows its
|
|
740
762
|
// timing after its prompt and steps, where the live view leaves it.
|
|
741
|
-
if (group.answer === null) parts.push(timingHtml(group));
|
|
763
|
+
else if (group.answer === null) parts.push(timingHtml(group));
|
|
742
764
|
else parts.push(answerHtml(items[group.answer], timingHtml(group)));
|
|
743
765
|
// A canceled turn ends with its cancel line, as the live view left it.
|
|
744
766
|
parts.push(cancelLineHtml(group.record, escapeHtml));
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -173,7 +173,7 @@ module Samagotchi
|
|
|
173
173
|
absorb_notes
|
|
174
174
|
|
|
175
175
|
if (prompt = take_initial_prompt)
|
|
176
|
-
run_prompt(prompt, nil)
|
|
176
|
+
run_prompt(prompt, nil) unless initial_command(prompt)
|
|
177
177
|
next
|
|
178
178
|
end
|
|
179
179
|
|
|
@@ -269,6 +269,29 @@ module Samagotchi
|
|
|
269
269
|
prompt
|
|
270
270
|
end
|
|
271
271
|
|
|
272
|
+
# A session command sent as a message (chi send -m "/model x", a web
|
|
273
|
+
# page's first message) runs as the command, as the Bridge does for a
|
|
274
|
+
# POST /turn; an unknown /word stays a prompt. Queued: the loop's next
|
|
275
|
+
# pass runs it.
|
|
276
|
+
# @return [Boolean] whether +text+ was one
|
|
277
|
+
def queue_as_command(text, origin)
|
|
278
|
+
return false unless @engine.command_registry.command?(text.to_s)
|
|
279
|
+
|
|
280
|
+
@bridge.queue_command(text, client_id: origin&.dig(:client_id))
|
|
281
|
+
true
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# The first prompt as a command: the session was saved as running for a
|
|
285
|
+
# turn that won't run.
|
|
286
|
+
# @return [Boolean] whether it was one
|
|
287
|
+
def initial_command(prompt)
|
|
288
|
+
return false unless queue_as_command(prompt, nil)
|
|
289
|
+
|
|
290
|
+
@session.status = Session::STATUS_IDLE
|
|
291
|
+
save_session
|
|
292
|
+
true
|
|
293
|
+
end
|
|
294
|
+
|
|
272
295
|
# Add the queued context notes to the conversation (between turns only,
|
|
273
296
|
# on this thread), save, then delete their files: a crash before the
|
|
274
297
|
# delete leaves them claimed, and Engine#add_context_note skips a note
|
|
@@ -293,7 +316,10 @@ module Samagotchi
|
|
|
293
316
|
|
|
294
317
|
begin
|
|
295
318
|
message, origin, no_interrupt, images = SessionInbox.read_input(claimed_file)
|
|
296
|
-
|
|
319
|
+
return if message.to_s.strip.empty?
|
|
320
|
+
return if Array(images).empty? && queue_as_command(message, origin)
|
|
321
|
+
|
|
322
|
+
run_prompt(message, origin, no_interrupt: !!no_interrupt, images: images || [])
|
|
297
323
|
ensure
|
|
298
324
|
FileUtils.rm_f(claimed_file)
|
|
299
325
|
end
|
|
@@ -42,7 +42,10 @@ module Samagotchi
|
|
|
42
42
|
return nil unless sidecar&.port&.positive?
|
|
43
43
|
|
|
44
44
|
begin
|
|
45
|
-
Socket.tcp(
|
|
45
|
+
# Not Socket.tcp: with connect_timeout (Ruby 3.4, macOS 26) it hands
|
|
46
|
+
# back the socket of a refused connect (SO_ERROR set), so a dead
|
|
47
|
+
# worker's port read as live. TCPSocket raises ECONNREFUSED.
|
|
48
|
+
TCPSocket.new(host, sidecar.port, connect_timeout: timeout).close
|
|
46
49
|
sidecar
|
|
47
50
|
rescue StandardError
|
|
48
51
|
FileUtils.rm_f(path(session_dir)) if unlink
|
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.17.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -137,6 +137,7 @@ files:
|
|
|
137
137
|
- docs/releasing.md
|
|
138
138
|
- docs/sessions.md
|
|
139
139
|
- docs/sub-agent.md
|
|
140
|
+
- docs/testing.md
|
|
140
141
|
- lib/samagotchi.rb
|
|
141
142
|
- lib/samagotchi/answer_command.rb
|
|
142
143
|
- lib/samagotchi/answer_display.rb
|
|
@@ -280,6 +281,7 @@ files:
|
|
|
280
281
|
- lib/samagotchi/memory_bundle/source.rb
|
|
281
282
|
- lib/samagotchi/memory_bundle/status.rb
|
|
282
283
|
- lib/samagotchi/memory_bundle/system_bundle.rb
|
|
284
|
+
- lib/samagotchi/memory_bundle/trash.rb
|
|
283
285
|
- lib/samagotchi/memory_bundle/uninstaller.rb
|
|
284
286
|
- lib/samagotchi/memory_paths.rb
|
|
285
287
|
- lib/samagotchi/model_catalog.rb
|
|
@@ -310,6 +312,10 @@ files:
|
|
|
310
312
|
- lib/samagotchi/question_desk.rb
|
|
311
313
|
- lib/samagotchi/recap_setup.rb
|
|
312
314
|
- lib/samagotchi/recap_store.rb
|
|
315
|
+
- lib/samagotchi/relay.rb
|
|
316
|
+
- lib/samagotchi/relay_desk.rb
|
|
317
|
+
- lib/samagotchi/relay_verifier.rb
|
|
318
|
+
- lib/samagotchi/relay_watcher.rb
|
|
313
319
|
- lib/samagotchi/reminder_queue.rb
|
|
314
320
|
- lib/samagotchi/reminder_store.rb
|
|
315
321
|
- lib/samagotchi/reply_wait.rb
|
|
@@ -366,6 +372,7 @@ files:
|
|
|
366
372
|
- lib/samagotchi/tools/builtins.rb
|
|
367
373
|
- lib/samagotchi/tools/cancel_reminder.rb
|
|
368
374
|
- lib/samagotchi/tools/delegate.rb
|
|
375
|
+
- lib/samagotchi/tools/delegate_relay.rb
|
|
369
376
|
- lib/samagotchi/tools/delegate_result.rb
|
|
370
377
|
- lib/samagotchi/tools/delegate_wait.rb
|
|
371
378
|
- lib/samagotchi/tools/edit.rb
|