@songsid/agend 2.1.7 → 2.1.8-beta.10
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.
- package/dist/agent-cli.js +14 -0
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.d.ts +4 -1
- package/dist/agent-endpoint.js +10 -1
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/backend/claude-code.d.ts +35 -5
- package/dist/backend/claude-code.js +288 -17
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/cli-env-probe-worker.d.ts +1 -0
- package/dist/backend/cli-env-probe-worker.js +25 -0
- package/dist/backend/cli-env-probe-worker.js.map +1 -0
- package/dist/backend/codex-session-lookup.d.ts +16 -0
- package/dist/backend/codex-session-lookup.js +76 -2
- package/dist/backend/codex-session-lookup.js.map +1 -1
- package/dist/backend/codex.d.ts +7 -4
- package/dist/backend/codex.js +120 -30
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/credential-profile.d.ts +9 -0
- package/dist/backend/credential-profile.js +20 -0
- package/dist/backend/credential-profile.js.map +1 -1
- package/dist/backend/grok.js +19 -0
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/muse.d.ts +6 -0
- package/dist/backend/muse.js +37 -3
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +21 -2
- package/dist/backend/types.js +14 -3
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +16 -4
- package/dist/channel/adapters/discord.js +20 -5
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/mcp-tools.js +6 -0
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/cli.js +138 -48
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +11 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/cross-instance-envelope.d.ts +7 -0
- package/dist/cross-instance-envelope.js +19 -1
- package/dist/cross-instance-envelope.js.map +1 -1
- package/dist/daemon.d.ts +151 -6
- package/dist/daemon.js +550 -72
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +9 -1
- package/dist/delivery-outbox.js +25 -4
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/fleet-context.d.ts +5 -0
- package/dist/fleet-context.js.map +1 -1
- package/dist/fleet-manager.d.ts +140 -18
- package/dist/fleet-manager.js +733 -106
- package/dist/fleet-manager.js.map +1 -1
- package/dist/forged-envelope-scan.d.ts +160 -0
- package/dist/forged-envelope-scan.js +312 -0
- package/dist/forged-envelope-scan.js.map +1 -0
- package/dist/general-knowledge/skills/persona-emoji/SKILL.md +52 -0
- package/dist/instance-lifecycle.d.ts +81 -6
- package/dist/instance-lifecycle.js +201 -17
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +34 -4
- package/dist/locale.js.map +1 -1
- package/dist/login-controller.js +11 -4
- package/dist/login-controller.js.map +1 -1
- package/dist/login-flows.d.ts +18 -1
- package/dist/login-flows.js +9 -2
- package/dist/login-flows.js.map +1 -1
- package/dist/ls-rows.d.ts +17 -0
- package/dist/ls-rows.js +73 -0
- package/dist/ls-rows.js.map +1 -1
- package/dist/outbound-handlers.d.ts +3 -0
- package/dist/outbound-handlers.js +3 -3
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +21 -4
- package/dist/outbound-schemas.js +12 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/paths.js +4 -2
- package/dist/paths.js.map +1 -1
- package/dist/pause-marker.d.ts +10 -1
- package/dist/pause-marker.js +26 -2
- package/dist/pause-marker.js.map +1 -1
- package/dist/service-installer.d.ts +8 -0
- package/dist/service-installer.js +36 -1
- package/dist/service-installer.js.map +1 -1
- package/dist/settings-api.d.ts +8 -8
- package/dist/settings-api.js +12 -5
- package/dist/settings-api.js.map +1 -1
- package/dist/status-emojis.d.ts +19 -0
- package/dist/status-emojis.js.map +1 -1
- package/dist/tool-permissions.js +8 -2
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +9 -0
- package/dist/topic-commands.js +64 -21
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-monitor.js +3 -0
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +63 -0
- package/dist/transcript-sources.js +171 -27
- package/dist/transcript-sources.js.map +1 -1
- package/dist/types.d.ts +25 -0
- package/dist/types.js +9 -1
- package/dist/types.js.map +1 -1
- package/dist/ui/settings.html +143 -49
- package/dist/wake-coordinator.d.ts +143 -0
- package/dist/wake-coordinator.js +321 -0
- package/dist/wake-coordinator.js.map +1 -0
- package/dist/web-api.d.ts +3 -1
- package/dist/web-api.js +1 -1
- package/dist/web-api.js.map +1 -1
- package/dist/web-terminal.d.ts +2 -0
- package/dist/web-terminal.js +3 -6
- package/dist/web-terminal.js.map +1 -1
- package/package.json +1 -1
- package/templates/systemd.service.ejs +7 -0
package/dist/ui/settings.html
CHANGED
|
@@ -37,8 +37,20 @@
|
|
|
37
37
|
.tag { font-size: 12px; color: var(--muted); background: #eef2ff; color: #3730a3; padding: 1px 7px; border-radius: 10px; }
|
|
38
38
|
.tag.persona { background: #fdf2f8; color: #9d174d; }
|
|
39
39
|
.tag.tg { background: #e0f2fe; color: #075985; }
|
|
40
|
+
.tag.se-default { background: transparent; color: var(--muted); border: 1px dashed var(--border); }
|
|
40
41
|
.sub { font-size: 13px; color: var(--muted); margin-top: 3px; margin-left: 19px; }
|
|
41
42
|
.spacer { flex: 1; }
|
|
43
|
+
/* "Connections & Bots" rows carry up to ten short facts. Left to shrink, every one of them wraps inside
|
|
44
|
+
its own box (a chip becomes two lines, "存取模式:" and "設定" break mid-word) and the tail is clipped by
|
|
45
|
+
the card. So: no fact wraps inside itself; when the row is too long it wraps between facts, and the
|
|
46
|
+
status/settings cluster stays together at the end. Long values (env names, user ids) may break
|
|
47
|
+
anywhere rather than push the row wider than its card. */
|
|
48
|
+
.conn-row { flex-wrap: wrap; row-gap: 6px; }
|
|
49
|
+
.conn-row > * { flex: 0 0 auto; max-width: 100%; }
|
|
50
|
+
.conn-row .name, .conn-row .tag, .conn-row button, .conn-row .row-end > * { white-space: nowrap; }
|
|
51
|
+
.conn-row .tag { overflow: hidden; text-overflow: ellipsis; }
|
|
52
|
+
.conn-row .sub { flex: 0 1 auto; min-width: 0; overflow-wrap: anywhere; }
|
|
53
|
+
.conn-row .row-end { display: inline-flex; align-items: center; gap: 10px; margin-left: auto; }
|
|
42
54
|
button { font-family: inherit; font-size: 13px; border-radius: var(--radius-sm); padding: 6px 12px; border: 1px solid var(--border); background: #fff; color: var(--fg); cursor: pointer; }
|
|
43
55
|
button:hover { background: #f3f4f6; }
|
|
44
56
|
button.primary { background: var(--accent); color: #fff; border-color: var(--accent); font-weight: 600; }
|
|
@@ -252,8 +264,8 @@
|
|
|
252
264
|
runtimeResources: "Runtime & Resources", autoPause: "Auto-pause after", minutes: "minutes", inheritDefault: "Inherit from defaults",
|
|
253
265
|
statusEmojis: "Status emojis", statusEmojisInstance: "This agent's status emojis (override the connection's)", statusEmojisChannel: "Status emojis for this connection's bots",
|
|
254
266
|
se_received: "Received", se_queued: "Queued", se_processing: "Processing", se_delivered: "Delivered", se_failed: "Failed", se_progress_prefix: "Progress prefix",
|
|
255
|
-
se_src_instance: "agent", se_src_platform: "connection", se_pick: "Pick…", se_useDefault: "Use the default", se_fallback: "the default is used instead",
|
|
256
|
-
se_unicode: "Emoji", se_telegramOnly: "Telegram allows only these reactions", se_serverEmojis: "
|
|
267
|
+
se_src_instance: "agent", se_src_platform: "connection", se_src_builtin: "default", se_pick: "Pick…", se_useDefault: "Use the default", se_fallback: "the default is used instead",
|
|
268
|
+
se_unicode: "Emoji", se_telegramOnly: "Telegram allows only these reactions", se_serverEmojis: "Server emojis", se_noServerEmojis: "No custom emojis on this server.", se_primaryServer: "primary", se_externalEmojiHint: "Another server's emoji needs the bot's Use External Emojis permission in the channel it reacts in.", se_unavailable: "unavailable",
|
|
257
269
|
se_hint: "Empty keeps the default. The preview is resolved by AgEnD exactly as the bot will react.",
|
|
258
270
|
detection: "Detection", startup: "Startup", backendSection: "Backend", advancedSection: "Advanced & Access", hangEnabled: "Hang detector enabled", hangTimeout: "Stuck timeout", agentMode: "Agent mode", toolSet: "Tool set", toolProgress: "Tool progress", replyCompletionGuard: "Require a delivered reply", replyGuardSupported: "Active for Claude Code in MCP mode.", replyGuardUnsupported: "Stored but inactive: {0} in {1} mode does not support reply-drop recovery.", logLevel: "Log level", concurrency: "Startup concurrency", staggerDelay: "Stagger delay", lightweight: "Lightweight mode", modelFailover: "Fallback model", displayName: "Display name", pause: "Pause", wake: "Wake", accessMode: "Access mode", affected: "Affected instances",
|
|
259
271
|
language: "Language (bot replies)", defaultBackend: "Default backend", defaultModel: "Default model", allowedUsers: "Allowed users (primary bot)",
|
|
@@ -289,7 +301,10 @@
|
|
|
289
301
|
applyRestartHint: "This change needs AgEnD itself restarted. You can restart it here, or run /restart in your chat channel.",
|
|
290
302
|
applyBusy: "AgEnD is already reloading its configuration. Your changes are saved — try Apply again in a moment.",
|
|
291
303
|
restartFleetButton: "Restart AgEnD", restartFleetConfirm: "Restart AgEnD now?\n\nEvery agent stops and starts again, work in progress is interrupted, and the panel is unreachable for up to a minute. A notice is posted to your chat channel.",
|
|
292
|
-
restartFleetStarted: "Restarting AgEnD — this page keeps checking.",
|
|
304
|
+
restartFleetStarted: "Restarting AgEnD — this page keeps checking.",
|
|
305
|
+
restartFleetBusy: "Restarting…", restartFleetTitle: "Restarting AgEnD…",
|
|
306
|
+
restartFleetWaiting: "AgEnD is restarting. This page keeps checking and will say when it is back — there is nothing to press.",
|
|
307
|
+
working: "Working…", restartFleetRefused: "Restart refused: {0}",
|
|
293
308
|
restartFleetRateLimited: "Restart refused: {0} (try again in about {1} minutes)",
|
|
294
309
|
signatureMismatch: "fleet.yaml and the running configuration disagree on: {0}. Restarting will not clear this — check fleet.log first.",
|
|
295
310
|
applyStillWorking: "Still restarting ({0}s) — AgEnD is still working; this page keeps checking.",
|
|
@@ -301,8 +316,8 @@
|
|
|
301
316
|
"zh-TW": {
|
|
302
317
|
statusEmojis: "狀態 emoji", statusEmojisInstance: "此 Agent 的狀態 emoji(覆蓋連線設定)", statusEmojisChannel: "此連線 bot 的狀態 emoji",
|
|
303
318
|
se_received: "已收到", se_queued: "排隊中", se_processing: "處理中", se_delivered: "已送達", se_failed: "失敗", se_progress_prefix: "進度前綴",
|
|
304
|
-
se_src_instance: "Agent", se_src_platform: "連線", se_pick: "選擇…", se_useDefault: "使用預設", se_fallback: "改用預設值",
|
|
305
|
-
se_unicode: "Emoji", se_telegramOnly: "Telegram 只允許這些反應", se_serverEmojis: "
|
|
319
|
+
se_src_instance: "Agent", se_src_platform: "連線", se_src_builtin: "預設", se_pick: "選擇…", se_useDefault: "使用預設", se_fallback: "改用預設值",
|
|
320
|
+
se_unicode: "Emoji", se_telegramOnly: "Telegram 只允許這些反應", se_serverEmojis: "伺服器 emoji", se_noServerEmojis: "此伺服器沒有自訂 emoji。", se_primaryServer: "主伺服器", se_externalEmojiHint: "使用其他伺服器的 emoji,bot 在要加反應的頻道需有「使用外部表情符號」權限。", se_unavailable: "無法使用",
|
|
306
321
|
se_hint: "留空即使用預設值。預覽由 AgEnD 依 bot 實際 react 的方式解析。",
|
|
307
322
|
title: "⚙️ AgEnD 設定", reloadFleet: "↻ 重新載入", myAgents: "🤖 我的 Agent", bots: "🔌 機器人", classicChannels: "🎮 ClassicBot 頻道", noClassic: "沒有 ClassicBot 頻道 — 在聊天中用 /start 建立。",
|
|
308
323
|
general: "⚙️ 一般設定", whatsNew: "🆕 更新重點", advanced: "📋 進階",
|
|
@@ -345,7 +360,10 @@
|
|
|
345
360
|
applyRestartHint: "此變更需要重新啟動 AgEnD 本身。可以在這裡重新啟動,或在聊天頻道執行 /restart。",
|
|
346
361
|
applyBusy: "AgEnD 正在重新載入設定。你的變更已儲存,請稍候再按一次套用。",
|
|
347
362
|
restartFleetButton: "重新啟動 AgEnD", restartFleetConfirm: "現在重新啟動 AgEnD?\n\n所有 agent 都會停止再啟動,進行中的工作會被中斷,面板會有最多約一分鐘無法連線。聊天頻道會收到一則通知。",
|
|
348
|
-
restartFleetStarted: "AgEnD 重新啟動中 —— 此頁面會持續確認。",
|
|
363
|
+
restartFleetStarted: "AgEnD 重新啟動中 —— 此頁面會持續確認。",
|
|
364
|
+
restartFleetBusy: "重啟中…", restartFleetTitle: "AgEnD 重新啟動中…",
|
|
365
|
+
restartFleetWaiting: "AgEnD 正在重新啟動。此頁面會持續確認,恢復後會告訴你 —— 不需要再按任何按鈕。",
|
|
366
|
+
working: "執行中…", restartFleetRefused: "重新啟動被拒絕:{0}",
|
|
349
367
|
restartFleetRateLimited: "重新啟動被拒絕:{0}(約 {1} 分鐘後可再試)",
|
|
350
368
|
signatureMismatch: "fleet.yaml 與執行中的設定在這些項目上不一致:{0}。重新啟動無法解決,請先查看 fleet.log。",
|
|
351
369
|
applyStillWorking: "仍在重啟中({0} 秒)—— AgEnD 還在處理,此頁面會持續確認。",
|
|
@@ -525,7 +543,9 @@
|
|
|
525
543
|
for (const entry of r.body.entries) {
|
|
526
544
|
preview.append(el("span", { class: "se-item", title: `${entry.key}: ${entry.value}` }, emojiGlyph(entry),
|
|
527
545
|
el("span", {}, t(`se_${entry.key}`)),
|
|
528
|
-
|
|
546
|
+
// Say where every value comes from, defaults included: 👀 is the built-in for three statuses, and
|
|
547
|
+
// an untagged 👀 left under "Processing" after Received is changed reads as if it had moved there.
|
|
548
|
+
el("span", { class: "tag" + (entry.source === "builtin" ? " se-default" : "") }, t(`se_src_${entry.source}`))));
|
|
529
549
|
}
|
|
530
550
|
problems.className = r.body.problems.length ? "feedback error" : "feedback";
|
|
531
551
|
problems.textContent = r.body.problems.map(p => `⚠ ${t(`se_${p.key}`)}: ${p.problem} — ${t("se_fallback")}`).join(" ");
|
|
@@ -549,15 +569,27 @@
|
|
|
549
569
|
const list = await fetchGuildEmojis(channelId, refreshList);
|
|
550
570
|
guildBox.innerHTML = "";
|
|
551
571
|
if (list.error) { guildBox.append(el("div", { class: "feedback error" }, `✗ ${list.error}`)); return; }
|
|
552
|
-
|
|
553
|
-
//
|
|
554
|
-
|
|
555
|
-
//
|
|
556
|
-
guildBox.append(
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
572
|
+
// One group per server the bot can draw on (#1021), primary first;
|
|
573
|
+
// a bot reacts with a custom emoji from any server it is in.
|
|
574
|
+
const groups = list.guilds;
|
|
575
|
+
// Discord checks Use External Emojis in the channel being reacted in.
|
|
576
|
+
if (groups.length > 1) guildBox.append(el("div", { class: "hint" }, t("se_externalEmojiHint")));
|
|
577
|
+
for (const g of groups) {
|
|
578
|
+
if (groups.length > 1) {
|
|
579
|
+
guildBox.append(el("div", { class: "hint se-guild" }, g.name || g.id,
|
|
580
|
+
...(g.primary ? [" ", el("span", { class: "tag" }, t("se_primaryServer"))] : [])));
|
|
581
|
+
}
|
|
582
|
+
if (g.error) { guildBox.append(el("div", { class: "feedback error" }, `✗ ${g.error}`)); continue; }
|
|
583
|
+
if (!g.emojis.length) { guildBox.append(el("div", { class: "hint" }, t("se_noServerEmojis"))); continue; }
|
|
584
|
+
// An unavailable emoji (e.g. the server lost the Boost tier it
|
|
585
|
+
// needs) cannot be reacted with; picking it would preview fine and
|
|
586
|
+
// then fail silently on every status. Shown, but not pickable.
|
|
587
|
+
guildBox.append(grid(g.emojis.map(e => el("button", {
|
|
588
|
+
type: "button", class: "sm" + (e.available ? "" : " off"), title: `:${e.name}:${e.available ? "" : ` (${t("se_unavailable")})`}`,
|
|
589
|
+
...(e.available ? {} : { disabled: "disabled", "aria-disabled": "true" }),
|
|
590
|
+
onclick: () => { if (e.available) choose(e.value); },
|
|
591
|
+
}, emojiImg(e.image_url, `:${e.name}:`)))));
|
|
592
|
+
}
|
|
561
593
|
};
|
|
562
594
|
picker.append(el("div", { class: "hint", style: "display:flex;gap:8px;align-items:center" }, t("se_serverEmojis"),
|
|
563
595
|
el("button", { type: "button", class: "sm ghost", onclick: () => load(true) }, "↻")), guildBox);
|
|
@@ -881,7 +913,8 @@
|
|
|
881
913
|
: row.status === "running" ? "…" : "·";
|
|
882
914
|
// restart-required is terminal and is NOT success: the value is saved and a
|
|
883
915
|
// running AgEnD cannot adopt it.
|
|
884
|
-
const note = row.status === "
|
|
916
|
+
const note = row.target === "fleet" && row.status === "running" ? t("restartFleetTitle")
|
|
917
|
+
: row.status === "restart-required" ? t("applyRestartNeeded")
|
|
885
918
|
: row.settled_by === "no-change" ? t("applyNoChange")
|
|
886
919
|
: row.settled_by === "fleet-restart" ? "(restart)"
|
|
887
920
|
: row.kind === "hot" ? t("impactNow") : t("impactAgent");
|
|
@@ -892,17 +925,31 @@
|
|
|
892
925
|
row.error ? el("span", { class: "err" }, row.error) : null);
|
|
893
926
|
}
|
|
894
927
|
|
|
895
|
-
|
|
928
|
+
// One restart at a time, and a button that says so. Without this the panel re-drew a fresh, enabled
|
|
929
|
+
// "Restart AgEnD" button the instant the request was accepted, so a second press asked again.
|
|
930
|
+
let restartingFleet = false;
|
|
931
|
+
async function restartFleet(job, button) {
|
|
932
|
+
if (restartingFleet) return;
|
|
896
933
|
// Its own confirmation, its own key. A single Apply must never be able to
|
|
897
934
|
// carry a full-fleet restart along with it.
|
|
898
935
|
if (!confirm(t("restartFleetConfirm"))) return;
|
|
936
|
+
restartingFleet = true;
|
|
937
|
+
const label = button?.textContent;
|
|
938
|
+
if (button) { button.disabled = true; button.textContent = t("restartFleetBusy"); }
|
|
899
939
|
const key = (crypto.randomUUID ? crypto.randomUUID() : `r-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
940
|
+
let res;
|
|
941
|
+
try {
|
|
942
|
+
res = await api("/api/settings/restart-fleet", {
|
|
943
|
+
method: "POST",
|
|
944
|
+
headers: { "Idempotency-Key": key },
|
|
945
|
+
body: JSON.stringify({ job_id: job.id, confirm: "restart-agend", idempotency_key: key }),
|
|
946
|
+
});
|
|
947
|
+
} catch {
|
|
948
|
+
res = { ok: false, status: 0, body: null };
|
|
949
|
+
}
|
|
905
950
|
if (!res.ok) {
|
|
951
|
+
restartingFleet = false;
|
|
952
|
+
if (button?.isConnected) { button.disabled = false; button.textContent = label; }
|
|
906
953
|
const secs = res.body?.retry_after_seconds;
|
|
907
954
|
showBanner(secs
|
|
908
955
|
? tf("restartFleetRateLimited", res.body?.error || "", Math.ceil(secs / 60))
|
|
@@ -910,17 +957,32 @@
|
|
|
910
957
|
return;
|
|
911
958
|
}
|
|
912
959
|
showBanner(t("restartFleetStarted"));
|
|
913
|
-
|
|
960
|
+
// The server has moved this job's fleet row to "running" (and will settle it after the restart). Watch
|
|
961
|
+
// *that* job: the one in hand still says "restart-required" and is finished, so watching it ended at once.
|
|
962
|
+
const fresh = await api(`/api/settings/apply/${encodeURIComponent(job.id)}`).catch(() => null);
|
|
963
|
+
const watching = fresh?.ok && fresh.body?.id ? fresh.body : {
|
|
964
|
+
...job, status: "running",
|
|
965
|
+
targets: job.targets.map(row => row.target === "fleet" ? { ...row, status: "running" } : row),
|
|
966
|
+
};
|
|
967
|
+
try {
|
|
968
|
+
await watchApplyJob(watching);
|
|
969
|
+
await reload();
|
|
970
|
+
} finally {
|
|
971
|
+
restartingFleet = false;
|
|
972
|
+
}
|
|
914
973
|
}
|
|
915
974
|
|
|
916
975
|
function renderApplyJob(job) {
|
|
917
976
|
$("applyPanel").classList.add("open");
|
|
918
|
-
|
|
977
|
+
const fleetRestarting = job.status === "running" && job.targets.some(row => row.target === "fleet" && row.status === "running");
|
|
978
|
+
$("applyTitle").textContent = fleetRestarting ? t("restartFleetTitle")
|
|
979
|
+
: job.status === "done"
|
|
919
980
|
? (job.targets.some(r => r.status === "failed") ? t("applyPartial") : t("applyDone"))
|
|
920
981
|
: job.status === "failed" ? t("applyPartial") : t("applying");
|
|
921
982
|
const needsRestart = job.targets.some(row => row.status === "restart-required");
|
|
922
983
|
const mismatch = state.schema.fleet_signature_mismatch;
|
|
923
|
-
$("applyNote").textContent =
|
|
984
|
+
$("applyNote").textContent = fleetRestarting ? t("restartFleetWaiting")
|
|
985
|
+
: job.overdue
|
|
924
986
|
? tf("applyStillWorking", Math.round(job.elapsed_ms / 1000))
|
|
925
987
|
: mismatch && needsRestart ? tf("signatureMismatch", mismatch.join(", "))
|
|
926
988
|
: needsRestart ? t("applyRestartHint") : "";
|
|
@@ -928,8 +990,11 @@
|
|
|
928
990
|
for (const row of job.targets) rows.append(applyRow(row));
|
|
929
991
|
// A restart that cannot clear the mismatch is worse than no button at all.
|
|
930
992
|
if (needsRestart && !mismatch) {
|
|
931
|
-
|
|
932
|
-
|
|
993
|
+
const restartButton = el("button", { class: "primary", onclick: ev => restartFleet(job, ev.currentTarget) },
|
|
994
|
+
restartingFleet ? t("restartFleetBusy") : t("restartFleetButton"));
|
|
995
|
+
// While a restart is under way this stays, disabled and saying so, if anything re-draws the panel.
|
|
996
|
+
if (restartingFleet) restartButton.disabled = true;
|
|
997
|
+
rows.append(el("div", { class: "apply-row" }, restartButton));
|
|
933
998
|
}
|
|
934
999
|
if (job.error) rows.append(el("div", { class: "apply-row err" }, job.error));
|
|
935
1000
|
}
|
|
@@ -944,7 +1009,10 @@
|
|
|
944
1009
|
let current = job;
|
|
945
1010
|
while (current.status === "running" && Date.now() < deadline) {
|
|
946
1011
|
await new Promise(r => setTimeout(r, 1000));
|
|
947
|
-
|
|
1012
|
+
// A restart takes the server down under this request: the fetch rejects until AgEnD is back. That is
|
|
1013
|
+
// the expected middle of a restart, not the end of the watch.
|
|
1014
|
+
let res;
|
|
1015
|
+
try { res = await api(`/api/settings/apply/${encodeURIComponent(job.id)}`); } catch { continue; }
|
|
948
1016
|
if (res.status === 404) {
|
|
949
1017
|
// The job outlives the process that made it, so a 404 means it aged out
|
|
950
1018
|
// of the store — not that it never happened.
|
|
@@ -1045,9 +1113,9 @@
|
|
|
1045
1113
|
const form = agentEditForm(name, inst);
|
|
1046
1114
|
openModal({ title: shortName(name, inst), subtitle: "ID: " + name, body: form.box, impacts: form.impacts, stage: form.stage });
|
|
1047
1115
|
} }, t("settingsButton")),
|
|
1048
|
-
running ? el("button", { class: "sm", onclick:
|
|
1049
|
-
paused ? el("button", { class: "sm", onclick:
|
|
1050
|
-
!paused ? el("button", { class: "sm", onclick:
|
|
1116
|
+
running ? el("button", { class: "sm", onclick: ev => pauseWakeAgent(name, "pause", ev.currentTarget) }, t("pause")) : null,
|
|
1117
|
+
paused ? el("button", { class: "sm", onclick: ev => pauseWakeAgent(name, "wake", ev.currentTarget) }, t("wake")) : null,
|
|
1118
|
+
!paused ? el("button", { class: "sm", onclick: ev => toggleAgent(name, running, ev.currentTarget) }, running ? t("stop") : t("start")) : null,
|
|
1051
1119
|
el("details", { class: "more-menu" },
|
|
1052
1120
|
el("summary", { title: "More actions" }, "⋯"),
|
|
1053
1121
|
el("div", { class: "menu" }, el("button", { class: "sm danger", onclick: () => delAgent(name) }, "Delete Agent"))),
|
|
@@ -1242,15 +1310,40 @@
|
|
|
1242
1310
|
"instance.lightweight", "instance.model_failover", "instance.systemPrompt",
|
|
1243
1311
|
"instance.tags", "instance.general_topic", "instance.status_emojis",
|
|
1244
1312
|
];
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
const
|
|
1252
|
-
|
|
1253
|
-
|
|
1313
|
+
// Start, stop, pause and wake take a moment and used to give no sign of it, so they were pressed again.
|
|
1314
|
+
// One action per agent at a time; the row's buttons are disabled and the pressed one says it is working.
|
|
1315
|
+
const busyAgents = new Set();
|
|
1316
|
+
async function runAgentAction(name, button, work) {
|
|
1317
|
+
if (busyAgents.has(name)) return;
|
|
1318
|
+
busyAgents.add(name);
|
|
1319
|
+
const row = button?.closest(".item");
|
|
1320
|
+
const buttons = row ? [...row.querySelectorAll("button")] : (button ? [button] : []);
|
|
1321
|
+
const label = button?.textContent;
|
|
1322
|
+
for (const b of buttons) b.disabled = true;
|
|
1323
|
+
if (button) button.textContent = t("working");
|
|
1324
|
+
try { await work(); }
|
|
1325
|
+
finally {
|
|
1326
|
+
busyAgents.delete(name);
|
|
1327
|
+
// The list is normally redrawn by then; if it was not (a failed reload), give the buttons back.
|
|
1328
|
+
for (const b of buttons) if (b.isConnected) b.disabled = false;
|
|
1329
|
+
if (button?.isConnected) button.textContent = label;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
function toggleAgent(name, running, button) {
|
|
1333
|
+
return runAgentAction(name, button, async () => {
|
|
1334
|
+
const res = running ? await api(`/stop/${encodeURIComponent(name)}`, { method: "POST" })
|
|
1335
|
+
: await api(`/api/instance/${encodeURIComponent(name)}/start`, { method: "POST" });
|
|
1336
|
+
if (!res.ok) showBanner(`${tf("actionFailed", running ? t("stop") : t("start"))}: ${res.body?.error || res.status}`, true);
|
|
1337
|
+
await new Promise(r => setTimeout(r, 800));
|
|
1338
|
+
await reloadLive();
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
function pauseWakeAgent(name, action, button) {
|
|
1342
|
+
return runAgentAction(name, button, async () => {
|
|
1343
|
+
const res = await api(`/api/settings/instances/${encodeURIComponent(name)}/${action}`, { method: "POST" });
|
|
1344
|
+
if (!res.ok) alert(res.body?.error || tf("actionFailed", action));
|
|
1345
|
+
await reloadLive();
|
|
1346
|
+
});
|
|
1254
1347
|
}
|
|
1255
1348
|
async function delAgent(name) {
|
|
1256
1349
|
if (!confirm(tf("deleteAgent", name))) return;
|
|
@@ -1295,7 +1388,7 @@
|
|
|
1295
1388
|
chs.forEach((ch, i) => {
|
|
1296
1389
|
const item = el("div", { class: "item" });
|
|
1297
1390
|
const type = ch.type || "?";
|
|
1298
|
-
item.append(el("div", { class: "item-row" },
|
|
1391
|
+
item.append(el("div", { class: "item-row conn-row" },
|
|
1299
1392
|
el("span", { class: "name" }, type === "telegram" ? "Telegram" : "Discord"),
|
|
1300
1393
|
el("span", { class: "tag" + (i === 0 ? "" : " persona") + (type === "telegram" ? " tg" : "") }, ch.id || chLabel(i)),
|
|
1301
1394
|
el("span", { class: "sub", style: "margin:0" }, ch.bot_token_env || "(no token env)"),
|
|
@@ -1305,13 +1398,14 @@
|
|
|
1305
1398
|
(ch.access?.allowed_users || []).length
|
|
1306
1399
|
? (ch.access.allowed_users).slice(0, 3).join(", ") + ((ch.access.allowed_users).length > 3 ? ` +${(ch.access.allowed_users).length - 3}` : "")
|
|
1307
1400
|
: t("noAllowedUsers")),
|
|
1308
|
-
el("span", { class: "
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1401
|
+
el("span", { class: "row-end" },
|
|
1402
|
+
el("span", { class: "tag", title: "secret status" }, state.connections.find(c => (c.id || c.type) === (ch.id || ch.type))?.token_present ? "Token configured" : "Token missing"),
|
|
1403
|
+
el("span", { style: `color:${state.fleetUp ? "var(--green)" : "var(--red)"};font-size:13px` }, state.fleetUp ? "Connected" : "Problem"),
|
|
1404
|
+
el("button", { class: "sm ghost", onclick: () => {
|
|
1405
|
+
const form = botEditForm(ch, i);
|
|
1406
|
+
openModal({ title: type === "telegram" ? "Telegram" : "Discord", subtitle: ch.id || chLabel(i), body: form.box, impacts: form.impacts, stage: form.stage });
|
|
1407
|
+
} }, t("settingsButton")),
|
|
1408
|
+
),
|
|
1315
1409
|
));
|
|
1316
1410
|
host.append(item);
|
|
1317
1411
|
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 2b (docs/design/phase2-submit-contract.md §1.2–§1.6, §3.1): the one
|
|
3
|
+
* owner that wakes a paused target because durable work is waiting for it.
|
|
4
|
+
*
|
|
5
|
+
* Before this, the only code that woke a target for a queued cross-instance
|
|
6
|
+
* message ran *after* the outbox pump had claimed the row, and the pump never
|
|
7
|
+
* claims a target without a live daemon — so a marker-only paused instance
|
|
8
|
+
* (paused across a fleet restart) was never woken: Bug1, the attempt_0
|
|
9
|
+
* deadlock. The coordinator wakes first; the pump stays the only claimer and
|
|
10
|
+
* only claims targets that are awake. Nothing here writes a pane or claims a
|
|
11
|
+
* row.
|
|
12
|
+
*
|
|
13
|
+
* Active only for targets whose `delivery_worker` is `wake_only` or `on`;
|
|
14
|
+
* `off` (the default) leaves every path as it was.
|
|
15
|
+
*/
|
|
16
|
+
import type { DeliveryWorkerMode } from "./types.js";
|
|
17
|
+
import type { OutboxDelivery } from "./delivery-outbox.js";
|
|
18
|
+
import type { PauseReason } from "./pause-marker.js";
|
|
19
|
+
/** Row kinds that never warrant waking a target. Empty: every current kind asks for action (§1.2). */
|
|
20
|
+
export declare const PASSIVE_KINDS: ReadonlySet<string>;
|
|
21
|
+
export declare const isWakeEligibleKind: (kind: string) => boolean;
|
|
22
|
+
export declare const WAKE_BACKOFF_BASE_MS = 1000;
|
|
23
|
+
export declare const WAKE_BACKOFF_MAX_MS = 60000;
|
|
24
|
+
/** Consecutive wake failures before both topics are told, once per failure episode. */
|
|
25
|
+
export declare const WAKE_FAILURE_NOTICE_AFTER = 3;
|
|
26
|
+
/** Pre-claim lease length (mirrors instance-lifecycle PRECLAIM_LEASE_MS). */
|
|
27
|
+
export declare const PRECLAIM_WINDOW_MS = 120000;
|
|
28
|
+
export declare const PARK_BASE_MS = 60000;
|
|
29
|
+
export declare const PARK_MAX_MS: number;
|
|
30
|
+
/** Queued work on an awake target that has not been claimed for this long is reported once (§1.4). */
|
|
31
|
+
export declare const CLAIM_BLOCK_NOTICE_MS: number;
|
|
32
|
+
/** How far `warm_cap` may be exceeded for leased work (§1.6). */
|
|
33
|
+
export declare const DEFAULT_WARM_OVERFLOW = 2;
|
|
34
|
+
export declare const WATCHDOG_INTERVAL_MS = 30000;
|
|
35
|
+
export interface WakeCoordinatorDeps {
|
|
36
|
+
mode(target: string): DeliveryWorkerMode;
|
|
37
|
+
/**
|
|
38
|
+
* Whether the outbox can be read now. False once its database is closed
|
|
39
|
+
* (fleet shutdown, a test tearing down): the coordinator then does nothing,
|
|
40
|
+
* and stops itself, instead of querying a closed connection.
|
|
41
|
+
*/
|
|
42
|
+
available(): boolean;
|
|
43
|
+
/** Rows still to deliver: queued, retry_wait, delivering, submission_started (created_seq order). */
|
|
44
|
+
listPending(): OutboxDelivery[];
|
|
45
|
+
isPaused(target: string): boolean;
|
|
46
|
+
pauseReason(target: string): PauseReason | null;
|
|
47
|
+
isRestarting(target: string): boolean;
|
|
48
|
+
/** The single-flight lifecycle wake, marked as the coordinator's own. */
|
|
49
|
+
wake(target: string): Promise<void>;
|
|
50
|
+
/** Instances counted against warm_cap right now (running, not paused, not general). */
|
|
51
|
+
residentNames(): string[];
|
|
52
|
+
/** warm_cap; 0 or less means unlimited. */
|
|
53
|
+
warmCap(): number;
|
|
54
|
+
warmOverflow(): number;
|
|
55
|
+
/** When the target was last woken or started for work (the pre-claim clock). */
|
|
56
|
+
wokeAt(target: string): number | undefined;
|
|
57
|
+
/** Why an awake target is not accepting input (dialog, auth hold…), or null. */
|
|
58
|
+
notAcceptingReason(target: string): string | null;
|
|
59
|
+
notifyTarget(target: string, text: string): void;
|
|
60
|
+
/**
|
|
61
|
+
* Run the outbox pump. A wake changes no outbox row, so nothing else would
|
|
62
|
+
* tell the pump that the target it skipped while paused can be claimed now.
|
|
63
|
+
*/
|
|
64
|
+
kickPump(): void;
|
|
65
|
+
notifySender(source: string, text: string): void;
|
|
66
|
+
now(): number;
|
|
67
|
+
logger: {
|
|
68
|
+
info(obj: object, msg: string): void;
|
|
69
|
+
warn(obj: object, msg: string): void;
|
|
70
|
+
debug?(obj: object, msg: string): void;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export declare class WakeCoordinator {
|
|
74
|
+
private readonly deps;
|
|
75
|
+
private readonly states;
|
|
76
|
+
/** Targets whose wake is in flight (a reservation each, counted against the hard cap). */
|
|
77
|
+
private readonly reserved;
|
|
78
|
+
private scanTimer;
|
|
79
|
+
private watchdog;
|
|
80
|
+
private stopped;
|
|
81
|
+
constructor(deps: WakeCoordinatorDeps);
|
|
82
|
+
start(): void;
|
|
83
|
+
stop(): void;
|
|
84
|
+
/** Schedule a scan soon (coalesced). Safe to call from any event. */
|
|
85
|
+
kick(delayMs?: number): void;
|
|
86
|
+
private state;
|
|
87
|
+
private active;
|
|
88
|
+
/**
|
|
89
|
+
* The pump must not claim this target (§3.1, §1.4): it is paused, being
|
|
90
|
+
* woken, or awake but not accepting input by the daemon's own account
|
|
91
|
+
* (a dialog, a login failure, stuck). Called from the pump's claim
|
|
92
|
+
* callback, in the same synchronous step that reads the daemon's bootId, so
|
|
93
|
+
* the verdict and the claim are about the same daemon generation. A busy
|
|
94
|
+
* (working) target is accepting: the manager's idle gate still waits it out.
|
|
95
|
+
*/
|
|
96
|
+
blocksClaim(target: string): boolean;
|
|
97
|
+
isWaking(target: string): boolean;
|
|
98
|
+
/** The last wake error, while the target is failing to wake (for the expiry reason). */
|
|
99
|
+
wakeFailure(target: string): string | null;
|
|
100
|
+
isParked(target: string): boolean;
|
|
101
|
+
/**
|
|
102
|
+
* A wake that did not come from here (an operator's /wake or wake_instance,
|
|
103
|
+
* a user's channel message) *succeeded*. Park and backoff block only
|
|
104
|
+
* automatic wakes, so such a wake always gets its one attempt (§1.6); only
|
|
105
|
+
* its success lifts them. A failed attempt, or a refusal for capacity,
|
|
106
|
+
* leaves the park and backoff exactly as they were. If the hold persists
|
|
107
|
+
* after a successful wake, the park comes back on its next window with the
|
|
108
|
+
* level kept (it is reset only by a delivery).
|
|
109
|
+
*/
|
|
110
|
+
noteExternalWake(target: string): void;
|
|
111
|
+
/** A row for `target` was delivered: the hold is over, and so are the backoffs. */
|
|
112
|
+
noteDelivered(target: string): void;
|
|
113
|
+
/** Free warm slots under the hard cap, counting every target exactly once. */
|
|
114
|
+
private freeSlots;
|
|
115
|
+
/** Synchronously take a slot for `target` (before any await). */
|
|
116
|
+
tryReserve(target: string): symbol | null;
|
|
117
|
+
/** Return exactly the slot `token` holds (idempotent). */
|
|
118
|
+
release(target: string, token: symbol): void;
|
|
119
|
+
get reservedCount(): number;
|
|
120
|
+
/**
|
|
121
|
+
* One pass over the pending queue: wake what needs waking, park what will
|
|
122
|
+
* not take work. Runs from timers, so it must never throw: an exception
|
|
123
|
+
* there is uncaught and takes the whole fleet process down (e.g. a scan
|
|
124
|
+
* landing after the outbox's database was closed). The next kick or the
|
|
125
|
+
* watchdog retries.
|
|
126
|
+
*/
|
|
127
|
+
scan(): void;
|
|
128
|
+
private scanOnce;
|
|
129
|
+
/**
|
|
130
|
+
* An awake target with queued work: past its pre-claim window without the
|
|
131
|
+
* work moving, it is parked (no automatic re-wake until the hold clears or
|
|
132
|
+
* the park expires), and a long pre-claim block is reported once.
|
|
133
|
+
*/
|
|
134
|
+
private checkAwakeTarget;
|
|
135
|
+
/**
|
|
136
|
+
* Whether a wake completion must do nothing but return its slot: the
|
|
137
|
+
* coordinator was stopped, or the outbox went away while the wake was in
|
|
138
|
+
* flight (the coordinator stops itself then). Checked before any side
|
|
139
|
+
* effect, so a late completion can never kick a pump at a closed database.
|
|
140
|
+
*/
|
|
141
|
+
private fencedCompletion;
|
|
142
|
+
private startWake;
|
|
143
|
+
}
|