@songsid/agend 2.1.4-beta.14 → 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/backend/kiro.js +7 -5
- package/dist/backend/kiro.js.map +1 -1
- package/dist/daemon.d.ts +28 -1
- package/dist/daemon.js +108 -10
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-context.d.ts +2 -0
- package/dist/fleet-manager.d.ts +21 -3
- package/dist/fleet-manager.js +153 -16
- 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;
|
|
@@ -289,6 +295,13 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
289
295
|
*/
|
|
290
296
|
private enforceWarmCap;
|
|
291
297
|
private waitForInstanceIdle;
|
|
298
|
+
/**
|
|
299
|
+
* Ask the daemon to capture the pane before answering, then wait for any state
|
|
300
|
+
* report produced after this request. The ordinary query is intentionally
|
|
301
|
+
* cache-only (idle gates call it frequently); this opt-in refresh is reserved
|
|
302
|
+
* for lifecycle decisions where a stale state would strand UI.
|
|
303
|
+
*/
|
|
304
|
+
private refreshInstanceExecutionState;
|
|
292
305
|
private deliverWithIdleGate;
|
|
293
306
|
/**
|
|
294
307
|
* Hand a payload to an instance's IPC, waiting out a *transient* disconnect.
|
|
@@ -712,10 +725,14 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
712
725
|
private startProgressTicker;
|
|
713
726
|
/**
|
|
714
727
|
* After a reply: give the instance REPLY_RETIRE_GRACE_MS to resume working; if
|
|
715
|
-
* it has not, retire its button.
|
|
716
|
-
*
|
|
728
|
+
* it has not, retire its button. The daemon is asked for a fresh pane capture
|
|
729
|
+
* before deciding. Reading only the transition cache stranded the first
|
|
730
|
+
* post-restart bubble when its startup "working" report never got a matching
|
|
731
|
+
* idle edge. Re-arming replaces the previous timer, so a burst of replies ends
|
|
732
|
+
* with exactly one pending check.
|
|
717
733
|
*/
|
|
718
734
|
private armReplyGrace;
|
|
735
|
+
private finishReplyGrace;
|
|
719
736
|
/** Retire (delete) every cancel button belonging to an instance. */
|
|
720
737
|
private retireInstanceButtons;
|
|
721
738
|
/** Begin retiring one button (delete + bounded retry on failure). Idempotent:
|
|
@@ -999,7 +1016,8 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
999
1016
|
/**
|
|
1000
1017
|
* Hot-reload: re-read fleet.yaml and reconcile running instances.
|
|
1001
1018
|
* Starts new, stops removed, restarts modified instances.
|
|
1002
|
-
*
|
|
1019
|
+
* Whitelisted runtime fields are pushed into live daemons; all other instance
|
|
1020
|
+
* fields, plus cold fleet-level settings, retain restart semantics.
|
|
1003
1021
|
*/
|
|
1004
1022
|
private reconcileInstances;
|
|
1005
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";
|
|
@@ -107,6 +108,8 @@ const CANCEL_BTN_IDLE_RETIRE_GRACE_MS = 2_000;
|
|
|
107
108
|
* button alone (the idle edge retires it when the run really ends).
|
|
108
109
|
*/
|
|
109
110
|
const REPLY_RETIRE_GRACE_MS = 2 * 60_000;
|
|
111
|
+
/** Bound for the daemon to capture the pane and answer a post-reply state query. */
|
|
112
|
+
const REPLY_STATE_REFRESH_TIMEOUT_MS = 2_000;
|
|
110
113
|
/**
|
|
111
114
|
* The daemon only broadcasts execution state on TRANSITIONS, so a long
|
|
112
115
|
* single-state run sends nothing for hours. The idle backstop therefore pokes a
|
|
@@ -163,6 +166,30 @@ const DELIVERY_STATUS_EMOJIS = new Set(["👀", "⏳", "✅", "❌"]);
|
|
|
163
166
|
* emoji never changes the documented delivery-state protocol.
|
|
164
167
|
*/
|
|
165
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
|
+
}
|
|
166
193
|
/**
|
|
167
194
|
* How long a delivery waits out a disconnected instance IPC before giving up.
|
|
168
195
|
*
|
|
@@ -748,6 +775,30 @@ export class FleetManager {
|
|
|
748
775
|
return null;
|
|
749
776
|
return this.instanceStateCache.get(name)?.state ?? null;
|
|
750
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
|
+
}
|
|
751
802
|
isClassicInstance(name) {
|
|
752
803
|
return this.classicChannels?.getAll().some(channel => channel.instanceName === name) ?? false;
|
|
753
804
|
}
|
|
@@ -899,6 +950,48 @@ export class FleetManager {
|
|
|
899
950
|
query();
|
|
900
951
|
});
|
|
901
952
|
}
|
|
953
|
+
/**
|
|
954
|
+
* Ask the daemon to capture the pane before answering, then wait for any state
|
|
955
|
+
* report produced after this request. The ordinary query is intentionally
|
|
956
|
+
* cache-only (idle gates call it frequently); this opt-in refresh is reserved
|
|
957
|
+
* for lifecycle decisions where a stale state would strand UI.
|
|
958
|
+
*/
|
|
959
|
+
refreshInstanceExecutionState(instanceName, timeoutMs) {
|
|
960
|
+
const ipc = this.instanceIpcClients.get(instanceName);
|
|
961
|
+
if (!ipc?.connected)
|
|
962
|
+
return Promise.resolve(false);
|
|
963
|
+
const previous = this.instanceStateCache.get(instanceName);
|
|
964
|
+
return new Promise(resolve => {
|
|
965
|
+
let settled = false;
|
|
966
|
+
const finish = (refreshed) => {
|
|
967
|
+
if (settled)
|
|
968
|
+
return;
|
|
969
|
+
settled = true;
|
|
970
|
+
clearTimeout(timeout);
|
|
971
|
+
const waiters = this.instanceIdleWaiters.get(instanceName);
|
|
972
|
+
waiters?.delete(check);
|
|
973
|
+
if (waiters?.size === 0)
|
|
974
|
+
this.instanceIdleWaiters.delete(instanceName);
|
|
975
|
+
resolve(refreshed);
|
|
976
|
+
};
|
|
977
|
+
const check = () => {
|
|
978
|
+
if (this.instanceStateCache.get(instanceName) !== previous)
|
|
979
|
+
finish(true);
|
|
980
|
+
};
|
|
981
|
+
const waiters = this.instanceIdleWaiters.get(instanceName) ?? new Set();
|
|
982
|
+
waiters.add(check);
|
|
983
|
+
this.instanceIdleWaiters.set(instanceName, waiters);
|
|
984
|
+
const timeout = setTimeout(() => finish(false), timeoutMs);
|
|
985
|
+
timeout.unref?.();
|
|
986
|
+
const sent = ipc.send({
|
|
987
|
+
type: "query_instance_state",
|
|
988
|
+
requestId: `reply-grace-${Date.now()}`,
|
|
989
|
+
refresh: true,
|
|
990
|
+
});
|
|
991
|
+
if (!sent)
|
|
992
|
+
finish(false);
|
|
993
|
+
});
|
|
994
|
+
}
|
|
902
995
|
async deliverWithIdleGate(instanceName, payload, timeoutMs) {
|
|
903
996
|
let idleObservedAfter = this.lastDeliveryAt.get(instanceName) ?? 0;
|
|
904
997
|
if (this.lifecycle.isPaused(instanceName)) {
|
|
@@ -2334,7 +2427,7 @@ export class FleetManager {
|
|
|
2334
2427
|
const { getUsageSnapshot } = await import("./usage/usage-api.js");
|
|
2335
2428
|
const { renderUsageMarkdown } = await import("./usage/format-rich.js");
|
|
2336
2429
|
// slash_command is Discord-only; editReply renders Markdown natively.
|
|
2337
|
-
await data.respond(renderUsageMarkdown(await getUsageSnapshot()));
|
|
2430
|
+
await data.respond(renderUsageMarkdown(await getUsageSnapshot(false, this.getActiveUsageProviderIds())));
|
|
2338
2431
|
}
|
|
2339
2432
|
catch (err) {
|
|
2340
2433
|
await data.respond(`⚠️ Usage fetch failed: ${err.message}`);
|
|
@@ -2636,7 +2729,7 @@ export class FleetManager {
|
|
|
2636
2729
|
const { getUsageSnapshot } = await import("./usage/usage-api.js");
|
|
2637
2730
|
const { renderUsageMarkdown } = await import("./usage/format-rich.js");
|
|
2638
2731
|
// slash_command is Discord-only; editReply renders Markdown natively.
|
|
2639
|
-
await data.respond(renderUsageMarkdown(await getUsageSnapshot()));
|
|
2732
|
+
await data.respond(renderUsageMarkdown(await getUsageSnapshot(false, this.getActiveUsageProviderIds())));
|
|
2640
2733
|
}
|
|
2641
2734
|
catch (err) {
|
|
2642
2735
|
await data.respond(`⚠️ Usage fetch failed: ${err.message}`);
|
|
@@ -5424,8 +5517,11 @@ export class FleetManager {
|
|
|
5424
5517
|
}
|
|
5425
5518
|
/**
|
|
5426
5519
|
* After a reply: give the instance REPLY_RETIRE_GRACE_MS to resume working; if
|
|
5427
|
-
* it has not, retire its button.
|
|
5428
|
-
*
|
|
5520
|
+
* it has not, retire its button. The daemon is asked for a fresh pane capture
|
|
5521
|
+
* before deciding. Reading only the transition cache stranded the first
|
|
5522
|
+
* post-restart bubble when its startup "working" report never got a matching
|
|
5523
|
+
* idle edge. Re-arming replaces the previous timer, so a burst of replies ends
|
|
5524
|
+
* with exactly one pending check.
|
|
5429
5525
|
*/
|
|
5430
5526
|
armReplyGrace(instanceName) {
|
|
5431
5527
|
for (const entry of this.cancelButtons.values()) {
|
|
@@ -5437,14 +5533,26 @@ export class FleetManager {
|
|
|
5437
5533
|
entry.replyGraceTimer = undefined;
|
|
5438
5534
|
if (!this.cancelButtons.has(entry.messageId))
|
|
5439
5535
|
return;
|
|
5440
|
-
|
|
5441
|
-
return; // resumed — a long run keeps its button
|
|
5442
|
-
this.logger.info({ instanceName, messageId: entry.messageId }, "Cancel button retired — no work resumed after reply");
|
|
5443
|
-
this.retireButton(entry);
|
|
5536
|
+
void this.finishReplyGrace(instanceName, entry);
|
|
5444
5537
|
}, REPLY_RETIRE_GRACE_MS);
|
|
5445
5538
|
entry.replyGraceTimer.unref?.();
|
|
5446
5539
|
}
|
|
5447
5540
|
}
|
|
5541
|
+
async finishReplyGrace(instanceName, entry) {
|
|
5542
|
+
const refreshed = await this.refreshInstanceExecutionState(instanceName, REPLY_STATE_REFRESH_TIMEOUT_MS);
|
|
5543
|
+
if (!this.cancelButtons.has(entry.messageId) || entry.retiring)
|
|
5544
|
+
return;
|
|
5545
|
+
if (!refreshed) {
|
|
5546
|
+
// Fail safe: without an authoritative answer, retain a potentially live
|
|
5547
|
+
// Cancel button. The 5-minute/30-minute/24-hour safety nets still apply.
|
|
5548
|
+
this.logger.debug({ instanceName, messageId: entry.messageId }, "Cancel reply-grace state refresh timed out");
|
|
5549
|
+
return;
|
|
5550
|
+
}
|
|
5551
|
+
if (!this.getInstanceIdle(instanceName))
|
|
5552
|
+
return; // genuine long run keeps its button
|
|
5553
|
+
this.logger.info({ instanceName, messageId: entry.messageId }, "Cancel button retired — no work resumed after reply");
|
|
5554
|
+
this.retireButton(entry);
|
|
5555
|
+
}
|
|
5448
5556
|
/** Retire (delete) every cancel button belonging to an instance. */
|
|
5449
5557
|
retireInstanceButtons(instanceName) {
|
|
5450
5558
|
// Snapshot first — retireButton may delete entries from the map on success.
|
|
@@ -7580,7 +7688,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
7580
7688
|
/**
|
|
7581
7689
|
* Hot-reload: re-read fleet.yaml and reconcile running instances.
|
|
7582
7690
|
* Starts new, stops removed, restarts modified instances.
|
|
7583
|
-
*
|
|
7691
|
+
* Whitelisted runtime fields are pushed into live daemons; all other instance
|
|
7692
|
+
* fields, plus cold fleet-level settings, retain restart semantics.
|
|
7584
7693
|
*/
|
|
7585
7694
|
async reconcileInstances() {
|
|
7586
7695
|
if (!this.configPath)
|
|
@@ -7634,9 +7743,16 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
7634
7743
|
this.scheduler?.reload();
|
|
7635
7744
|
const newInstances = this.fleetConfig.instances;
|
|
7636
7745
|
const topicMode = this.fleetConfig?.channel?.mode === "topic";
|
|
7637
|
-
// Detect fleet-level
|
|
7638
|
-
|
|
7639
|
-
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 });
|
|
7640
7756
|
if (oldFleetLevel !== newFleetLevel) {
|
|
7641
7757
|
this.logger.warn("Fleet-level config changed (channel/defaults) — use /restart for full effect");
|
|
7642
7758
|
}
|
|
@@ -7648,7 +7764,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
7648
7764
|
await this.stopInstance(name).catch(err => this.logger.error({ err, name }, "Failed to stop removed instance"));
|
|
7649
7765
|
}
|
|
7650
7766
|
}
|
|
7651
|
-
// 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.
|
|
7652
7770
|
for (const [name, config] of Object.entries(newInstances)) {
|
|
7653
7771
|
if (!this.daemons.has(name)) {
|
|
7654
7772
|
// New instance — startInstance already calls connectIpcToInstance
|
|
@@ -7656,14 +7774,33 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
7656
7774
|
await this.startInstance(name, config, topicMode).catch(err => this.logger.error({ err, name }, "Failed to start new instance"));
|
|
7657
7775
|
}
|
|
7658
7776
|
else if (oldConfig?.instances[name]) {
|
|
7659
|
-
|
|
7660
|
-
|
|
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)) {
|
|
7661
7783
|
this.logger.info({ name }, "Instance config changed — restarting");
|
|
7662
7784
|
await this.stopInstance(name).catch(() => { });
|
|
7663
7785
|
await this.startInstance(name, config, topicMode).catch(err => this.logger.error({ err, name }, "Failed to restart modified instance"));
|
|
7664
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
|
+
}
|
|
7665
7799
|
}
|
|
7666
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();
|
|
7667
7804
|
this.logger.info({ running: this.daemons.size, configured: Object.keys(newInstances).length }, "Reconcile complete");
|
|
7668
7805
|
}
|
|
7669
7806
|
async restartInstances() {
|