@songsid/agend 2.1.5-beta.3 → 2.1.5-beta.5
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/backend/claude-code.js +2 -2
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/types.d.ts +22 -0
- package/dist/backend/types.js +23 -1
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/adapters/discord.js +5 -1
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/fleet-manager.d.ts +37 -0
- package/dist/fleet-manager.js +141 -20
- package/dist/fleet-manager.js.map +1 -1
- package/dist/locale.js +2 -0
- package/dist/locale.js.map +1 -1
- package/dist/restart-progress.d.ts +31 -4
- package/dist/restart-progress.js +167 -16
- package/dist/restart-progress.js.map +1 -1
- package/package.json +1 -1
package/dist/fleet-manager.js
CHANGED
|
@@ -60,7 +60,7 @@ import { clearPausedMarker } from "./pause-marker.js";
|
|
|
60
60
|
import { releaseProcessFleetLock } from "./fleet-lock.js";
|
|
61
61
|
import { GENERAL_PAUSE_ERROR, isGeneralInstance } from "./general-instance.js";
|
|
62
62
|
import { loadOrCreateWebToken, WEB_TOKEN_INVALID_MESSAGE } from "./web-auth.js";
|
|
63
|
-
import { RestartProgress } from "./restart-progress.js";
|
|
63
|
+
import { RESTART_PROGRESS_TERMINAL_TIMEOUT_MS, RestartProgress, } from "./restart-progress.js";
|
|
64
64
|
import { collectRedundantInstanceDefaultPaths } from "./fleet-yaml-slim.js";
|
|
65
65
|
import { StormWindow } from "./storm-window.js";
|
|
66
66
|
import { SpawnGate } from "./spawn-gate.js";
|
|
@@ -228,7 +228,39 @@ const CLEAR_CONFIRM_TIMEOUT_MS = 15_000;
|
|
|
228
228
|
/** Default lifetime for long-lived nonce prompts (clear overrides this to 15s). */
|
|
229
229
|
const NONCE_BUTTON_TIMEOUT_MS = 15 * 60_000;
|
|
230
230
|
const TIP_BUTTON_TIMEOUT_MS = 24 * 60 * 60_000;
|
|
231
|
-
const CLI_ENV_TTL_MS = 24 * 60 * 60 * 1000; //
|
|
231
|
+
const CLI_ENV_TTL_MS = 24 * 60 * 60 * 1000; // hard validity bound for the cached CLI env
|
|
232
|
+
/**
|
|
233
|
+
* How old the cached CLI env may be before `/model` re-probes it live.
|
|
234
|
+
*
|
|
235
|
+
* The cache is a file under AGEND_HOME, so it outlives the process, and the only
|
|
236
|
+
* thing that refreshed it was the startup probe. That is why a newly released
|
|
237
|
+
* model showed up only after `agend stop` + `agend start`: a bare `agend restart`
|
|
238
|
+
* signals SIGUSR2 and restarts the instances inside the *same* manager process,
|
|
239
|
+
* so nothing re-probed and `/model` kept serving a list up to 24h old.
|
|
240
|
+
*/
|
|
241
|
+
const CLI_ENV_FRESH_MS = 60 * 60 * 1000;
|
|
242
|
+
/**
|
|
243
|
+
* Upper bound on a live probe driven by `/model`.
|
|
244
|
+
*
|
|
245
|
+
* A probe chains bounded leaves but is not itself bounded: claude-code runs
|
|
246
|
+
* `--version` (5s) then listModels then listApiModels (an 8s AbortController
|
|
247
|
+
* against api.anthropic.com), and several backends' probes carry no explicit
|
|
248
|
+
* timeout at all. Awaiting that inline would make `/model` the next thing to
|
|
249
|
+
* hang, so it races this deadline and falls back to the cached list.
|
|
250
|
+
*
|
|
251
|
+
* Must stay above CLI_PROBE_LONGEST_CHAIN_MS — the probe's bounded steps run
|
|
252
|
+
* back to back, so clearing only the longest single leaf would be false
|
|
253
|
+
* confidence: a deadline above 8s but below the 13s chain still truncates a
|
|
254
|
+
* probe that would have succeeded. Bounding the chain rather than the leaf is
|
|
255
|
+
* the lesson from the usage hang, and the tests assert against the derived
|
|
256
|
+
* chain constant so raising either step cannot silently break it.
|
|
257
|
+
*
|
|
258
|
+
* The deadline exists to stop a probe hanging forever, not to cut short one that
|
|
259
|
+
* would have finished: truncating a legitimate probe serves the previous list
|
|
260
|
+
* and hides exactly the newly released model the user opened `/model` to find.
|
|
261
|
+
* The wait is announced before it starts, so it reads as progress, not a stall.
|
|
262
|
+
*/
|
|
263
|
+
export const CLI_ENV_PROBE_DEADLINE_MS = 16_000;
|
|
232
264
|
export class FleetManager {
|
|
233
265
|
dataDir;
|
|
234
266
|
static signalTarget = null;
|
|
@@ -1751,16 +1783,60 @@ export class FleetManager {
|
|
|
1751
1783
|
if (!generalName)
|
|
1752
1784
|
return null;
|
|
1753
1785
|
const adapter = this.getAdapterForInstance(generalName);
|
|
1786
|
+
const adapterId = this.getInstanceAdapterId(generalName);
|
|
1754
1787
|
const chatId = this.getGroupIdForInstance(generalName);
|
|
1755
1788
|
if (!adapter || !chatId)
|
|
1756
1789
|
return null;
|
|
1757
1790
|
const topicId = this.fleetConfig?.instances[generalName]?.topic_id;
|
|
1758
1791
|
return {
|
|
1759
1792
|
adapter,
|
|
1793
|
+
resolveAdapter: adapterId ? () => this.readyProgressAdapter(adapterId) : undefined,
|
|
1760
1794
|
chatId,
|
|
1761
1795
|
threadId: topicId != null ? String(topicId) : undefined,
|
|
1762
1796
|
};
|
|
1763
1797
|
}
|
|
1798
|
+
/** Resolve only an adapter generation that can accept progress delivery.
|
|
1799
|
+
* Discord exposes direct gateway readiness; adapters without a health
|
|
1800
|
+
* snapshot use the fleet startup/retry state. */
|
|
1801
|
+
readyProgressAdapter(adapterId) {
|
|
1802
|
+
const adapter = this.adapters.get(adapterId);
|
|
1803
|
+
if (!adapter)
|
|
1804
|
+
return undefined;
|
|
1805
|
+
const health = adapter.getHealthSnapshot?.();
|
|
1806
|
+
if (health)
|
|
1807
|
+
return health.isReady ? adapter : undefined;
|
|
1808
|
+
return this.adapterState.get(adapterId)?.status === "connected" ? adapter : undefined;
|
|
1809
|
+
}
|
|
1810
|
+
/** Last-resort completion after RestartProgress could not deliver to its
|
|
1811
|
+
* adopted target. It stays bounded and never wakes a not-ready gateway. */
|
|
1812
|
+
async sendFleetStartCompletionFallback(chatId, text, threadId) {
|
|
1813
|
+
const adapterId = this.getPrimaryAdapterId();
|
|
1814
|
+
const adapter = adapterId ? this.readyProgressAdapter(adapterId) : undefined;
|
|
1815
|
+
if (!adapter) {
|
|
1816
|
+
this.logger.error({ adapterId }, "Fleet start completion fallback skipped because the primary adapter is not ready");
|
|
1817
|
+
return false;
|
|
1818
|
+
}
|
|
1819
|
+
let timer;
|
|
1820
|
+
const timeout = new Promise(resolve => {
|
|
1821
|
+
timer = setTimeout(() => resolve({ status: "timeout" }), RESTART_PROGRESS_TERMINAL_TIMEOUT_MS);
|
|
1822
|
+
timer.unref?.();
|
|
1823
|
+
});
|
|
1824
|
+
const delivery = Promise.resolve()
|
|
1825
|
+
.then(() => adapter.sendText(chatId, text, { threadId }))
|
|
1826
|
+
.then(() => ({ status: "sent" }), err => ({ status: "failed", err }));
|
|
1827
|
+
const result = await Promise.race([delivery, timeout]);
|
|
1828
|
+
if (timer)
|
|
1829
|
+
clearTimeout(timer);
|
|
1830
|
+
if (result.status === "sent")
|
|
1831
|
+
return true;
|
|
1832
|
+
if (result.status === "failed") {
|
|
1833
|
+
this.logger.error({ err: result.err }, "Failed to send fleet start completion fallback");
|
|
1834
|
+
}
|
|
1835
|
+
else {
|
|
1836
|
+
this.logger.error({ timeout_ms: RESTART_PROGRESS_TERMINAL_TIMEOUT_MS }, "Timed out sending fleet start completion fallback");
|
|
1837
|
+
}
|
|
1838
|
+
return false;
|
|
1839
|
+
}
|
|
1764
1840
|
async stopInstance(name) {
|
|
1765
1841
|
this.explicitStopGeneration.set(name, (this.explicitStopGeneration.get(name) ?? 0) + 1);
|
|
1766
1842
|
this.cancelStartupRetry(name);
|
|
@@ -2317,12 +2393,17 @@ export class FleetManager {
|
|
|
2317
2393
|
progressStart = adapterStartup.then(() => {
|
|
2318
2394
|
if (pendingUpdateProgress) {
|
|
2319
2395
|
const saved = pendingUpdateProgress.progress.target;
|
|
2320
|
-
const
|
|
2321
|
-
|
|
2322
|
-
|
|
2396
|
+
const target = {
|
|
2397
|
+
adapter: this.adapters.get(saved.adapterId),
|
|
2398
|
+
// Adapter retries replace the failed object in this map. Resolve at
|
|
2399
|
+
// every progress delivery so the adopted update message follows the
|
|
2400
|
+
// live, ready generation instead of remaining pinned to a stopped
|
|
2401
|
+
// client. This also waits when the first generation failed before
|
|
2402
|
+
// any adapter object was registered.
|
|
2403
|
+
resolveAdapter: () => this.readyProgressAdapter(saved.adapterId),
|
|
2323
2404
|
chatId: saved.chatId,
|
|
2324
2405
|
threadId: saved.threadId,
|
|
2325
|
-
}
|
|
2406
|
+
};
|
|
2326
2407
|
return startupProgress.resume(target, saved.messageId);
|
|
2327
2408
|
}
|
|
2328
2409
|
return startupProgress.start(this.restartProgressTarget());
|
|
@@ -2439,7 +2520,7 @@ export class FleetManager {
|
|
|
2439
2520
|
})()
|
|
2440
2521
|
: undefined,
|
|
2441
2522
|
});
|
|
2442
|
-
if (!progressCompleted &&
|
|
2523
|
+
if (!progressCompleted && fleet.channel?.group_id) {
|
|
2443
2524
|
let text;
|
|
2444
2525
|
if (failedNames.length === 0 && pausedNames.length === 0) {
|
|
2445
2526
|
text = t("fleet.ready", started, total, agendVersion);
|
|
@@ -2451,9 +2532,7 @@ export class FleetManager {
|
|
|
2451
2532
|
text = t("fleet.ready_with_failed", started, total, agendVersion, failedNames.join(", "))
|
|
2452
2533
|
+ (pausedNames.length > 0 ? `\n⏸ Paused: ${pausedNames.join(", ")}` : "");
|
|
2453
2534
|
}
|
|
2454
|
-
this.
|
|
2455
|
-
threadId: generalThreadId != null ? String(generalThreadId) : undefined,
|
|
2456
|
-
}).catch(e => this.logger.warn({ err: e }, "Failed to send fleet start notification"));
|
|
2535
|
+
await this.sendFleetStartCompletionFallback(String(fleet.channel.group_id), text, generalThreadId != null ? String(generalThreadId) : undefined);
|
|
2457
2536
|
}
|
|
2458
2537
|
}
|
|
2459
2538
|
// Health HTTP endpoint
|
|
@@ -8376,6 +8455,32 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
8376
8455
|
catch { /* missing / stale / corrupt */ }
|
|
8377
8456
|
return null;
|
|
8378
8457
|
}
|
|
8458
|
+
/** True when a cached CLI env is old enough that `/model` should re-probe. */
|
|
8459
|
+
cliEnvNeedsRefresh(env) {
|
|
8460
|
+
return !env || typeof env.probedAt !== "number" || Date.now() - env.probedAt >= CLI_ENV_FRESH_MS;
|
|
8461
|
+
}
|
|
8462
|
+
/**
|
|
8463
|
+
* Run a live probe under a deadline, falling back to whatever the cache holds.
|
|
8464
|
+
* A model list is an aid: a vendor that stops answering must degrade to the
|
|
8465
|
+
* previous list, never stall the command that asked for it.
|
|
8466
|
+
*/
|
|
8467
|
+
async probeBackendBounded(backend) {
|
|
8468
|
+
const work = this.probeBackend(backend);
|
|
8469
|
+
work.catch(() => { });
|
|
8470
|
+
let timer;
|
|
8471
|
+
const deadline = new Promise(resolve => {
|
|
8472
|
+
timer = setTimeout(() => {
|
|
8473
|
+
this.logger.warn({ backend, deadlineMs: CLI_ENV_PROBE_DEADLINE_MS }, "CLI env live probe exceeded its deadline — serving the cached model list");
|
|
8474
|
+
resolve(null);
|
|
8475
|
+
}, CLI_ENV_PROBE_DEADLINE_MS);
|
|
8476
|
+
});
|
|
8477
|
+
try {
|
|
8478
|
+
return await Promise.race([work, deadline]);
|
|
8479
|
+
}
|
|
8480
|
+
finally {
|
|
8481
|
+
clearTimeout(timer);
|
|
8482
|
+
}
|
|
8483
|
+
}
|
|
8379
8484
|
/**
|
|
8380
8485
|
* Resolve the effective model for a fleet or ClassicBot instance, plus where it
|
|
8381
8486
|
* came from. Single source of truth for `/model` and `/ctx` — precedence:
|
|
@@ -8477,16 +8582,23 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
8477
8582
|
void this.probeBackend(b);
|
|
8478
8583
|
}
|
|
8479
8584
|
/** Best-effort model list for `/model`: cached CLI env first, else live probe. Never throws. */
|
|
8480
|
-
async getModelOptions(instanceName, refresh = false) {
|
|
8585
|
+
async getModelOptions(instanceName, refresh = false, onLiveProbe) {
|
|
8481
8586
|
const backendName = this.backendNameForInstance(instanceName);
|
|
8482
|
-
|
|
8483
|
-
|
|
8484
|
-
|
|
8485
|
-
|
|
8486
|
-
|
|
8487
|
-
//
|
|
8488
|
-
|
|
8489
|
-
|
|
8587
|
+
const cached = this.readCliEnv(backendName);
|
|
8588
|
+
if (!refresh && cached?.models.length && !this.cliEnvNeedsRefresh(cached))
|
|
8589
|
+
return cached.models;
|
|
8590
|
+
// About to go to the vendor: let the caller say so. A silent 1–10s pause on
|
|
8591
|
+
// an interactive command reads as another hang, which is the wrong lesson to
|
|
8592
|
+
// teach a user who has just been bitten by one.
|
|
8593
|
+
onLiveProbe?.();
|
|
8594
|
+
// Stale, missing, or a forced refresh → probe live (also refreshes the cache).
|
|
8595
|
+
// A newly released model is invisible until this runs, which is why staleness
|
|
8596
|
+
// triggers it rather than waiting for the 24h hard expiry or a cold start.
|
|
8597
|
+
const env = await this.probeBackendBounded(backendName);
|
|
8598
|
+
if (env?.models.length)
|
|
8599
|
+
return env.models;
|
|
8600
|
+
// Probe failed or timed out: the previous list is still the best answer.
|
|
8601
|
+
return cached?.models ?? [];
|
|
8490
8602
|
}
|
|
8491
8603
|
/**
|
|
8492
8604
|
* Model catalog behind the `list_models` tool.
|
|
@@ -8777,7 +8889,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
8777
8889
|
await data.respond(t("model.usage"));
|
|
8778
8890
|
return;
|
|
8779
8891
|
}
|
|
8780
|
-
const options = await this.getModelOptions(name, isRefresh)
|
|
8892
|
+
const options = await this.getModelOptions(name, isRefresh, () => {
|
|
8893
|
+
void data.respond(t("model.refreshing")).catch(() => { });
|
|
8894
|
+
});
|
|
8781
8895
|
if (options.length === 0) {
|
|
8782
8896
|
await data.respond(t("model.list_unavailable", name));
|
|
8783
8897
|
return;
|
|
@@ -9788,6 +9902,13 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
9788
9902
|
this.logger.error("Cannot restart: no config path (was startAll called?)");
|
|
9789
9903
|
return;
|
|
9790
9904
|
}
|
|
9905
|
+
// A graceful restart keeps this manager process, so the startup probe does
|
|
9906
|
+
// not run again. Without this, `agend restart` left the cached CLI env
|
|
9907
|
+
// untouched and `/model` kept serving an old list until a cold start —
|
|
9908
|
+
// exactly the "only stop+start works" report. Background, never blocking:
|
|
9909
|
+
// /model re-probes on staleness anyway, this just makes a restart do the
|
|
9910
|
+
// refreshing a user expects of it.
|
|
9911
|
+
this.probeCliEnvs();
|
|
9791
9912
|
const instanceNames = [...this.daemons.keys()];
|
|
9792
9913
|
if (instanceNames.length === 0) {
|
|
9793
9914
|
this.logger.info("No instances to restart");
|