samagotchi 0.32.0 → 0.34.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 +75 -1
- data/README.md +4 -1
- data/docs/context.md +8 -1
- data/docs/guardrails.md +31 -1
- data/docs/hooks.md +7 -4
- data/docs/memory.md +17 -1
- data/docs/plugins.md +79 -3
- data/docs/releasing.md +3 -2
- data/docs/sessions.md +9 -3
- data/lib/samagotchi/answer_command.rb +2 -1
- data/lib/samagotchi/archive_store.rb +1 -15
- data/lib/samagotchi/bridge.rb +8 -3
- data/lib/samagotchi/bundles/coordinator/manifest.yml +14 -0
- data/lib/samagotchi/bundles/coordinator/plugin.rb +114 -0
- data/lib/samagotchi/bundles/coordinator/skill_coordinator.md +60 -0
- data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
- data/lib/samagotchi/bundles/mcp/manifest.yml +3 -3
- data/lib/samagotchi/bundles/mcp/plugin.rb +10 -11
- data/lib/samagotchi/bundles/system/delegated.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/bundles/system/self_map.md +4 -2
- data/lib/samagotchi/child_reports.rb +31 -10
- data/lib/samagotchi/children_status.rb +157 -0
- data/lib/samagotchi/client_id.rb +52 -0
- data/lib/samagotchi/context_absorber.rb +2 -1
- data/lib/samagotchi/context_fetch.rb +14 -45
- data/lib/samagotchi/context_sources.rb +20 -10
- data/lib/samagotchi/cut_policy.rb +58 -0
- data/lib/samagotchi/engine.rb +24 -108
- data/lib/samagotchi/engine_peers.rb +30 -0
- data/lib/samagotchi/guardrail_wiring.rb +27 -2
- data/lib/samagotchi/guardrails/approvals.rb +7 -0
- data/lib/samagotchi/guardrails/child_boundary.rb +103 -0
- data/lib/samagotchi/guardrails/context.rb +32 -0
- data/lib/samagotchi/guardrails/parent_approvals.rb +0 -2
- data/lib/samagotchi/guardrails/shared_git.rb +124 -0
- data/lib/samagotchi/guardrails.rb +1 -0
- data/lib/samagotchi/kernel_loop.rb +9 -22
- data/lib/samagotchi/llm/chat_loop.rb +11 -22
- data/lib/samagotchi/memory_bundle/builder.rb +35 -1
- data/lib/samagotchi/memory_bundle/installer.rb +6 -2
- data/lib/samagotchi/memory_bundle/manifest.rb +28 -5
- data/lib/samagotchi/memory_bundle/provenance.rb +2 -1
- data/lib/samagotchi/memory_paths.rb +10 -0
- data/lib/samagotchi/plugin/api.rb +2 -2
- data/lib/samagotchi/plugin/sessions.rb +47 -2
- data/lib/samagotchi/process_group.rb +157 -0
- data/lib/samagotchi/prompt_warmup.rb +20 -1
- data/lib/samagotchi/question_desk.rb +2 -1
- data/lib/samagotchi/relay_verifier.rb +2 -4
- data/lib/samagotchi/reply_wait.rb +9 -4
- data/lib/samagotchi/send_command.rb +2 -1
- data/lib/samagotchi/session_manager.rb +2 -4
- data/lib/samagotchi/steer.rb +27 -19
- data/lib/samagotchi/steer_cut.rb +125 -0
- data/lib/samagotchi/system_prompt.rb +20 -8
- data/lib/samagotchi/terminal_ui/attached_loop.rb +5 -4
- data/lib/samagotchi/terminal_ui/formatting.rb +21 -12
- data/lib/samagotchi/terminal_ui.rb +2 -1
- data/lib/samagotchi/tool_declarations.rb +9 -5
- data/lib/samagotchi/tools/builtins.rb +2 -2
- data/lib/samagotchi/tools/delegate.rb +105 -16
- data/lib/samagotchi/tools/delegate_wait.rb +55 -13
- data/lib/samagotchi/tools/execute.rb +10 -81
- data/lib/samagotchi/tools/task_runtime.rb +25 -38
- data/lib/samagotchi/turn_note.rb +9 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +2 -1
- data/lib/samagotchi/web/public/app.js +19 -20
- data/lib/samagotchi/web/public/data.js +3 -0
- data/lib/samagotchi/web/public/question_card.js +3 -4
- data/lib/samagotchi/web/public/shown_texts.js +13 -0
- data/lib/samagotchi/web/public/stable_html.js +18 -0
- data/lib/samagotchi/web/public/turn_events.js +9 -2
- data/lib/samagotchi/web/public/turn_view.js +1 -1
- data/lib/samagotchi/worker.rb +40 -13
- metadata +14 -1
|
@@ -6,6 +6,7 @@ require "securerandom"
|
|
|
6
6
|
require "shellwords"
|
|
7
7
|
require "time"
|
|
8
8
|
require_relative "../atomic_file"
|
|
9
|
+
require_relative "../process_group"
|
|
9
10
|
require_relative "shell"
|
|
10
11
|
|
|
11
12
|
module Samagotchi
|
|
@@ -24,6 +25,9 @@ module Samagotchi
|
|
|
24
25
|
MARKER_ENV_KEYS = %w[SAMAGOTCHI_PARENT_SESSION SAMAGOTCHI_SESSION_MODEL].freeze
|
|
25
26
|
# task_wait and task_get add it to a task the user stopped (the web's
|
|
26
27
|
# stop-task button), so the model doesn't rerun the job.
|
|
28
|
+
# A record whose pid isn't a process group chi started (missing, 0, 1,
|
|
29
|
+
# not a number, or a pid another process reuses): nothing is signalled.
|
|
30
|
+
NOT_CHIS_PROCESS = "the task's process is gone or isn't the one chi started"
|
|
27
31
|
USER_STOP_NOTE = "note: the user stopped this task; don't restart it unless they ask."
|
|
28
32
|
|
|
29
33
|
module_function
|
|
@@ -54,13 +58,12 @@ module Samagotchi
|
|
|
54
58
|
|
|
55
59
|
wrapped_command = wrapped_shell_command(normalized_command, exit_code_path)
|
|
56
60
|
# Non-login, the shell Execute uses too (Shell).
|
|
57
|
-
pid =
|
|
61
|
+
pid = ProcessGroup.spawn(
|
|
58
62
|
spawn_env,
|
|
59
63
|
*Shell.argv(wrapped_command),
|
|
60
64
|
chdir: resolved_cwd,
|
|
61
65
|
out: output_io,
|
|
62
|
-
err: output_io
|
|
63
|
-
pgroup: true
|
|
66
|
+
err: output_io
|
|
64
67
|
)
|
|
65
68
|
Process.detach(pid)
|
|
66
69
|
|
|
@@ -185,26 +188,15 @@ module Samagotchi
|
|
|
185
188
|
refreshed = refresh_record(record)
|
|
186
189
|
return [refreshed, nil] unless refreshed["status"] == "running"
|
|
187
190
|
|
|
191
|
+
pid = refreshed["pid"]
|
|
192
|
+
# Checked again just before the signal: never a group chi didn't start.
|
|
193
|
+
return [mark_not_chis(refreshed), nil] unless ProcessGroup.leader?(pid)
|
|
194
|
+
|
|
188
195
|
refreshed["stop_requested_by"] = by.to_s
|
|
189
196
|
write_record(refreshed)
|
|
190
|
-
pid = refreshed["pid"].to_i
|
|
191
|
-
|
|
192
|
-
begin
|
|
193
|
-
Process.kill("TERM", -pid)
|
|
194
|
-
rescue Errno::ESRCH
|
|
195
|
-
# Process already exited; status refresh below handles final state.
|
|
196
|
-
end
|
|
197
197
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if process_alive?(pid)
|
|
202
|
-
begin
|
|
203
|
-
Process.kill("KILL", -pid)
|
|
204
|
-
rescue Errno::ESRCH
|
|
205
|
-
# Process exited between checks.
|
|
206
|
-
end
|
|
207
|
-
end
|
|
198
|
+
# A group that isn't ours (EPERM) is an error, not a stop.
|
|
199
|
+
ProcessGroup.stop(pid, grace: STOP_GRACE_SEC, poll: STOP_POLL_INTERVAL_SEC, quiet: [Errno::ESRCH])
|
|
208
200
|
|
|
209
201
|
updated = mark_stopped(refresh_record(refreshed))
|
|
210
202
|
write_record(updated)
|
|
@@ -217,10 +209,18 @@ module Samagotchi
|
|
|
217
209
|
def refresh_record(record)
|
|
218
210
|
return record unless record["status"] == "running"
|
|
219
211
|
|
|
220
|
-
pid = record["pid"]
|
|
221
|
-
return
|
|
212
|
+
pid = record["pid"]
|
|
213
|
+
return record if ProcessGroup.leader?(pid)
|
|
214
|
+
# Gone: its exit code says how it ended.
|
|
215
|
+
return mark_finished_without_exit_code(record) if ProcessGroup.signalable?(pid) && !ProcessGroup.alive?(pid)
|
|
222
216
|
|
|
223
|
-
record
|
|
217
|
+
mark_not_chis(record)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# A running record whose pid isn't a group chi started (NOT_CHIS_PROCESS)
|
|
221
|
+
# ends as failed, unless its exit code or a stop on disk says more.
|
|
222
|
+
def mark_not_chis(record)
|
|
223
|
+
mark_finished_without_exit_code(record, missing_reason: NOT_CHIS_PROCESS)
|
|
224
224
|
end
|
|
225
225
|
|
|
226
226
|
# A session id (a uuid) is never a task's: task ids are a timestamp
|
|
@@ -338,16 +338,7 @@ module Samagotchi
|
|
|
338
338
|
filtered.join(" ")
|
|
339
339
|
end
|
|
340
340
|
|
|
341
|
-
def
|
|
342
|
-
Process.kill(0, pid)
|
|
343
|
-
true
|
|
344
|
-
rescue Errno::ESRCH
|
|
345
|
-
false
|
|
346
|
-
rescue Errno::EPERM
|
|
347
|
-
true
|
|
348
|
-
end
|
|
349
|
-
|
|
350
|
-
def mark_finished_without_exit_code(record)
|
|
341
|
+
def mark_finished_without_exit_code(record, missing_reason: "process_ended_without_exit_code")
|
|
351
342
|
record = current_record(record)
|
|
352
343
|
return record unless record["status"] == "running"
|
|
353
344
|
|
|
@@ -359,7 +350,7 @@ module Samagotchi
|
|
|
359
350
|
elsif exit_code.nil?
|
|
360
351
|
record["status"] = "failed"
|
|
361
352
|
record["exit_code"] = nil
|
|
362
|
-
record["stop_reason"] ||=
|
|
353
|
+
record["stop_reason"] ||= missing_reason
|
|
363
354
|
elsif exit_code.zero?
|
|
364
355
|
record["status"] = "completed"
|
|
365
356
|
record["exit_code"] = 0
|
|
@@ -401,10 +392,6 @@ module Samagotchi
|
|
|
401
392
|
def timestamp
|
|
402
393
|
Time.now.utc.iso8601
|
|
403
394
|
end
|
|
404
|
-
|
|
405
|
-
def monotonic_time
|
|
406
|
-
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
407
|
-
end
|
|
408
395
|
end
|
|
409
396
|
end
|
|
410
397
|
end
|
data/lib/samagotchi/turn_note.rb
CHANGED
|
@@ -36,8 +36,16 @@ module Samagotchi
|
|
|
36
36
|
# @param restored [Boolean] the prompt went back to its sender (the
|
|
37
37
|
# failed user message is not in the conversation any more)
|
|
38
38
|
# @param continued [Boolean] it was a continue turn (no user message)
|
|
39
|
-
|
|
39
|
+
# @param wake [String, nil] what woke it, when chi started the turn and
|
|
40
|
+
# not the user ("the change in attached context pr-7"): the user
|
|
41
|
+
# wrote nothing, and wakes pause until they do
|
|
42
|
+
# @param kept [String, nil] what becomes of the wake's news, after
|
|
43
|
+
# that (a delegate report's rings stay for the user's next turn)
|
|
44
|
+
def failed(summary, restored: false, continued: false, wake: nil, kept: nil)
|
|
40
45
|
tail = if restored then RESTORED
|
|
46
|
+
elsif wake
|
|
47
|
+
"The wake turn for #{one_line(wake)} was not answered; chi starts no other wake turn until the user writes." +
|
|
48
|
+
(kept ? " #{one_line(kept)}" : "")
|
|
41
49
|
elsif continued then "The continued turn stopped there."
|
|
42
50
|
else "The user's last message was not answered."
|
|
43
51
|
end
|
data/lib/samagotchi/version.rb
CHANGED
data/lib/samagotchi/web/app.rb
CHANGED
|
@@ -9,6 +9,7 @@ require "uri"
|
|
|
9
9
|
require "rack"
|
|
10
10
|
require "rack/request"
|
|
11
11
|
|
|
12
|
+
require_relative "../client_id"
|
|
12
13
|
require_relative "../answer_tail"
|
|
13
14
|
require_relative "../bridge_client"
|
|
14
15
|
require_relative "../bridge/bounded_queue"
|
|
@@ -1069,7 +1070,7 @@ module Samagotchi
|
|
|
1069
1070
|
# {error: "held" | "not_running" | "unsupported" | "failed", reason, detail} in words; 409
|
|
1070
1071
|
# owned_by_tui; 404.
|
|
1071
1072
|
def handle_restart(_req, id)
|
|
1072
|
-
result = @manager.restart_session(id, state_dir: @state_dir, client_id:
|
|
1073
|
+
result = @manager.restart_session(id, state_dir: @state_dir, client_id: ClientId::WEB_RESTART)
|
|
1073
1074
|
@hub&.touch(id)
|
|
1074
1075
|
json_response(200, { status: "restarted", session_id: id, from_version: result.from_version,
|
|
1075
1076
|
version: result.version })
|
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
addContext,
|
|
22
22
|
} from "./data.js";
|
|
23
23
|
import { createContextChips } from "./context_chips.js";
|
|
24
|
+
import { renderStable } from "./stable_html.js";
|
|
25
|
+
import { createShownTexts } from "./shown_texts.js";
|
|
24
26
|
import { MODEL_KEY } from "./model_pick.js";
|
|
25
27
|
import { emptySessionState } from "./session_state.js";
|
|
26
28
|
import { createModelPicker } from "./model_picker.js";
|
|
@@ -63,7 +65,7 @@ const infoBarEl = $("#infoBar");
|
|
|
63
65
|
const firstPeekEl = $("#firstPeek");
|
|
64
66
|
const infoTextEl = $("#infoText");
|
|
65
67
|
// The open session's attached context, in its own row of the session bar
|
|
66
|
-
// (updateInfoBar
|
|
68
|
+
// (updateInfoBar redraws #infoText every tick; the chips keep their popover).
|
|
67
69
|
const contextChips = createContextChips({
|
|
68
70
|
el: $("#contextChips"),
|
|
69
71
|
api: { list: listContext, show: getContext, detach: detachContext, add: addContext },
|
|
@@ -87,7 +89,8 @@ let eventsControl = null;
|
|
|
87
89
|
let resyncsInFlight = 0;
|
|
88
90
|
let resyncAgain = false;
|
|
89
91
|
let allSessions = [];
|
|
90
|
-
|
|
92
|
+
// The running turn's texts on the page (the turn output dedupe).
|
|
93
|
+
const shownTexts = createShownTexts();
|
|
91
94
|
// The page's view of a turn (web.view, or ?view=stage|turn): the running
|
|
92
95
|
// turn pinned above the composer (the stage, stage_view.js), or one block
|
|
93
96
|
// per turn with its steps in the history (turn_view.js).
|
|
@@ -98,7 +101,7 @@ const viewArgs = {
|
|
|
98
101
|
historyEl,
|
|
99
102
|
appendToHistory: (el) => appendToHistory(el),
|
|
100
103
|
isNearBottom: () => isNearBottom(),
|
|
101
|
-
sawText: (k) =>
|
|
104
|
+
sawText: (k) => shownTexts.saw(k),
|
|
102
105
|
sessionId: () => selected,
|
|
103
106
|
// The stop-task button on a running task_wait, for a worker that has
|
|
104
107
|
// the route (its sidecar features).
|
|
@@ -950,11 +953,7 @@ function applyFinalMarkdown(messages) {
|
|
|
950
953
|
}
|
|
951
954
|
|
|
952
955
|
function seedSeenFromDom() {
|
|
953
|
-
|
|
954
|
-
findInTurnRoots(".bubble").forEach((b) => {
|
|
955
|
-
const k = normalize(b.textContent);
|
|
956
|
-
if (k) seenContent.add(k);
|
|
957
|
-
});
|
|
956
|
+
shownTexts.seed(findInTurnRoots(".bubble").map((b) => normalize(b.textContent)));
|
|
958
957
|
}
|
|
959
958
|
|
|
960
959
|
// Sticky-follow scroll: only auto-scroll to the bottom while the user is
|
|
@@ -974,7 +973,7 @@ function atHistoryEnd() {
|
|
|
974
973
|
function appendChunk(text) {
|
|
975
974
|
if (!text) return;
|
|
976
975
|
const k = normalize(text);
|
|
977
|
-
if (!k ||
|
|
976
|
+
if (!k || shownTexts.shows(k)) return;
|
|
978
977
|
const follow = isNearBottom();
|
|
979
978
|
const hint = historyEl.querySelector(".hint");
|
|
980
979
|
if (hint) hint.remove();
|
|
@@ -1464,7 +1463,7 @@ function updateInfoBar() {
|
|
|
1464
1463
|
}
|
|
1465
1464
|
if (!selected) {
|
|
1466
1465
|
// The start page hides the info bar (the storage path is on #/sessions).
|
|
1467
|
-
infoTextEl
|
|
1466
|
+
renderStable(infoTextEl, "");
|
|
1468
1467
|
infoTextEl.title = "";
|
|
1469
1468
|
if (stopBtn) stopBtn.style.display = "none";
|
|
1470
1469
|
return;
|
|
@@ -1488,9 +1487,11 @@ function updateInfoBar() {
|
|
|
1488
1487
|
const attachCmd = `chi --attach ${selected}`;
|
|
1489
1488
|
const copied = Date.now() - attachCopiedAt < 2000;
|
|
1490
1489
|
const copyHtml = `<button type="button" class="copy-attach" title="Copy "${escapeHtml(attachCmd)}" to attach a terminal">${copied ? "copied ✓" : "copy chi --attach"}</button>`;
|
|
1491
|
-
const metaHtml = `<div class="meta"><span class="id" title="${escapeHtml(selected)}">${escapeHtml(idShort)}</span>${copyHtml}<span class="${statusClass}">${escapeHtml(current.status||"")}</span><span class="model${label.mismatch ? " served-mismatch" : ""}" title="${escapeHtml(label.title)}">${escapeHtml(label.text)}</span>${parentHtml}${current.usedMemories.length ? `<span class="model mem-chip" title="${escapeHtml(memChipTitle(current.usedMemories))}">${escapeHtml(memChipText(current.usedMemories))}</span>` : ""}<span class="dir" title="${escapeHtml(current.dir||"")}"><bdi>${escapeHtml(current.dir||"")}</bdi></span>${ctxText ? `<span class="model ctx" title="${escapeHtml(ctxTip)}">${escapeHtml(ctxText)}</span>` : ""}${speedTextNow ? `<span class="model speed" title="${escapeHtml(speedTip)}">${escapeHtml(speedTextNow)}</span>` : ""}${timingText ? `<span class="model"
|
|
1490
|
+
const metaHtml = `<div class="meta"><span class="id" title="${escapeHtml(selected)}">${escapeHtml(idShort)}</span>${copyHtml}<span class="${statusClass}">${escapeHtml(current.status||"")}</span><span class="model${label.mismatch ? " served-mismatch" : ""}" title="${escapeHtml(label.title)}">${escapeHtml(label.text)}</span>${parentHtml}${current.usedMemories.length ? `<span class="model mem-chip" title="${escapeHtml(memChipTitle(current.usedMemories))}">${escapeHtml(memChipText(current.usedMemories))}</span>` : ""}<span class="dir" title="${escapeHtml(current.dir||"")}"><bdi>${escapeHtml(current.dir||"")}</bdi></span>${ctxText ? `<span class="model ctx" title="${escapeHtml(ctxTip)}">${escapeHtml(ctxText)}</span>` : ""}${speedTextNow ? `<span class="model speed" title="${escapeHtml(speedTip)}">${escapeHtml(speedTextNow)}</span>` : ""}${timingText ? `<span class="model session-time"></span>` : ""}${workerBadgeHtml()}</div>`;
|
|
1492
1491
|
const previewHtml = `<div class="preview-line"><span class="preview">${escapeHtml(current.firstPreview || "—")}</span> · <span class="mem" title="${escapeHtml(current.usedMemories.join(", "))}">mem: ${escapeHtml(memText)}</span></div>`;
|
|
1493
|
-
|
|
1492
|
+
// The session clock ticks every second while a turn runs: only its text
|
|
1493
|
+
// changes, the bar's other nodes (a tooltip, a button mid-press) stay.
|
|
1494
|
+
renderStable(infoTextEl, metaHtml + previewHtml, { ".session-time": timingText });
|
|
1494
1495
|
infoTextEl.title = memoriesTooltip(current.usedMemories, current.preloadedMemories, current.mutedMemories);
|
|
1495
1496
|
}
|
|
1496
1497
|
|
|
@@ -1759,6 +1760,7 @@ const streamHandlers = {
|
|
|
1759
1760
|
// for a queued prompt among its rows.
|
|
1760
1761
|
turnDom.handOff?.({ immediate: true });
|
|
1761
1762
|
markRecapStale();
|
|
1763
|
+
shownTexts.turnStarted();
|
|
1762
1764
|
const promptBubble = applyPromptOps(data);
|
|
1763
1765
|
setTurnRunning(true);
|
|
1764
1766
|
setLiveStatus("running");
|
|
@@ -2071,12 +2073,9 @@ async function handleSendTurn() {
|
|
|
2071
2073
|
// for its ack is never taken back (4.13), so drop it here. The prompt
|
|
2072
2074
|
// is still in the composer, as the echo was removed.
|
|
2073
2075
|
dropEarlyRestores(earlyRestores);
|
|
2074
|
-
// The chips stay (uploaded ones keep their ref) for another try
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
return;
|
|
2078
|
-
}
|
|
2079
|
-
if (/owned_by_tui|409/.test(e.message)) {
|
|
2076
|
+
// The chips stay (uploaded ones keep their ref) for another try: an
|
|
2077
|
+
// image refused (bad_image, too_large, bad_images) says why below.
|
|
2078
|
+
if (e.code === "owned_by_tui") {
|
|
2080
2079
|
current.owner = "tui";
|
|
2081
2080
|
updateComposerMode();
|
|
2082
2081
|
return;
|
|
@@ -2107,7 +2106,7 @@ async function handleSendCommand(line) {
|
|
|
2107
2106
|
// The command woke the worker: follow its stream if none is open.
|
|
2108
2107
|
followWokenWorker();
|
|
2109
2108
|
} catch (e) {
|
|
2110
|
-
if (
|
|
2109
|
+
if (e.code === "owned_by_tui") {
|
|
2111
2110
|
current.owner = "tui";
|
|
2112
2111
|
updateComposerMode();
|
|
2113
2112
|
return;
|
|
@@ -2168,7 +2167,7 @@ async function handleCancel() {
|
|
|
2168
2167
|
await cancelTurn(selected);
|
|
2169
2168
|
} catch (e) {
|
|
2170
2169
|
// The turn ended meanwhile: nothing left to cancel.
|
|
2171
|
-
if (
|
|
2170
|
+
if (e.code !== "not_running") alert(e.message);
|
|
2172
2171
|
} finally {
|
|
2173
2172
|
cancelBtn.disabled = false;
|
|
2174
2173
|
}
|
|
@@ -8,6 +8,9 @@ export function api(path, opts = {}) {
|
|
|
8
8
|
if (!r.ok) {
|
|
9
9
|
const error = new Error(`${j.detail || j.error || r.statusText} (${r.status})`);
|
|
10
10
|
error.status = r.status;
|
|
11
|
+
// The server's error code ({error: "owned_by_tui", detail}): what
|
|
12
|
+
// the page matches on, never the message's words.
|
|
13
|
+
error.code = j.error || null;
|
|
11
14
|
throw error;
|
|
12
15
|
}
|
|
13
16
|
return j;
|
|
@@ -163,14 +163,13 @@ export function approvalAllowed(pq, answer) {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
// What the card says when an answer or a dismissal didn't go through. 503
|
|
166
|
-
// (the server found no live bridge) reads as the session not
|
|
167
|
-
// other failure — the worker's own (the server maps it to a 502
|
|
166
|
+
// not_live (the server found no live bridge) reads as the session not
|
|
167
|
+
// running; any other failure — the worker's own (the server maps it to a 502
|
|
168
168
|
// bridge_error) or the connection's — shows what it said, so a worker that
|
|
169
169
|
// failed the request is not mistaken for one that is gone.
|
|
170
170
|
export function answerErrorText(error) {
|
|
171
|
-
const status = error?.status;
|
|
172
171
|
const message = String(error?.message ?? error ?? "");
|
|
173
|
-
if (status === 503 ||
|
|
172
|
+
if (error?.status === 503 || error?.code === "not_live") return "Session is not running — restart it to answer.";
|
|
174
173
|
return message;
|
|
175
174
|
}
|
|
176
175
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// The texts the running turn already shows (its generations as they close,
|
|
2
|
+
// the bubbles a render drew): turn_completed's answer gets a bubble of its
|
|
3
|
+
// own only when it isn't one of them (it wasn't streamed). A new turn starts
|
|
4
|
+
// over, so an earlier turn's "Done." doesn't hide this one's.
|
|
5
|
+
export function createShownTexts() {
|
|
6
|
+
let seen = new Set();
|
|
7
|
+
return {
|
|
8
|
+
turnStarted() { seen = new Set(); },
|
|
9
|
+
seed(texts) { seen = new Set(texts.filter(Boolean)); },
|
|
10
|
+
saw(text) { if (text) seen.add(text); },
|
|
11
|
+
shows(text) { return seen.has(text); },
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// A block redrawn every tick (the info bar, once a second while a turn
|
|
2
|
+
// runs) keeps its nodes: +html+ is written only when it changed, so a
|
|
3
|
+
// shown tooltip stays up and a press on one of its buttons isn't lost to a
|
|
4
|
+
// rewrite between mousedown and mouseup. +live+ ({selector: text}) names
|
|
5
|
+
// the parts that change every tick (a running clock): +html+ carries them
|
|
6
|
+
// empty and only their text is set, so a tick alone rewrites nothing else.
|
|
7
|
+
const shown = new WeakMap();
|
|
8
|
+
|
|
9
|
+
export function renderStable(el, html, live = {}) {
|
|
10
|
+
if (shown.get(el) !== html) {
|
|
11
|
+
el.innerHTML = html;
|
|
12
|
+
shown.set(el, html);
|
|
13
|
+
}
|
|
14
|
+
for (const [selector, text] of Object.entries(live)) {
|
|
15
|
+
const part = el.querySelector(selector);
|
|
16
|
+
if (part && part.textContent !== text) part.textContent = text;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -93,7 +93,13 @@ export function isOwn(clientId, myId) {
|
|
|
93
93
|
|
|
94
94
|
// As the TUI labels them (Formatting::CLIENT_LABELS); this file's label
|
|
95
95
|
// helpers and the TUI's agree per spec/shared/labels_matrix.json.
|
|
96
|
-
|
|
96
|
+
// By the whole id or its prefix (up to and with its first ":"); an id
|
|
97
|
+
// chi doesn't know is "automatic", not the user (ClientId.human?).
|
|
98
|
+
const CLIENT_LABELS = Object.freeze({
|
|
99
|
+
"tui:": "tui", "web:": "web", "system:": "reminder", "delegate:": "delegate", "child:": "delegate report",
|
|
100
|
+
"cli:send": "chi send", "cli:answer": "chi answer", plugin: "plugin",
|
|
101
|
+
});
|
|
102
|
+
const AUTOMATIC_LABEL = "automatic";
|
|
97
103
|
|
|
98
104
|
// A delegate child's report comes in as client child:<id8> (ChildReports).
|
|
99
105
|
const REPORT_CLIENT = "child:";
|
|
@@ -110,7 +116,8 @@ export function clientLabel(clientId) {
|
|
|
110
116
|
if (!clientId) return null;
|
|
111
117
|
const id = String(clientId);
|
|
112
118
|
if (id.startsWith(CONTEXT_CLIENT) && id.length > CONTEXT_CLIENT.length) return `context ${id.slice(CONTEXT_CLIENT.length)} changed`;
|
|
113
|
-
|
|
119
|
+
const colon = id.indexOf(":");
|
|
120
|
+
return CLIENT_LABELS[id] || (colon >= 0 && CLIENT_LABELS[id.slice(0, colon + 1)]) || AUTOMATIC_LABEL;
|
|
114
121
|
}
|
|
115
122
|
|
|
116
123
|
// The note for reminders a turn got (a reminder turn has no prompt).
|
|
@@ -390,7 +390,7 @@ export function createTurnView({
|
|
|
390
390
|
}
|
|
391
391
|
writeClosedLabel(entry, gen);
|
|
392
392
|
if (!entry.toggled) entry.el.open = false;
|
|
393
|
-
// Every generation's text goes into
|
|
393
|
+
// Every generation's text goes into the shown texts (the turn output dedupe).
|
|
394
394
|
const k = normalize(gen.text);
|
|
395
395
|
if (k) sawText(k);
|
|
396
396
|
}
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "fileutils"
|
|
|
4
4
|
require "securerandom"
|
|
5
5
|
require "time"
|
|
6
6
|
|
|
7
|
+
require_relative "client_id"
|
|
7
8
|
require_relative "config"
|
|
8
9
|
require_relative "events"
|
|
9
10
|
require_relative "session"
|
|
@@ -287,6 +288,9 @@ module Samagotchi
|
|
|
287
288
|
@context_poller&.stop
|
|
288
289
|
@engine&.shutdown
|
|
289
290
|
@bridge&.stop
|
|
291
|
+
# The process exits next: a parent wake it rang for (a crash's
|
|
292
|
+
# ring comes just above) must spawn the parent's worker first.
|
|
293
|
+
ChildRing.await_wakes
|
|
290
294
|
end
|
|
291
295
|
end
|
|
292
296
|
|
|
@@ -425,7 +429,7 @@ module Samagotchi
|
|
|
425
429
|
@context_absorber.commit(waking ? woken(batch, waking) : batch)
|
|
426
430
|
return false unless waking
|
|
427
431
|
|
|
428
|
-
run_context_wake_turn(waking
|
|
432
|
+
run_context_wake_turn(waking, turn_id)
|
|
429
433
|
true
|
|
430
434
|
rescue SystemCallError, IOError => e
|
|
431
435
|
Log.exception(:worker, "context_failed", e)
|
|
@@ -473,17 +477,21 @@ module Samagotchi
|
|
|
473
477
|
# turn from context:<name> whose first message is the wake note already
|
|
474
478
|
# in the conversation (named by +turn_id+), on the same rules as a
|
|
475
479
|
# delegate report's wake turn: it counts toward session.max_wakes, and
|
|
476
|
-
# a failure goes back to before the turn (the note stays
|
|
477
|
-
#
|
|
478
|
-
|
|
480
|
+
# a failure goes back to before the turn (the note stays, as a plain
|
|
481
|
+
# one: #unmark_wake_note) and pauses wakes until a human's input.
|
|
482
|
+
# +waking+: the ContextAbsorber::Delivery that woke it.
|
|
483
|
+
def run_context_wake_turn(waking, turn_id)
|
|
484
|
+
name = waking.name
|
|
479
485
|
@wakes_in_a_row += 1
|
|
480
486
|
Log.info(:worker, "context_wake_turn", name: name, in_a_row: @wakes_in_a_row)
|
|
481
487
|
@turn_flow.before_prompt_turn
|
|
482
|
-
run_engine_turn(nil, continue: true, origin: { client_id: "#{
|
|
488
|
+
run_engine_turn(nil, continue: true, origin: { client_id: "#{ClientId::CONTEXT_PREFIX}#{name}" },
|
|
483
489
|
id: turn_id, max_iterations: IterationLimit.for) do |result, error|
|
|
484
490
|
if error
|
|
485
|
-
note = TurnNote.failed(error.respond_to?(:summary) ? error.summary : error.message
|
|
491
|
+
note = TurnNote.failed(error.respond_to?(:summary) ? error.summary : error.message,
|
|
492
|
+
wake: "the change in attached context #{name}")
|
|
486
493
|
@turn_flow.prompt_turn_failed(note: note)
|
|
494
|
+
unmark_wake_note(turn_id, waking.note)
|
|
487
495
|
@wakes_paused = true
|
|
488
496
|
else
|
|
489
497
|
@continue_offer.after_turn(result)
|
|
@@ -491,6 +499,18 @@ module Samagotchi
|
|
|
491
499
|
end
|
|
492
500
|
end
|
|
493
501
|
|
|
502
|
+
# A failed wake turn kept nothing but its note: the update goes back to
|
|
503
|
+
# a plain note (no turn start, the background wording), so a reload
|
|
504
|
+
# draws no empty turn for it.
|
|
505
|
+
def unmark_wake_note(turn_id, note)
|
|
506
|
+
messages = @engine.messages_checkpoint
|
|
507
|
+
index = messages.index { |m| m[:turn_start] && m[:turn_id] == turn_id }
|
|
508
|
+
return unless index
|
|
509
|
+
|
|
510
|
+
messages[index] = ContextNote.message(**note)
|
|
511
|
+
@engine.rollback_to(messages)
|
|
512
|
+
end
|
|
513
|
+
|
|
494
514
|
# Whether the session has had a turn: a user or assistant message.
|
|
495
515
|
def conversation_started?
|
|
496
516
|
Array(@session.messages).any? { |m| %w[user assistant model].include?((m[:role] || m["role"]).to_s) }
|
|
@@ -551,11 +571,11 @@ module Samagotchi
|
|
|
551
571
|
@engine.clear_due_reminder_names!
|
|
552
572
|
return false unless @engine.reminders_due?
|
|
553
573
|
|
|
554
|
-
@continue_offer.drop({ client_id:
|
|
574
|
+
@continue_offer.drop({ client_id: ClientId::REMINDER })
|
|
555
575
|
# A failure has no prompt to hand back (the Engine announced
|
|
556
576
|
# :turn_failed). The offer went before the turn
|
|
557
577
|
# (ContinueOffer#drop); either way the rollback window closes.
|
|
558
|
-
run_engine_turn(nil, continue: true, origin: { client_id:
|
|
578
|
+
run_engine_turn(nil, continue: true, origin: { client_id: ClientId::REMINDER },
|
|
559
579
|
max_iterations: IterationLimit.for) { @turn_flow.after_reminder_turn }
|
|
560
580
|
true
|
|
561
581
|
end
|
|
@@ -597,7 +617,11 @@ module Samagotchi
|
|
|
597
617
|
@turn_flow.before_prompt_turn
|
|
598
618
|
run_engine_turn(nil, continue: true, origin: reports.first.origin, max_iterations: IterationLimit.for) do |result, error|
|
|
599
619
|
if error
|
|
600
|
-
|
|
620
|
+
# The rings stay (settle_child_reports): the user's next turn takes them.
|
|
621
|
+
note = TurnNote.failed(error.respond_to?(:summary) ? error.summary : error.message,
|
|
622
|
+
wake: reports.size == 1 ? "a delegate's report" : "#{reports.size} delegate reports",
|
|
623
|
+
kept: "chi keeps #{reports.size == 1 ? "the report and brings it" : "the reports and brings them"} " \
|
|
624
|
+
"again with the user's next message.")
|
|
601
625
|
@turn_flow.prompt_turn_failed(note: note)
|
|
602
626
|
@wakes_paused = true
|
|
603
627
|
else
|
|
@@ -759,8 +783,11 @@ module Samagotchi
|
|
|
759
783
|
rescue StandardError => e
|
|
760
784
|
error = e
|
|
761
785
|
# Still running: no turn of the Engine's ended (it never began).
|
|
786
|
+
# It ends here, failed, as an Engine turn would record it: a wait
|
|
787
|
+
# (ReplyWait) tells a turn's end by last_turn.
|
|
762
788
|
if @session.status == Session::STATUS_RUNNING
|
|
763
789
|
@session.status = Session::STATUS_IDLE
|
|
790
|
+
@engine.record_last_turn(@session, "failed", 0, turn_args[:origin], { type: :turn_failed })
|
|
764
791
|
Log.warn(:worker, "turn_not_begun", error: e.class.name, msg: e.message)
|
|
765
792
|
end
|
|
766
793
|
ensure
|
|
@@ -800,12 +827,12 @@ module Samagotchi
|
|
|
800
827
|
# Whether the turn starting now is a delegate child's parent's: its
|
|
801
828
|
# task (the initial prompt), a follow-up, a continue the parent
|
|
802
829
|
# answered, a reminder, anything but a human typing into the child
|
|
803
|
-
# (
|
|
830
|
+
# (ClientId.human?), and any turn after a question the parent
|
|
804
831
|
# was rung about.
|
|
805
832
|
def reports_to_parent?(origin)
|
|
806
833
|
return false unless @session.delegate?
|
|
807
834
|
|
|
808
|
-
@initial_turn || @owes_parent || !
|
|
835
|
+
@initial_turn || @owes_parent || !ClientId.human?(origin&.dig(:client_id))
|
|
809
836
|
end
|
|
810
837
|
|
|
811
838
|
# The Engine published a question (after the question desk saved it).
|
|
@@ -1023,11 +1050,11 @@ module Samagotchi
|
|
|
1023
1050
|
Log.info(:worker, "idle_exit", idle_s: @idle_exit.idle_seconds.round)
|
|
1024
1051
|
end
|
|
1025
1052
|
|
|
1026
|
-
# A human's input (
|
|
1053
|
+
# A human's input (ClientId.human?) un-archives the session;
|
|
1027
1054
|
# a delegate's, a plugin's or a reminder's doesn't.
|
|
1028
1055
|
# It also resets the delegate-report wakes (budget and pause).
|
|
1029
1056
|
def user_input(client_id)
|
|
1030
|
-
return unless
|
|
1057
|
+
return unless ClientId.human?(client_id)
|
|
1031
1058
|
|
|
1032
1059
|
ArchiveStore.user_input(@session_id, state_dir: @state_dir)
|
|
1033
1060
|
@wakes_in_a_row = 0
|
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.34.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -169,6 +169,9 @@ files:
|
|
|
169
169
|
- lib/samagotchi/bundles/btw/plugin.rb
|
|
170
170
|
- lib/samagotchi/bundles/check-in/manifest.yml
|
|
171
171
|
- lib/samagotchi/bundles/check-in/plugin.rb
|
|
172
|
+
- lib/samagotchi/bundles/coordinator/manifest.yml
|
|
173
|
+
- lib/samagotchi/bundles/coordinator/plugin.rb
|
|
174
|
+
- lib/samagotchi/bundles/coordinator/skill_coordinator.md
|
|
172
175
|
- lib/samagotchi/bundles/core/manifest.yml
|
|
173
176
|
- lib/samagotchi/bundles/dev/manifest.yml
|
|
174
177
|
- lib/samagotchi/bundles/github-pr/manifest.yml
|
|
@@ -198,11 +201,13 @@ files:
|
|
|
198
201
|
- lib/samagotchi/bundles/system/self_map.md
|
|
199
202
|
- lib/samagotchi/cancellation_controller.rb
|
|
200
203
|
- lib/samagotchi/child_reports.rb
|
|
204
|
+
- lib/samagotchi/children_status.rb
|
|
201
205
|
- lib/samagotchi/cli/command.rb
|
|
202
206
|
- lib/samagotchi/cli/exit.rb
|
|
203
207
|
- lib/samagotchi/cli/flags.rb
|
|
204
208
|
- lib/samagotchi/cli/parent_wait.rb
|
|
205
209
|
- lib/samagotchi/client.rb
|
|
210
|
+
- lib/samagotchi/client_id.rb
|
|
206
211
|
- lib/samagotchi/command_steps.rb
|
|
207
212
|
- lib/samagotchi/commands/registry.rb
|
|
208
213
|
- lib/samagotchi/config.rb
|
|
@@ -219,6 +224,7 @@ files:
|
|
|
219
224
|
- lib/samagotchi/context_usage.rb
|
|
220
225
|
- lib/samagotchi/context_window.rb
|
|
221
226
|
- lib/samagotchi/continue_offer.rb
|
|
227
|
+
- lib/samagotchi/cut_policy.rb
|
|
222
228
|
- lib/samagotchi/debug_log.rb
|
|
223
229
|
- lib/samagotchi/desktop.rb
|
|
224
230
|
- lib/samagotchi/desktop/macos.rb
|
|
@@ -235,6 +241,7 @@ files:
|
|
|
235
241
|
- lib/samagotchi/edit_preview.rb
|
|
236
242
|
- lib/samagotchi/empty_answer_retry.rb
|
|
237
243
|
- lib/samagotchi/engine.rb
|
|
244
|
+
- lib/samagotchi/engine_peers.rb
|
|
238
245
|
- lib/samagotchi/events.rb
|
|
239
246
|
- lib/samagotchi/gem_update.rb
|
|
240
247
|
- lib/samagotchi/generation_phase.rb
|
|
@@ -242,6 +249,7 @@ files:
|
|
|
242
249
|
- lib/samagotchi/guardrails.rb
|
|
243
250
|
- lib/samagotchi/guardrails/approval.rb
|
|
244
251
|
- lib/samagotchi/guardrails/approvals.rb
|
|
252
|
+
- lib/samagotchi/guardrails/child_boundary.rb
|
|
245
253
|
- lib/samagotchi/guardrails/context.rb
|
|
246
254
|
- lib/samagotchi/guardrails/gate.rb
|
|
247
255
|
- lib/samagotchi/guardrails/load_failures.rb
|
|
@@ -254,6 +262,7 @@ files:
|
|
|
254
262
|
- lib/samagotchi/guardrails/rm_targets.rb
|
|
255
263
|
- lib/samagotchi/guardrails/rules.rb
|
|
256
264
|
- lib/samagotchi/guardrails/scratch_writes.rb
|
|
265
|
+
- lib/samagotchi/guardrails/shared_git.rb
|
|
257
266
|
- lib/samagotchi/guardrails/shell_git_dirs.rb
|
|
258
267
|
- lib/samagotchi/guardrails/shell_lex.rb
|
|
259
268
|
- lib/samagotchi/guardrails/shell_paths.rb
|
|
@@ -334,6 +343,7 @@ files:
|
|
|
334
343
|
- lib/samagotchi/plugin/tool_result.rb
|
|
335
344
|
- lib/samagotchi/plugin_session_state.rb
|
|
336
345
|
- lib/samagotchi/plugin_tasks.rb
|
|
346
|
+
- lib/samagotchi/process_group.rb
|
|
337
347
|
- lib/samagotchi/project_scope.rb
|
|
338
348
|
- lib/samagotchi/prompt.rb
|
|
339
349
|
- lib/samagotchi/prompt_history.rb
|
|
@@ -364,6 +374,7 @@ files:
|
|
|
364
374
|
- lib/samagotchi/session_retention.rb
|
|
365
375
|
- lib/samagotchi/sessions_command.rb
|
|
366
376
|
- lib/samagotchi/steer.rb
|
|
377
|
+
- lib/samagotchi/steer_cut.rb
|
|
367
378
|
- lib/samagotchi/system_prompt.rb
|
|
368
379
|
- lib/samagotchi/terminal_ui.rb
|
|
369
380
|
- lib/samagotchi/terminal_ui/attach_launcher.rb
|
|
@@ -483,6 +494,8 @@ files:
|
|
|
483
494
|
- lib/samagotchi/web/public/sentences.js
|
|
484
495
|
- lib/samagotchi/web/public/session_state.js
|
|
485
496
|
- lib/samagotchi/web/public/sessions_list.js
|
|
497
|
+
- lib/samagotchi/web/public/shown_texts.js
|
|
498
|
+
- lib/samagotchi/web/public/stable_html.js
|
|
486
499
|
- lib/samagotchi/web/public/stage_model.js
|
|
487
500
|
- lib/samagotchi/web/public/stage_view.js
|
|
488
501
|
- lib/samagotchi/web/public/strip.js
|