@songsid/agend 2.2.0-alpha.1 → 2.2.0-alpha.2
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/README.md +2 -0
- package/README.zh-TW.md +2 -0
- package/dist/agent-cli.js +6 -1
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +68 -12
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-token-header.d.ts +13 -0
- package/dist/agent-token-header.js +16 -0
- package/dist/agent-token-header.js.map +1 -0
- package/dist/backend/antigravity.js +4 -3
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/grok.js +1 -1
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro.d.ts +7 -0
- package/dist/backend/kiro.js +82 -33
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/muse.js +1 -1
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +6 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/cache-analysis.d.ts +92 -0
- package/dist/cache-analysis.js +147 -0
- package/dist/cache-analysis.js.map +1 -0
- package/dist/cache-ledger.d.ts +108 -0
- package/dist/cache-ledger.js +230 -0
- package/dist/cache-ledger.js.map +1 -0
- package/dist/cache-prices.d.ts +32 -0
- package/dist/cache-prices.js +73 -0
- package/dist/cache-prices.js.map +1 -0
- package/dist/cache-scan.d.ts +39 -0
- package/dist/cache-scan.js +115 -0
- package/dist/cache-scan.js.map +1 -0
- package/dist/cache-service.d.ts +115 -0
- package/dist/cache-service.js +417 -0
- package/dist/cache-service.js.map +1 -0
- package/dist/cli-entry.d.ts +19 -0
- package/dist/cli-entry.js +36 -0
- package/dist/cli-entry.js.map +1 -0
- package/dist/cli.js +251 -24
- package/dist/cli.js.map +1 -1
- package/dist/daemon.d.ts +6 -0
- package/dist/daemon.js +90 -37
- package/dist/daemon.js.map +1 -1
- package/dist/event-log.d.ts +4 -1
- package/dist/event-log.js +6 -3
- package/dist/event-log.js.map +1 -1
- package/dist/fleet-control-audit.d.ts +5 -0
- package/dist/fleet-control-audit.js +7 -0
- package/dist/fleet-control-audit.js.map +1 -1
- package/dist/fleet-level-config.js +22 -0
- package/dist/fleet-level-config.js.map +1 -1
- package/dist/fleet-manager.d.ts +46 -0
- package/dist/fleet-manager.js +287 -40
- package/dist/fleet-manager.js.map +1 -1
- package/dist/full-restart.js +2 -3
- package/dist/full-restart.js.map +1 -1
- package/dist/general-knowledge/skills/persona-emoji/SKILL.md +3 -1
- package/dist/install-lock.d.ts +49 -0
- package/dist/install-lock.js +200 -0
- package/dist/install-lock.js.map +1 -0
- package/dist/instance-config-impact.js +3 -2
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/locale.js +14 -0
- package/dist/locale.js.map +1 -1
- package/dist/outbound-schemas.js +1 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/package-preimage.d.ts +26 -0
- package/dist/package-preimage.js +96 -0
- package/dist/package-preimage.js.map +1 -0
- package/dist/public-web-gateway.js +2 -2
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/quickstart.js +10 -4
- package/dist/quickstart.js.map +1 -1
- package/dist/restart-guard.d.ts +52 -0
- package/dist/restart-guard.js +109 -0
- package/dist/restart-guard.js.map +1 -0
- package/dist/scheduler/db.js +6 -2
- package/dist/scheduler/db.js.map +1 -1
- package/dist/service-activation.d.ts +47 -5
- package/dist/service-activation.js +156 -51
- package/dist/service-activation.js.map +1 -1
- package/dist/service-installer.d.ts +56 -1
- package/dist/service-installer.js +134 -12
- package/dist/service-installer.js.map +1 -1
- package/dist/service-plan.d.ts +65 -0
- package/dist/service-plan.js +108 -0
- package/dist/service-plan.js.map +1 -0
- package/dist/settings-api.js +46 -5
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +34 -6
- package/dist/settings-change.js.map +1 -1
- package/dist/setup-host.js +2 -3
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-wizard.js +2 -1
- package/dist/setup-wizard.js.map +1 -1
- package/dist/sqlite-open-errors.d.ts +12 -0
- package/dist/sqlite-open-errors.js +13 -0
- package/dist/sqlite-open-errors.js.map +1 -0
- package/dist/tmux-control.d.ts +22 -0
- package/dist/tmux-control.js +89 -5
- package/dist/tmux-control.js.map +1 -1
- package/dist/topic-commands.js +14 -5
- package/dist/topic-commands.js.map +1 -1
- package/dist/ui/chat-commands.js +43 -0
- package/dist/ui/chat-render.js +2 -2
- package/dist/ui/chat-store.js +30 -1
- package/dist/ui/chat-strings.js +22 -2
- package/dist/ui/chat-thread.js +47 -10
- package/dist/ui/fleet-cache.js +168 -0
- package/dist/ui/panel-chat.js +104 -18
- package/dist/ui/panel-fleet.js +26 -12
- package/dist/ui/panel-settings.js +12 -8
- package/dist/ui/preview-panel.js +202 -0
- package/dist/ui/preview.js +7 -3
- package/dist/ui/settings-strings.js +4 -2
- package/dist/ui/shared/app-route.js +1 -1
- package/dist/ui/shared/app-shell.js +6 -4
- package/dist/ui/shared/app.css +147 -3
- package/dist/ui/shared/signin.js +13 -1
- package/dist/ui/shared/ui-icons.js +2 -0
- package/dist/ui/signin.html +2 -1
- package/dist/update-dispatch.d.ts +28 -0
- package/dist/update-dispatch.js +73 -0
- package/dist/update-dispatch.js.map +1 -0
- package/dist/update-install.d.ts +65 -7
- package/dist/update-install.js +126 -26
- package/dist/update-install.js.map +1 -1
- package/dist/web-api.d.ts +14 -0
- package/dist/web-api.js +65 -0
- package/dist/web-api.js.map +1 -1
- package/dist/web-commands.d.ts +77 -0
- package/dist/web-commands.js +81 -0
- package/dist/web-commands.js.map +1 -0
- package/dist/web-shell-routes.d.ts +1 -1
- package/dist/web-shell-routes.js +1 -1
- package/dist/web-shell-routes.js.map +1 -1
- package/launcher/agend +110 -0
- package/launcher/agend-agent +110 -0
- package/launcher/agend-agent.cjs +4 -0
- package/launcher/agend.cjs +4 -0
- package/launcher/install-admission.cjs +105 -0
- package/launcher/launch.cjs +64 -0
- package/launcher/postinstall.cjs +114 -0
- package/launcher/run-in-process.cjs +13 -0
- package/launcher/runtime-platform.cjs +86 -0
- package/launcher/runtime-select.cjs +217 -0
- package/package.json +12 -4
- package/scripts/preinstall-guard.cjs +40 -21
- package/templates/launchd.plist.ejs +2 -2
- package/templates/systemd.service.ejs +1 -1
package/dist/fleet-manager.js
CHANGED
|
@@ -39,6 +39,7 @@ import { STATUS_EMOJI_CONFIG_KEYS, STATUS_EMOJI_KEYS, STATUS_EMOJI_SUGGESTIONS,
|
|
|
39
39
|
import { isProbeableRouteTarget } from "./fleet-context.js";
|
|
40
40
|
import { loadFleetConfig, loadRawFleetConfig, DEFAULT_COST_GUARD, DEFAULT_DAILY_SUMMARY, DEFAULT_INSTANCE_CONFIG } from "./config.js";
|
|
41
41
|
import { EventLog } from "./event-log.js";
|
|
42
|
+
import { classifySqliteOpenError } from "./sqlite-open-errors.js";
|
|
42
43
|
import { AdapterWorld } from "./adapter-world.js";
|
|
43
44
|
import { CostGuard, formatCents } from "./cost-guard.js";
|
|
44
45
|
import { startEventLoopWatch } from "./event-loop-watch.js";
|
|
@@ -129,7 +130,8 @@ import { tightenInstanceDirs } from "./private-dir.js";
|
|
|
129
130
|
import { decideSlash } from "./slash-authz.js";
|
|
130
131
|
import { commandSpec, decideCommand, ruleFor } from "./command-table.js";
|
|
131
132
|
import { runVisibilityCommand } from "./cross-instance-notice.js";
|
|
132
|
-
import {
|
|
133
|
+
import { installedChannel, isPrereleaseVersion, updateNoticeKey } from "./update-check.js";
|
|
134
|
+
import { resolveInstalledAgend, updateCommand } from "./update-dispatch.js";
|
|
133
135
|
import { LoginController, LOGIN_TOKEN_RESEND_PREFIX, POST_LOGIN_RECOVERY_DEADLINE_MS } from "./login-controller.js";
|
|
134
136
|
import { runBeforeDeadline } from "./deadline.js";
|
|
135
137
|
import { LoginWindowLock } from "./login-window-lock.js";
|
|
@@ -156,6 +158,10 @@ import { NeedsYouHub } from "./needs-you-hub.js";
|
|
|
156
158
|
import { WEB_CHAT_NOTICE, WEB_REMOTE_DOCS_URL, claimNotice, hasWebChat, releaseNotice, upgradeNoticesPath } from "./upgrade-notices.js";
|
|
157
159
|
import { GENERAL_PAUSE_ERROR, isGeneralInstance } from "./general-instance.js";
|
|
158
160
|
import { buildOrgChart } from "./web-org.js";
|
|
161
|
+
import { CacheService, WINDOWS } from "./cache-service.js";
|
|
162
|
+
import { runWebCommand } from "./web-commands.js";
|
|
163
|
+
import { claudeProjectKey } from "./backend/claude-code.js";
|
|
164
|
+
import { sharedRolloutIndex } from "./rollout-index.js";
|
|
159
165
|
import { mayUseTool, resolveToolSet, scheduleOpRefusal, toolForIpcType, toolRefusedMessage, } from "./tool-permissions.js";
|
|
160
166
|
import { authorizeSession, decideWebGate, loadOrCreateWebToken, readWebToken } from "./web-auth.js";
|
|
161
167
|
import { bypassesWebGate, handleAuthRequest, serveSigninPage } from "./auth-api.js";
|
|
@@ -1898,11 +1904,16 @@ export class FleetManager {
|
|
|
1898
1904
|
const { spawn } = await import("node:child_process");
|
|
1899
1905
|
// Plain `agend update`: the CLI picks the channel from the version it is about to replace (a beta install
|
|
1900
1906
|
// stays on beta). Deciding here, from the package.json next to THIS code, read a source checkout's 1.22.0 as
|
|
1901
|
-
// "not a beta" and sent a beta install to @latest.
|
|
1902
|
-
const
|
|
1907
|
+
// "not a beta" and sent a beta install to @latest. #1450 C5: the INSTALLED agend, verified, by absolute path.
|
|
1908
|
+
const installed = await resolveInstalledAgend();
|
|
1909
|
+
if (!installed.ok) {
|
|
1910
|
+
this.failUpdateProgress(`/update cannot verify the installed AgEnD (${installed.reason}). Run \`agend update\` from a shell.`);
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1903
1913
|
const origin = `slash /update by ${adapterId}:${data.userId}`;
|
|
1904
1914
|
recordInternalRequest(this.dataDir, "update", origin);
|
|
1905
|
-
const
|
|
1915
|
+
const { command, args } = updateCommand(installed.agend);
|
|
1916
|
+
const child = spawn(command, args, {
|
|
1906
1917
|
detached: true, stdio: "ignore", env: withOrigin(origin),
|
|
1907
1918
|
});
|
|
1908
1919
|
child.once("error", err => this.failUpdateProgress(err.message));
|
|
@@ -8581,6 +8592,17 @@ export class FleetManager {
|
|
|
8581
8592
|
this.writeFleetConfigBackup(source);
|
|
8582
8593
|
const tempPath = `${this.configPath}.tmp-${process.pid}`;
|
|
8583
8594
|
writeFileSync(tempPath, output, "utf-8");
|
|
8595
|
+
// #1056 (Fable's 2.2 audit): the patcher writes leaves, not configs, so a wrong patch can produce a file the
|
|
8596
|
+
// validator — or the next start — refuses (a bare channels[0], a channels list with a null). Load and validate
|
|
8597
|
+
// what is about to replace fleet.yaml; a result that adds an error is not written, and the save fails instead.
|
|
8598
|
+
const refusal = this.savedFleetConfigProblem(tempPath);
|
|
8599
|
+
if (refusal) {
|
|
8600
|
+
try {
|
|
8601
|
+
unlinkSync(tempPath);
|
|
8602
|
+
}
|
|
8603
|
+
catch { /* already gone */ }
|
|
8604
|
+
throw new Error(refusal);
|
|
8605
|
+
}
|
|
8584
8606
|
if (existsSync(this.configPath))
|
|
8585
8607
|
chmodSync(tempPath, statSync(this.configPath).mode);
|
|
8586
8608
|
renameSync(tempPath, this.configPath);
|
|
@@ -8588,6 +8610,27 @@ export class FleetManager {
|
|
|
8588
8610
|
this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
|
|
8589
8611
|
this.logger.info({ path: this.configPath, strippedDefaults: redundantPaths.length }, "Saved fleet config (lossless patch)");
|
|
8590
8612
|
}
|
|
8613
|
+
/** Why the config written to `candidate` must not replace fleet.yaml, or null: it does not load, or it adds errors. */
|
|
8614
|
+
savedFleetConfigProblem(candidate) {
|
|
8615
|
+
let after;
|
|
8616
|
+
try {
|
|
8617
|
+
after = validateFleetConfig(loadFleetConfig(candidate));
|
|
8618
|
+
}
|
|
8619
|
+
catch (err) {
|
|
8620
|
+
return `Refusing to save fleet.yaml: the result would not load (${err.message})`;
|
|
8621
|
+
}
|
|
8622
|
+
let had = new Set();
|
|
8623
|
+
try {
|
|
8624
|
+
if (this.configPath && existsSync(this.configPath)) {
|
|
8625
|
+
had = new Set(validateFleetConfig(loadFleetConfig(this.configPath)).errors.map(e => `${e.path}\u0000${e.message}`));
|
|
8626
|
+
}
|
|
8627
|
+
}
|
|
8628
|
+
catch { /* the current file does not load: every error of the result counts as new */ }
|
|
8629
|
+
const introduced = after.errors.filter(e => !had.has(`${e.path}\u0000${e.message}`));
|
|
8630
|
+
return introduced.length
|
|
8631
|
+
? `Refusing to save fleet.yaml: it would be invalid (${introduced.map(e => `${e.path}: ${e.message}`).join("; ")})`
|
|
8632
|
+
: null;
|
|
8633
|
+
}
|
|
8591
8634
|
/** One-time upgrade migration; invalid YAML is never rewritten. */
|
|
8592
8635
|
slimFleetConfigAtStartup() {
|
|
8593
8636
|
const redundantPaths = collectRedundantInstanceDefaultPaths(this.rawFleetConfig);
|
|
@@ -8646,6 +8689,20 @@ export class FleetManager {
|
|
|
8646
8689
|
}
|
|
8647
8690
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8648
8691
|
if (isRecord(before) && isRecord(after)) {
|
|
8692
|
+
// #1056: a legacy `channel` is being migrated (the caller removed it and kept `channels`). The file has no
|
|
8693
|
+
// `channels` for the leaf patch below to land on — the snapshot's list is only the normalized alias — so a bare
|
|
8694
|
+
// channels[0] holding just the changed field would be written while `channel` is deleted. Move the connection
|
|
8695
|
+
// as it is in the file NOW (a concurrent or manual edit, an option AgEnD does not know) into channels[0]
|
|
8696
|
+
// first; the patch then applies only this save's own changes on top, with the usual identity/order check.
|
|
8697
|
+
if (path.length === 0 && this.rawFleetConfig.channel && !this.rawFleetConfig.channels
|
|
8698
|
+
&& after.channel === undefined && Array.isArray(after.channels) && !document.hasIn(["channels"])) {
|
|
8699
|
+
const fresh = document.getIn(["channel"], true);
|
|
8700
|
+
if (isMap(fresh)) {
|
|
8701
|
+
const list = document.createNode([]);
|
|
8702
|
+
list.items.push(fresh.clone());
|
|
8703
|
+
document.setIn(["channels"], list);
|
|
8704
|
+
}
|
|
8705
|
+
}
|
|
8649
8706
|
const keys = new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
8650
8707
|
for (const key of keys) {
|
|
8651
8708
|
// `channel` is a derived alias when the raw file uses `channels`.
|
|
@@ -8790,6 +8847,21 @@ export class FleetManager {
|
|
|
8790
8847
|
* to a plain add (never worse than the old behaviour).
|
|
8791
8848
|
*/
|
|
8792
8849
|
lastStatusEmoji = new Map();
|
|
8850
|
+
/**
|
|
8851
|
+
* #1056: every status emoji each bot really stamped, per `${adapterId}:${messageId}` (match keys). A connection's
|
|
8852
|
+
* status set can change while AgEnD runs, so "is this a stamp?" cannot be answered from the current set alone: a
|
|
8853
|
+
* bot's earlier stamp in the old emoji must still be plumbing, not an agent's reaction. Unbounded for the same
|
|
8854
|
+
* reason as lastStatusEmoji (a few short strings per stamped message).
|
|
8855
|
+
*/
|
|
8856
|
+
stampedStatus = new Map();
|
|
8857
|
+
noteStamped(adapter, messageId, emoji) {
|
|
8858
|
+
const adapterId = typeof adapter.id === "string" ? adapter.id : "?";
|
|
8859
|
+
const k = `${adapterId}:${messageId}`;
|
|
8860
|
+
let set = this.stampedStatus.get(k);
|
|
8861
|
+
if (!set)
|
|
8862
|
+
this.stampedStatus.set(k, set = new Set());
|
|
8863
|
+
set.add(statusMatchKey(emoji));
|
|
8864
|
+
}
|
|
8793
8865
|
/**
|
|
8794
8866
|
* One in-flight status update per bot+message: updates run strictly in call
|
|
8795
8867
|
* order, so a delayed ❌ add can never land after a newer ✅'s removal —
|
|
@@ -8812,7 +8884,7 @@ export class FleetManager {
|
|
|
8812
8884
|
*/
|
|
8813
8885
|
resolveStatusEmojisFor(instanceName, adapterId, adapter) {
|
|
8814
8886
|
const worldId = adapterId ?? this.getInstanceAdapterId(instanceName);
|
|
8815
|
-
const channel =
|
|
8887
|
+
const channel = this.statusEmojiChannel(worldId);
|
|
8816
8888
|
// The adapter that will react decides the vocabulary: a Telegram bot gets
|
|
8817
8889
|
// Telegram's reaction set even if the channel lookup fell back elsewhere.
|
|
8818
8890
|
const reacting = adapter ?? (worldId ? this.worlds.get(worldId)?.adapter : undefined) ?? this.adapter;
|
|
@@ -8833,6 +8905,22 @@ export class FleetManager {
|
|
|
8833
8905
|
return { ...resolved, platform };
|
|
8834
8906
|
}
|
|
8835
8907
|
warnedStatusEmojis = new Set();
|
|
8908
|
+
/**
|
|
8909
|
+
* The connection whose `options.status_emojis` an instance on `worldId` stamps with. #1056: read from the live config
|
|
8910
|
+
* first — a Settings save replaces `fleetConfig.channels`, and the running world keeps the object it started with —
|
|
8911
|
+
* so a change to the connection's emojis applies at the next stamp, as an instance's own override does. Only the
|
|
8912
|
+
* emoji options are taken from it: the platform is still the running adapter's (resolveStatusEmojisFor).
|
|
8913
|
+
*/
|
|
8914
|
+
statusEmojiChannel(worldId) {
|
|
8915
|
+
const started = (worldId ? this.worlds.get(worldId)?.channelConfig : undefined) ?? this.getChannelConfig(worldId ?? undefined);
|
|
8916
|
+
if (!worldId)
|
|
8917
|
+
return started;
|
|
8918
|
+
const list = this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : []);
|
|
8919
|
+
const live = list.find(ch => (ch.id ?? ch.type) === worldId);
|
|
8920
|
+
if (!live || !started)
|
|
8921
|
+
return live ?? started;
|
|
8922
|
+
return { ...started, options: { ...(started.options ?? {}), status_emojis: live.options?.status_emojis } };
|
|
8923
|
+
}
|
|
8836
8924
|
/**
|
|
8837
8925
|
* A Discord connection's server emojis for the Settings picker (#1005), from
|
|
8838
8926
|
* every server its bot can draw on (#1021): the primary `group_id` server,
|
|
@@ -8922,11 +9010,18 @@ export class FleetManager {
|
|
|
8922
9010
|
return { error: this.personaEmojiMissing(instanceName) };
|
|
8923
9011
|
const worldId = this.getInstanceAdapterId(instanceName);
|
|
8924
9012
|
const { platform } = this.resolveStatusEmojisFor(instanceName);
|
|
8925
|
-
const channel =
|
|
9013
|
+
const channel = this.statusEmojiChannel(worldId);
|
|
8926
9014
|
const current = previewStatusEmojis({ platform, platformConfig: channel?.options?.status_emojis, instanceConfig: self?.status_emojis });
|
|
8927
9015
|
const out = {
|
|
8928
9016
|
platform: platform ?? null,
|
|
8929
|
-
|
|
9017
|
+
// #1056: progress_prefix is the emoji at the start of the progress message, not a reaction stamp.
|
|
9018
|
+
statuses: current.entries.map(e => ({ status: e.key, value: e.value, source: e.source, kind: e.key === "progress_prefix" ? "text_prefix" : "reaction" })),
|
|
9019
|
+
// #1056: say inline why the lists differ by platform, so an agent need not know it already.
|
|
9020
|
+
platform_note: platform === "telegram"
|
|
9021
|
+
? "Telegram has no server custom emoji, so there is no server_emojis list: standard.reactions is the complete set a status reaction can use. progress_prefix is message text, not a reaction, so any single emoji works there."
|
|
9022
|
+
: platform === "discord"
|
|
9023
|
+
? "A status reaction can be any single emoji, or a server emoji from server_emojis as <:name:id> (one of the servers this bot is in). progress_prefix is message text, not a reaction."
|
|
9024
|
+
: "This instance has no chat connection, so no platform rules apply yet.",
|
|
8930
9025
|
standard: platform === "telegram"
|
|
8931
9026
|
? { reactions: [...TELEGRAM_REACTION_EMOJIS], note: "Telegram reacts only with these; progress_prefix may be any single emoji" }
|
|
8932
9027
|
: { suggestions: STATUS_EMOJI_SUGGESTIONS, note: "any single emoji works" },
|
|
@@ -9365,6 +9460,9 @@ export class FleetManager {
|
|
|
9365
9460
|
const reactorWorlds = new Set(worlds.filter(w => w.botUserId === r.userId).map(w => w.id));
|
|
9366
9461
|
if (reactorWorlds.size === 0)
|
|
9367
9462
|
return false; // a human, or a bot outside this fleet
|
|
9463
|
+
// #1056: what this bot really stamped on this message, whatever its status set says now.
|
|
9464
|
+
if ([...reactorWorlds].some(id => this.stampedStatus.get(`${id}:${r.messageId}`)?.has(key)))
|
|
9465
|
+
return true;
|
|
9368
9466
|
return [...names]
|
|
9369
9467
|
.filter(n => reactorWorlds.has(this.getInstanceAdapterId(n) ?? ""))
|
|
9370
9468
|
.some(n => statusMatchKeys(this.resolveStatusEmojisFor(n)).includes(key));
|
|
@@ -9372,6 +9470,8 @@ export class FleetManager {
|
|
|
9372
9470
|
// Fallback: the built-in ladder (as before #1005) plus every value an
|
|
9373
9471
|
// operator configured. Not the Telegram built-ins — 👎 is AgEnD's failed
|
|
9374
9472
|
// stamp there, but also far too common a human reaction to swallow blind.
|
|
9473
|
+
if (worlds.some(w => this.stampedStatus.get(`${w.id}:${r.messageId}`)?.has(key)))
|
|
9474
|
+
return true; // #1056, as above
|
|
9375
9475
|
const keys = new Set(statusMatchKeys(builtinStatusEmojis(undefined)));
|
|
9376
9476
|
for (const name of names) {
|
|
9377
9477
|
const resolved = this.resolveStatusEmojisFor(name);
|
|
@@ -9394,9 +9494,15 @@ export class FleetManager {
|
|
|
9394
9494
|
const emoji = this.receivedReactionFor(instanceName, adapter, msg.adapterId);
|
|
9395
9495
|
// Classic's system receipt is status-owned too. An ordinary react call
|
|
9396
9496
|
// would label it as an agent/other reaction and suppress later statuses.
|
|
9497
|
+
// #1056: recorded as this bot's stamp like the delivery statuses (provenance survives a status-set change) —
|
|
9498
|
+
// on Telegram once the adapter took the slot, on Discord before the add (its gateway can echo it first).
|
|
9397
9499
|
if (adapter instanceof TelegramAdapter) {
|
|
9398
|
-
return adapter.reactDeliveryStatus(msg.chatId, msg.messageId, emoji, msg.timestamp.getTime()).then(() => {
|
|
9500
|
+
return adapter.reactDeliveryStatus(msg.chatId, msg.messageId, emoji, msg.timestamp.getTime()).then((took) => {
|
|
9501
|
+
if (took)
|
|
9502
|
+
this.noteStamped(adapter, msg.messageId, emoji);
|
|
9503
|
+
});
|
|
9399
9504
|
}
|
|
9505
|
+
this.noteStamped(adapter, msg.messageId, emoji);
|
|
9400
9506
|
return adapter.react(msg.threadId ?? msg.chatId, msg.messageId, emoji);
|
|
9401
9507
|
}
|
|
9402
9508
|
reactClassicForwardedAttachment(instanceName, adapter, msg, kind) {
|
|
@@ -9482,8 +9588,10 @@ export class FleetManager {
|
|
|
9482
9588
|
if (await adapter.reactDeliveryStatus(chatId, messageId, emoji, receivedAt)) {
|
|
9483
9589
|
if (emoji == null)
|
|
9484
9590
|
this.lastStatusEmoji.delete(key);
|
|
9485
|
-
else
|
|
9591
|
+
else {
|
|
9486
9592
|
this.lastStatusEmoji.set(key, { emoji, status });
|
|
9593
|
+
this.noteStamped(adapter, messageId, emoji);
|
|
9594
|
+
}
|
|
9487
9595
|
}
|
|
9488
9596
|
return;
|
|
9489
9597
|
}
|
|
@@ -9507,6 +9615,8 @@ export class FleetManager {
|
|
|
9507
9615
|
// Best effort: a failed removal must not block the new status.
|
|
9508
9616
|
await adapter.unreact(target, messageId, last.emoji, threadId).catch(e => this.logger.debug({ err: e.message }, "Delivery status reaction removal failed"));
|
|
9509
9617
|
}
|
|
9618
|
+
// Recorded before the add: Discord can report the reaction back before react() resolves.
|
|
9619
|
+
this.noteStamped(adapter, messageId, emoji);
|
|
9510
9620
|
await adapter.react(target, messageId, emoji, threadId);
|
|
9511
9621
|
this.lastStatusEmoji.set(key, { emoji, status });
|
|
9512
9622
|
}
|
|
@@ -9694,13 +9804,28 @@ export class FleetManager {
|
|
|
9694
9804
|
wasDeliveryIdPrunedForCaller(deliveryId, callerInstance) {
|
|
9695
9805
|
return this.deliveryOutbox?.wasDeliveryIdPrunedForCaller(deliveryId, callerInstance) ?? false;
|
|
9696
9806
|
}
|
|
9807
|
+
/** How long opening events.db waits for another process's lock (a field so tests need not wait 5 s). */
|
|
9808
|
+
eventLogBusyTimeoutMs = 5000;
|
|
9697
9809
|
openEventLog() {
|
|
9698
9810
|
const dbPath = join(this.dataDir, "events.db");
|
|
9699
9811
|
try {
|
|
9700
|
-
return new EventLog(dbPath);
|
|
9812
|
+
return new EventLog(dbPath, { busyTimeoutMs: this.eventLogBusyTimeoutMs });
|
|
9701
9813
|
}
|
|
9702
9814
|
catch (err) {
|
|
9703
|
-
|
|
9815
|
+
// #1490: only a file SQLite proved corrupt is moved aside. A lock (it outlasted the busy timeout), a driver that
|
|
9816
|
+
// cannot load (ABI), or a permission/I/O problem says nothing against the file: it stays where it is, history
|
|
9817
|
+
// intact, and the fleet runs without event logging until it restarts.
|
|
9818
|
+
const kind = classifySqliteOpenError(err);
|
|
9819
|
+
if (kind !== "corrupt") {
|
|
9820
|
+
const key = kind === "busy" ? "eventlog.locked" : kind === "abi" ? "eventlog.abi" : "eventlog.unopenable";
|
|
9821
|
+
this.logger.error({ err, dbPath, kind }, `events.db not opened (${kind}) — left in place; continuing without event logging`);
|
|
9822
|
+
try {
|
|
9823
|
+
this.notifyFleetError(t(key));
|
|
9824
|
+
}
|
|
9825
|
+
catch { /* best effort: adapters may not be up yet; the log line stands */ }
|
|
9826
|
+
return null;
|
|
9827
|
+
}
|
|
9828
|
+
this.logger.error({ err, dbPath }, "events.db is corrupt — moving it aside and starting a fresh one");
|
|
9704
9829
|
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
9705
9830
|
for (const suffix of ["", "-wal", "-shm"]) {
|
|
9706
9831
|
try {
|
|
@@ -9709,7 +9834,7 @@ export class FleetManager {
|
|
|
9709
9834
|
catch { /* may not exist */ }
|
|
9710
9835
|
}
|
|
9711
9836
|
try {
|
|
9712
|
-
return new EventLog(dbPath);
|
|
9837
|
+
return new EventLog(dbPath, { busyTimeoutMs: this.eventLogBusyTimeoutMs });
|
|
9713
9838
|
}
|
|
9714
9839
|
catch (retryErr) {
|
|
9715
9840
|
// History is worth losing; a fleet that won't start is not.
|
|
@@ -10253,6 +10378,38 @@ export class FleetManager {
|
|
|
10253
10378
|
},
|
|
10254
10379
|
});
|
|
10255
10380
|
}
|
|
10381
|
+
cacheService = null;
|
|
10382
|
+
/**
|
|
10383
|
+
* #1468: the prompt-cache expiry analysis for the web, per instance, over `window`. Read from each instance's
|
|
10384
|
+
* ledger as it is now; the first request starts the bounded, persisted catch-up over the transcripts
|
|
10385
|
+
* (cache-service.ts) — never a vendor call, never a synchronous transcript read.
|
|
10386
|
+
*/
|
|
10387
|
+
cacheReport(window) {
|
|
10388
|
+
const w = Object.prototype.hasOwnProperty.call(WINDOWS, window) ? window : "7d";
|
|
10389
|
+
this.cacheService ??= new CacheService({
|
|
10390
|
+
// fleet.yaml's instances, then the ClassicBot rooms (their workspace under the AgEnD home, their own backend).
|
|
10391
|
+
instances: () => {
|
|
10392
|
+
const fleet = Object.entries(this.fleetConfig?.instances ?? {}).map(([name, cfg]) => ({
|
|
10393
|
+
name, backend: this.backendNameOf(name), workingDirectory: cfg.working_directory, ledgerPath: join(this.getInstanceDir(name), "cache-ledger.json"),
|
|
10394
|
+
}));
|
|
10395
|
+
const taken = new Set(fleet.map(i => i.name));
|
|
10396
|
+
const classic = (this.classicChannels?.getAll() ?? []).filter(ch => !taken.has(ch.instanceName)).map(ch => ({
|
|
10397
|
+
name: ch.instanceName,
|
|
10398
|
+
backend: this.classicChannels?.getBackendByInstance(ch.instanceName, this.fleetConfig?.defaults?.backend) ?? this.fleetConfig?.defaults?.backend ?? "claude-code",
|
|
10399
|
+
workingDirectory: join(getAgendHome(), "workspaces", ch.instanceName),
|
|
10400
|
+
ledgerPath: join(this.getInstanceDir(ch.instanceName), "cache-ledger.json"),
|
|
10401
|
+
}));
|
|
10402
|
+
return [...fleet, ...new Map(classic.map(c => [c.name, c])).values()];
|
|
10403
|
+
},
|
|
10404
|
+
claudeProjectsDir: () => join(process.env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), ".claude"), "projects"),
|
|
10405
|
+
claudeKey: claudeProjectKey,
|
|
10406
|
+
codexSessionsDir: () => join(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"), "sessions"),
|
|
10407
|
+
listRollouts: root => sharedRolloutIndex(root).list(),
|
|
10408
|
+
metaPath: join(this.dataDir, "cache-codex-rollouts.json"),
|
|
10409
|
+
log: (level, msg, extra) => this.logger[level](extra ?? {}, msg),
|
|
10410
|
+
});
|
|
10411
|
+
return this.cacheService.report(w);
|
|
10412
|
+
}
|
|
10256
10413
|
/** The web's Acknowledge (any item; a signed-in session is fleet-admin level). */
|
|
10257
10414
|
acknowledgeNeedsItem(id, principal) {
|
|
10258
10415
|
if (!this.needsYou)
|
|
@@ -10680,29 +10837,18 @@ export class FleetManager {
|
|
|
10680
10837
|
});
|
|
10681
10838
|
return nonce ? null : t("clear.prompt_unavailable");
|
|
10682
10839
|
}
|
|
10683
|
-
/**
|
|
10684
|
-
|
|
10685
|
-
|
|
10686
|
-
|
|
10687
|
-
|
|
10688
|
-
|
|
10689
|
-
|
|
10690
|
-
const
|
|
10691
|
-
this.
|
|
10692
|
-
|
|
10693
|
-
await this.retireNonceButtons(pending, pending.messageId ?? data.messageId, t("clear.cancelled", pending.instanceName));
|
|
10694
|
-
return true;
|
|
10695
|
-
}
|
|
10696
|
-
// The nonce was claimed synchronously. Keep the exact owner across platform
|
|
10697
|
-
// retirement awaits; a replacement with the same name is not this clear's
|
|
10698
|
-
// target, and a role granted at claim time may be revoked while editing.
|
|
10699
|
-
const ipc = this.instanceIpcClients.get(pending.instanceName);
|
|
10700
|
-
const daemon = this.daemons.get(pending.instanceName);
|
|
10701
|
-
const epoch = this.getDeliveryEpoch(pending.instanceName);
|
|
10702
|
-
const groupId = this.getChannelConfig(callbackAdapterId)?.group_id;
|
|
10840
|
+
/**
|
|
10841
|
+
* What a confirmed /clear must still find when it acts — shared by the platform's Confirm button and the web
|
|
10842
|
+
* (#1269): the same daemon and IPC client, the same interaction owner, lifecycle epoch and delivery epoch as when the
|
|
10843
|
+
* fence was taken. Null when there is no owner to pin (then nothing may be cleared). The reads are cached and
|
|
10844
|
+
* synchronous; they never probe the pane.
|
|
10845
|
+
*/
|
|
10846
|
+
clearTargetFence(instanceName) {
|
|
10847
|
+
const ipc = this.instanceIpcClients.get(instanceName);
|
|
10848
|
+
const daemon = this.daemons.get(instanceName);
|
|
10849
|
+
const epoch = this.getDeliveryEpoch(instanceName);
|
|
10703
10850
|
// Object identity survives a resident daemon's respawn/freeze, and stop
|
|
10704
10851
|
// invalidates its lifecycle epoch before the queued work replaces objects.
|
|
10705
|
-
// These reads are cached and synchronous; they never probe the pane.
|
|
10706
10852
|
const readOwner = () => {
|
|
10707
10853
|
try {
|
|
10708
10854
|
const owner = daemon?.getInteractionSnapshot?.()?.owner;
|
|
@@ -10718,7 +10864,7 @@ export class FleetManager {
|
|
|
10718
10864
|
};
|
|
10719
10865
|
const readLifecycleEpoch = () => {
|
|
10720
10866
|
try {
|
|
10721
|
-
const value = this.lifecycle?.epochOf(
|
|
10867
|
+
const value = this.lifecycle?.epochOf(instanceName);
|
|
10722
10868
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
10723
10869
|
}
|
|
10724
10870
|
catch {
|
|
@@ -10726,8 +10872,112 @@ export class FleetManager {
|
|
|
10726
10872
|
}
|
|
10727
10873
|
};
|
|
10728
10874
|
const owner = readOwner(), lifecycleEpoch = readLifecycleEpoch();
|
|
10875
|
+
if (owner === null || lifecycleEpoch === null)
|
|
10876
|
+
return null;
|
|
10877
|
+
return () => {
|
|
10878
|
+
try {
|
|
10879
|
+
const currentOwner = readOwner();
|
|
10880
|
+
return this.daemons.get(instanceName) === daemon
|
|
10881
|
+
&& this.instanceIpcClients.get(instanceName) === ipc
|
|
10882
|
+
&& currentOwner !== null && sameInteractionOwner(owner, currentOwner)
|
|
10883
|
+
&& readLifecycleEpoch() === lifecycleEpoch
|
|
10884
|
+
&& this.isDeliveryEpochCurrent(instanceName, epoch);
|
|
10885
|
+
}
|
|
10886
|
+
catch {
|
|
10887
|
+
return false;
|
|
10888
|
+
}
|
|
10889
|
+
};
|
|
10890
|
+
}
|
|
10891
|
+
/** #1269: the web's /clear confirmations — one token per question, used once, for the instance as it was asked about. */
|
|
10892
|
+
webClearTokens = new Map();
|
|
10893
|
+
/** #1269: an instance's chat command from the web chat (web-commands.ts); `publicLink`: a gateway request. */
|
|
10894
|
+
webCommand(input, opts) {
|
|
10895
|
+
const webMeta = (instance) => {
|
|
10896
|
+
const topicId = this.fleetConfig?.instances[instance]?.topic_id;
|
|
10897
|
+
return {
|
|
10898
|
+
chatId: this.getGroupIdForInstance(instance) || "", messageId: newWebMessageId(), username: "web-user", userId: "web-user",
|
|
10899
|
+
threadId: topicId != null ? String(topicId) : undefined, adapterId: this.getAdapterForInstance(instance)?.id ?? "", source: "web",
|
|
10900
|
+
};
|
|
10901
|
+
};
|
|
10902
|
+
const choicesFromCache = (instance) => {
|
|
10903
|
+
const cached = this.readCliEnv(this.backendNameForInstance(instance)); // the cached catalog only: never a probe here
|
|
10904
|
+
if (!cached?.models.length)
|
|
10905
|
+
return null;
|
|
10906
|
+
const current = this.resolveInstanceModel(instance).model;
|
|
10907
|
+
return { current, options: cached.models.map(o => ({ id: o.id, label: this.modelChoiceLabel(o, current) })) };
|
|
10908
|
+
};
|
|
10909
|
+
return runWebCommand({
|
|
10910
|
+
// Own entries only: a name like "__proto__" or "constructor" must not be found on the prototype (#1476 review).
|
|
10911
|
+
scope: (instance) => {
|
|
10912
|
+
const instances = this.fleetConfig?.instances;
|
|
10913
|
+
if (instances && Object.prototype.hasOwnProperty.call(instances, instance))
|
|
10914
|
+
return isGeneralInstance(this.fleetConfig, instance) ? "general" : "fleet";
|
|
10915
|
+
return this.classicChannels?.getChannelIdByInstance(instance) !== undefined ? "classic" : null;
|
|
10916
|
+
},
|
|
10917
|
+
ctx: (instance) => this.topicCommands.getCtxText(instance),
|
|
10918
|
+
compact: (instance, instructions) => this.topicCommands.sendCompact(instance, instructions),
|
|
10919
|
+
applyModel: (instance, name) => this.applyModel(instance, name),
|
|
10920
|
+
modelChoices: choicesFromCache,
|
|
10921
|
+
applyEffort: (instance, level) => this.applyEffort(instance, level),
|
|
10922
|
+
effortChoices: (instance) => {
|
|
10923
|
+
const levels = this.effortLevelsFor(instance);
|
|
10924
|
+
if (!levels.length)
|
|
10925
|
+
return null;
|
|
10926
|
+
const current = this.resolveInstanceEffort(instance).effort;
|
|
10927
|
+
return { current, options: levels.map(l => ({ id: l, label: this.effortChoiceLabel(l, current) })) };
|
|
10928
|
+
},
|
|
10929
|
+
cancel: (instance) => this.cancelInstance(instance),
|
|
10930
|
+
steer: (instance, text) => this.topicCommands.sendSteer(instance, text, webMeta(instance)),
|
|
10931
|
+
btw: (instance, text) => this.topicCommands.sendBtw(instance, text, webMeta(instance)),
|
|
10932
|
+
pauseWake: (instance, action) => this.topicCommands.runPauseWake(instance, action),
|
|
10933
|
+
save: (instance, filename) => this.topicCommands.sendSave(instance, filename),
|
|
10934
|
+
clearAsk: (instance) => {
|
|
10935
|
+
if (!this.topicCommands.supportsClear(instance))
|
|
10936
|
+
return { refused: t("clear.unsupported") };
|
|
10937
|
+
const fence = this.clearTargetFence(instance);
|
|
10938
|
+
if (!fence)
|
|
10939
|
+
return { refused: t("clear.not_connected") };
|
|
10940
|
+
const now = performance.now();
|
|
10941
|
+
for (const [k, v] of this.webClearTokens)
|
|
10942
|
+
if (v.deadline < now)
|
|
10943
|
+
this.webClearTokens.delete(k);
|
|
10944
|
+
const token = randomBytes(16).toString("hex");
|
|
10945
|
+
this.webClearTokens.set(token, { instance, fence, deadline: now + CLEAR_CONFIRM_TIMEOUT_MS });
|
|
10946
|
+
return { token, message: t("clear.confirm_message", instance) };
|
|
10947
|
+
},
|
|
10948
|
+
clearConfirm: async (instance, token) => {
|
|
10949
|
+
const entry = this.webClearTokens.get(token);
|
|
10950
|
+
this.webClearTokens.delete(token); // used once, whatever happens next
|
|
10951
|
+
if (!entry || entry.instance !== instance || performance.now() > entry.deadline)
|
|
10952
|
+
return { status: 409, text: t("clear.expired", instance) };
|
|
10953
|
+
this.eventLog?.insert(instance, "clear_action", { action: "confirm", userId: "web" });
|
|
10954
|
+
// sendClear sends its first IPC synchronously: no await separates this check from the effect.
|
|
10955
|
+
if (this.shuttingDown || !entry.fence())
|
|
10956
|
+
return { status: 409, text: t("menu.click_stale") };
|
|
10957
|
+
return { status: 200, text: await this.topicCommands.sendClear(instance) };
|
|
10958
|
+
},
|
|
10959
|
+
t: (key, ...args) => t(key, ...args),
|
|
10960
|
+
}, input, opts);
|
|
10961
|
+
}
|
|
10962
|
+
/** Consume `/clear` Confirm/Cancel exactly once; only Confirm reaches IPC. */
|
|
10963
|
+
async handleClearConfirmation(data, callbackAdapterId, receivingAdapter) {
|
|
10964
|
+
const claimed = this.consumeNonceCallback(CLEAR_CONFIRM_CALLBACK_PREFIX, /^clear-confirm:([0-9a-f]+):(confirm|cancel)$/, data, callbackAdapterId, receivingAdapter);
|
|
10965
|
+
if (claimed === null)
|
|
10966
|
+
return false;
|
|
10967
|
+
if (claimed === "consumed")
|
|
10968
|
+
return true;
|
|
10969
|
+
const { entry: pending, action } = claimed;
|
|
10970
|
+
this.eventLog?.insert(pending.instanceName, "clear_action", { action, userId: data.userId });
|
|
10971
|
+
if (action === "cancel") {
|
|
10972
|
+
await this.retireNonceButtons(pending, pending.messageId ?? data.messageId, t("clear.cancelled", pending.instanceName));
|
|
10973
|
+
return true;
|
|
10974
|
+
}
|
|
10975
|
+
// The nonce was claimed synchronously. Keep the exact owner across platform
|
|
10976
|
+
// retirement awaits; a replacement with the same name is not this clear's
|
|
10977
|
+
// target, and a role granted at claim time may be revoked while editing.
|
|
10978
|
+
const target = this.clearTargetFence(pending.instanceName);
|
|
10979
|
+
const groupId = this.getChannelConfig(callbackAdapterId)?.group_id;
|
|
10729
10980
|
const current = () => {
|
|
10730
|
-
const currentOwner = readOwner();
|
|
10731
10981
|
try {
|
|
10732
10982
|
return !this.shuttingDown
|
|
10733
10983
|
&& !!data.userId && !!pending.authChannelId
|
|
@@ -10739,11 +10989,7 @@ export class FleetManager {
|
|
|
10739
10989
|
? pending.adapter.type !== "telegram" || pending.chatId === pending.authChannelId
|
|
10740
10990
|
: String(this.getChannelConfig(callbackAdapterId)?.group_id ?? "") === pending.chatId)
|
|
10741
10991
|
&& this.getChannelConfig(callbackAdapterId)?.group_id === groupId
|
|
10742
|
-
&&
|
|
10743
|
-
&& this.instanceIpcClients.get(pending.instanceName) === ipc
|
|
10744
|
-
&& owner !== null && currentOwner !== null && sameInteractionOwner(owner, currentOwner)
|
|
10745
|
-
&& lifecycleEpoch !== null && readLifecycleEpoch() === lifecycleEpoch
|
|
10746
|
-
&& this.isDeliveryEpochCurrent(pending.instanceName, epoch);
|
|
10992
|
+
&& target !== null && target();
|
|
10747
10993
|
}
|
|
10748
10994
|
catch {
|
|
10749
10995
|
return false;
|
|
@@ -14623,6 +14869,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
14623
14869
|
// #1386: every live "Needs you" message says the fleet stopped (capabilities revoked first) — while the
|
|
14624
14870
|
// adapters can still edit. Bounded: a platform that does not answer must not hold the shutdown.
|
|
14625
14871
|
const needsStopped = this.stopNeedsYou();
|
|
14872
|
+
this.cacheService?.stop();
|
|
14626
14873
|
const profileStopped = this.runtimeCpuProfiler?.shutdown("fleet shutdown");
|
|
14627
14874
|
this.ipcStoppingInstances.add("__fleet_stopping__");
|
|
14628
14875
|
// Release held delivery promises before awaiting daemon shutdown, then
|