@songsid/agend 2.1.4-beta.15 → 2.1.4-beta.16
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-endpoint.js +1 -1
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/daemon.d.ts +20 -1
- package/dist/daemon.js +80 -0
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-context.d.ts +2 -0
- package/dist/fleet-manager.d.ts +8 -1
- package/dist/fleet-manager.js +88 -10
- package/dist/fleet-manager.js.map +1 -1
- package/dist/outbound-handlers.d.ts +2 -0
- package/dist/outbound-handlers.js +1 -1
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/settings-api.js +1 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/topic-commands.d.ts +2 -2
- package/dist/topic-commands.js +3 -3
- package/dist/topic-commands.js.map +1 -1
- package/dist/types.d.ts +2 -1
- package/dist/ui/settings.html +74 -14
- package/dist/usage/usage-api.d.ts +8 -2
- package/dist/usage/usage-api.js +24 -3
- package/dist/usage/usage-api.js.map +1 -1
- package/package.json +1 -1
package/dist/fleet-context.d.ts
CHANGED
|
@@ -54,6 +54,8 @@ export interface FleetContext {
|
|
|
54
54
|
readonly classicChannels: ClassicChannelManager | null;
|
|
55
55
|
getSysInfo(): SysInfo;
|
|
56
56
|
getInstanceStatus(name: string): "running" | "paused" | "stopped" | "crashed";
|
|
57
|
+
/** Subscription provider IDs used by running or paused fleet/Classic instances. */
|
|
58
|
+
getActiveUsageProviderIds?(): ReadonlySet<string>;
|
|
57
59
|
toggleFleetCollab(instanceName: string): boolean;
|
|
58
60
|
/** Apply a model to an instance (runtime paste or persist + restart). Returns a status string. */
|
|
59
61
|
applyModel(instanceName: string, model: string): Promise<string>;
|
package/dist/fleet-manager.d.ts
CHANGED
|
@@ -273,6 +273,12 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
273
273
|
bindInstanceAdapter(name: string, adapterId: string, fromInbound?: boolean): void;
|
|
274
274
|
getInstanceStatus(name: string): "running" | "paused" | "stopped" | "crashed";
|
|
275
275
|
getInstanceExecutionState(name: string): InstanceState | null;
|
|
276
|
+
/**
|
|
277
|
+
* Subscription providers currently relevant to this fleet. Stopped/crashed
|
|
278
|
+
* rows do not keep a usage card alive, while persisted paused instances do.
|
|
279
|
+
* Classic channels use their effective backend merge chain, not the raw row.
|
|
280
|
+
*/
|
|
281
|
+
getActiveUsageProviderIds(): ReadonlySet<string>;
|
|
276
282
|
isClassicInstance(name: string): boolean;
|
|
277
283
|
private cacheInstanceExecutionState;
|
|
278
284
|
private cancelIdleButtonRetirement;
|
|
@@ -1010,7 +1016,8 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
1010
1016
|
/**
|
|
1011
1017
|
* Hot-reload: re-read fleet.yaml and reconcile running instances.
|
|
1012
1018
|
* Starts new, stops removed, restarts modified instances.
|
|
1013
|
-
*
|
|
1019
|
+
* Whitelisted runtime fields are pushed into live daemons; all other instance
|
|
1020
|
+
* fields, plus cold fleet-level settings, retain restart semantics.
|
|
1014
1021
|
*/
|
|
1015
1022
|
private reconcileInstances;
|
|
1016
1023
|
restartInstances(): Promise<void>;
|
package/dist/fleet-manager.js
CHANGED
|
@@ -3,6 +3,7 @@ import { randomBytes } from "node:crypto";
|
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import { join, dirname, basename } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { isDeepStrictEqual } from "node:util";
|
|
6
7
|
import { getAgendHome, ensureWorkspaceGit } from "./paths.js";
|
|
7
8
|
import { beginUpdateProgress as persistUpdateProgress, clearUpdateMarker, isUpdateInProgress, readUpdateProgress, setUpdateProgressStage, } from "./update-marker.js";
|
|
8
9
|
import { formatUpdateProgress } from "./update-progress.js";
|
|
@@ -42,7 +43,7 @@ import { StatuslineWatcher } from "./statusline-watcher.js";
|
|
|
42
43
|
import { outboundHandlers } from "./outbound-handlers.js";
|
|
43
44
|
import { handleWebRequest, broadcastSseEvent } from "./web-api.js";
|
|
44
45
|
import { handleViewRequest, isViewPath } from "./view-api.js";
|
|
45
|
-
import { handleUsageRequest, isUsagePath } from "./usage/usage-api.js";
|
|
46
|
+
import { handleUsageRequest, isUsagePath, usageProviderIdForBackend } from "./usage/usage-api.js";
|
|
46
47
|
import { handleSettingsRequest } from "./settings-api.js";
|
|
47
48
|
import { setLocale, detectLocale, t } from "./locale.js";
|
|
48
49
|
import { handleAgentRequest } from "./agent-endpoint.js";
|
|
@@ -165,6 +166,30 @@ const DELIVERY_STATUS_EMOJIS = new Set(["👀", "⏳", "✅", "❌"]);
|
|
|
165
166
|
* emoji never changes the documented delivery-state protocol.
|
|
166
167
|
*/
|
|
167
168
|
const IGNORED_REACTION_EMOJIS = new Set(["📷"]);
|
|
169
|
+
const HOT_INSTANCE_CONFIG_KEYS = new Set([
|
|
170
|
+
"tool_progress",
|
|
171
|
+
"mcp_proxy_reply",
|
|
172
|
+
"auto_pause_after",
|
|
173
|
+
"warm_cap",
|
|
174
|
+
"display_name",
|
|
175
|
+
"description",
|
|
176
|
+
"tags",
|
|
177
|
+
"log_level",
|
|
178
|
+
]);
|
|
179
|
+
function splitHotColdConfig(config) {
|
|
180
|
+
const hot = {};
|
|
181
|
+
const cold = {};
|
|
182
|
+
for (const [key, value] of Object.entries(config)) {
|
|
183
|
+
(HOT_INSTANCE_CONFIG_KEYS.has(key) ? hot : cold)[key] = value;
|
|
184
|
+
}
|
|
185
|
+
return { hot, cold };
|
|
186
|
+
}
|
|
187
|
+
function hotConfigUpdate(config) {
|
|
188
|
+
const update = {};
|
|
189
|
+
for (const key of HOT_INSTANCE_CONFIG_KEYS)
|
|
190
|
+
update[key] = config[key] ?? null;
|
|
191
|
+
return update;
|
|
192
|
+
}
|
|
168
193
|
/**
|
|
169
194
|
* How long a delivery waits out a disconnected instance IPC before giving up.
|
|
170
195
|
*
|
|
@@ -750,6 +775,30 @@ export class FleetManager {
|
|
|
750
775
|
return null;
|
|
751
776
|
return this.instanceStateCache.get(name)?.state ?? null;
|
|
752
777
|
}
|
|
778
|
+
/**
|
|
779
|
+
* Subscription providers currently relevant to this fleet. Stopped/crashed
|
|
780
|
+
* rows do not keep a usage card alive, while persisted paused instances do.
|
|
781
|
+
* Classic channels use their effective backend merge chain, not the raw row.
|
|
782
|
+
*/
|
|
783
|
+
getActiveUsageProviderIds() {
|
|
784
|
+
const providers = new Set();
|
|
785
|
+
const add = (name, backend) => {
|
|
786
|
+
const status = this.getInstanceStatus(name);
|
|
787
|
+
if (status !== "running" && status !== "paused")
|
|
788
|
+
return;
|
|
789
|
+
const provider = usageProviderIdForBackend(backend);
|
|
790
|
+
if (provider)
|
|
791
|
+
providers.add(provider);
|
|
792
|
+
};
|
|
793
|
+
for (const [name, config] of Object.entries(this.fleetConfig?.instances ?? {})) {
|
|
794
|
+
// loadFleetConfig() has already merged the fleet default into each row.
|
|
795
|
+
add(name, config.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code");
|
|
796
|
+
}
|
|
797
|
+
for (const channel of this.classicChannels?.getAll() ?? []) {
|
|
798
|
+
add(channel.instanceName, this.classicChannels?.getBackendByInstance(channel.instanceName, this.fleetConfig?.defaults?.backend));
|
|
799
|
+
}
|
|
800
|
+
return providers;
|
|
801
|
+
}
|
|
753
802
|
isClassicInstance(name) {
|
|
754
803
|
return this.classicChannels?.getAll().some(channel => channel.instanceName === name) ?? false;
|
|
755
804
|
}
|
|
@@ -2378,7 +2427,7 @@ export class FleetManager {
|
|
|
2378
2427
|
const { getUsageSnapshot } = await import("./usage/usage-api.js");
|
|
2379
2428
|
const { renderUsageMarkdown } = await import("./usage/format-rich.js");
|
|
2380
2429
|
// slash_command is Discord-only; editReply renders Markdown natively.
|
|
2381
|
-
await data.respond(renderUsageMarkdown(await getUsageSnapshot()));
|
|
2430
|
+
await data.respond(renderUsageMarkdown(await getUsageSnapshot(false, this.getActiveUsageProviderIds())));
|
|
2382
2431
|
}
|
|
2383
2432
|
catch (err) {
|
|
2384
2433
|
await data.respond(`⚠️ Usage fetch failed: ${err.message}`);
|
|
@@ -2680,7 +2729,7 @@ export class FleetManager {
|
|
|
2680
2729
|
const { getUsageSnapshot } = await import("./usage/usage-api.js");
|
|
2681
2730
|
const { renderUsageMarkdown } = await import("./usage/format-rich.js");
|
|
2682
2731
|
// slash_command is Discord-only; editReply renders Markdown natively.
|
|
2683
|
-
await data.respond(renderUsageMarkdown(await getUsageSnapshot()));
|
|
2732
|
+
await data.respond(renderUsageMarkdown(await getUsageSnapshot(false, this.getActiveUsageProviderIds())));
|
|
2684
2733
|
}
|
|
2685
2734
|
catch (err) {
|
|
2686
2735
|
await data.respond(`⚠️ Usage fetch failed: ${err.message}`);
|
|
@@ -7639,7 +7688,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
7639
7688
|
/**
|
|
7640
7689
|
* Hot-reload: re-read fleet.yaml and reconcile running instances.
|
|
7641
7690
|
* Starts new, stops removed, restarts modified instances.
|
|
7642
|
-
*
|
|
7691
|
+
* Whitelisted runtime fields are pushed into live daemons; all other instance
|
|
7692
|
+
* fields, plus cold fleet-level settings, retain restart semantics.
|
|
7643
7693
|
*/
|
|
7644
7694
|
async reconcileInstances() {
|
|
7645
7695
|
if (!this.configPath)
|
|
@@ -7693,9 +7743,16 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
7693
7743
|
this.scheduler?.reload();
|
|
7694
7744
|
const newInstances = this.fleetConfig.instances;
|
|
7695
7745
|
const topicMode = this.fleetConfig?.channel?.mode === "topic";
|
|
7696
|
-
// Detect fleet-level
|
|
7697
|
-
|
|
7698
|
-
const
|
|
7746
|
+
// Detect fleet-level changes which still need a restart. Hot defaults are
|
|
7747
|
+
// reconciled below and must not produce a misleading restart warning.
|
|
7748
|
+
const oldDefaultCold = oldConfig?.defaults
|
|
7749
|
+
? splitHotColdConfig(oldConfig.defaults).cold
|
|
7750
|
+
: {};
|
|
7751
|
+
const newDefaultCold = this.fleetConfig?.defaults
|
|
7752
|
+
? splitHotColdConfig(this.fleetConfig.defaults).cold
|
|
7753
|
+
: {};
|
|
7754
|
+
const oldFleetLevel = JSON.stringify({ channel: oldConfig?.channel, defaults: oldDefaultCold });
|
|
7755
|
+
const newFleetLevel = JSON.stringify({ channel: this.fleetConfig?.channel, defaults: newDefaultCold });
|
|
7699
7756
|
if (oldFleetLevel !== newFleetLevel) {
|
|
7700
7757
|
this.logger.warn("Fleet-level config changed (channel/defaults) — use /restart for full effect");
|
|
7701
7758
|
}
|
|
@@ -7707,7 +7764,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
7707
7764
|
await this.stopInstance(name).catch(err => this.logger.error({ err, name }, "Failed to stop removed instance"));
|
|
7708
7765
|
}
|
|
7709
7766
|
}
|
|
7710
|
-
// Start new +
|
|
7767
|
+
// Start new + reconcile modified instances. Hot values are always sent as a
|
|
7768
|
+
// complete snapshot: Settings mutates FleetManager's config before SIGHUP,
|
|
7769
|
+
// so an old/new diff alone can miss the live daemon's stale value.
|
|
7711
7770
|
for (const [name, config] of Object.entries(newInstances)) {
|
|
7712
7771
|
if (!this.daemons.has(name)) {
|
|
7713
7772
|
// New instance — startInstance already calls connectIpcToInstance
|
|
@@ -7715,14 +7774,33 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
7715
7774
|
await this.startInstance(name, config, topicMode).catch(err => this.logger.error({ err, name }, "Failed to start new instance"));
|
|
7716
7775
|
}
|
|
7717
7776
|
else if (oldConfig?.instances[name]) {
|
|
7718
|
-
|
|
7719
|
-
|
|
7777
|
+
const daemon = this.daemons.get(name);
|
|
7778
|
+
const runtimeConfig = daemon.getConfigSnapshot?.() ?? oldConfig.instances[name];
|
|
7779
|
+
const oldParts = splitHotColdConfig(runtimeConfig);
|
|
7780
|
+
const newParts = splitHotColdConfig(config);
|
|
7781
|
+
// Every field not explicitly classified hot is cold by default.
|
|
7782
|
+
if (!isDeepStrictEqual(oldParts.cold, newParts.cold)) {
|
|
7720
7783
|
this.logger.info({ name }, "Instance config changed — restarting");
|
|
7721
7784
|
await this.stopInstance(name).catch(() => { });
|
|
7722
7785
|
await this.startInstance(name, config, topicMode).catch(err => this.logger.error({ err, name }, "Failed to restart modified instance"));
|
|
7723
7786
|
}
|
|
7787
|
+
else if (!isDeepStrictEqual(oldParts.hot, newParts.hot)) {
|
|
7788
|
+
const update = hotConfigUpdate(config);
|
|
7789
|
+
const ipc = this.instanceIpcClients.get(name);
|
|
7790
|
+
const sent = ipc?.connected === true && ipc.send({ type: "config_update", config: update });
|
|
7791
|
+
if (!sent) {
|
|
7792
|
+
// Daemon is in-process, so a reconnect gap must not leave runtime
|
|
7793
|
+
// state stale. Normal operation still uses the explicit IPC contract.
|
|
7794
|
+
daemon.applyConfigUpdate(update);
|
|
7795
|
+
this.logger.warn({ name }, "Config-update IPC unavailable — applied hot config in-process");
|
|
7796
|
+
}
|
|
7797
|
+
this.logger.info({ name, fields: [...HOT_INSTANCE_CONFIG_KEYS] }, "Instance hot config reloaded");
|
|
7798
|
+
}
|
|
7724
7799
|
}
|
|
7725
7800
|
}
|
|
7801
|
+
// warm_cap is fleet-owned; enforce the reloaded value immediately against
|
|
7802
|
+
// currently idle instances instead of waiting for a future state edge.
|
|
7803
|
+
this.enforceWarmCap();
|
|
7726
7804
|
this.logger.info({ running: this.daemons.size, configured: Object.keys(newInstances).length }, "Reconcile complete");
|
|
7727
7805
|
}
|
|
7728
7806
|
async restartInstances() {
|