@songsid/agend 2.1.4-beta.11 → 2.1.4-beta.13

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.
@@ -9,7 +9,7 @@ import { formatUpdateProgress } from "./update-progress.js";
9
9
  import { sdNotify, sdNotifyBlocking } from "./sd-notify.js";
10
10
  import { readFleetMemory } from "./process-memory.js";
11
11
  import { ReplyDeduper } from "./reply-dedup.js";
12
- import { isScalar, parseDocument } from "yaml";
12
+ import { isMap, isScalar, parseDocument } from "yaml";
13
13
  const __filename = fileURLToPath(import.meta.url);
14
14
  const __dirname = dirname(__filename);
15
15
  /** Fallback access policy for a channel with no `access:` block — open (no gate). */
@@ -54,6 +54,7 @@ import { releaseProcessFleetLock } from "./fleet-lock.js";
54
54
  import { GENERAL_PAUSE_ERROR, isGeneralInstance } from "./general-instance.js";
55
55
  import { loadOrCreateWebToken, WEB_TOKEN_INVALID_MESSAGE } from "./web-auth.js";
56
56
  import { RestartProgress } from "./restart-progress.js";
57
+ import { collectRedundantInstanceDefaultPaths } from "./fleet-yaml-slim.js";
57
58
  import { getTmuxSession } from "./config.js";
58
59
  export function resolveReplyThreadId(argsThreadId, instanceConfig) {
59
60
  if (typeof argsThreadId === "string" && argsThreadId.length > 0) {
@@ -1453,6 +1454,7 @@ export class FleetManager {
1453
1454
  // Rotate fleet.log if oversized (before any logging)
1454
1455
  rotateLogIfNeeded(join(this.dataDir, "fleet.log"));
1455
1456
  const fleet = this.loadConfig(configPath);
1457
+ this.slimFleetConfigAtStartup();
1456
1458
  setLocale(detectLocale(fleet)); // user-facing text language (fleet.yaml defaults.locale / timezone)
1457
1459
  const savedUpdateProgress = readUpdateProgress(this.dataDir);
1458
1460
  const pendingUpdateProgress = savedUpdateProgress
@@ -3902,7 +3904,29 @@ export class FleetManager {
3902
3904
  }
3903
3905
  /** Resolve display name for an instance, fallback to instance name. */
3904
3906
  resolveDisplayName(instanceName) {
3905
- return this.fleetConfig?.instances[instanceName]?.display_name ?? instanceName;
3907
+ return this.fleetConfig?.instances[instanceName]?.display_name
3908
+ ?? this.classicChannels?.getAll().find(ch => ch.instanceName === instanceName)?.displayName
3909
+ ?? instanceName;
3910
+ }
3911
+ /** Persist identity to the instance's actual config store. Classic instances
3912
+ * are registry rows in classicBot.yaml, not fleet.yaml instance entries. */
3913
+ setInstanceDisplayName(instanceName, displayName) {
3914
+ const fleetInstance = this.fleetConfig?.instances[instanceName];
3915
+ if (fleetInstance) {
3916
+ fleetInstance.display_name = displayName;
3917
+ this.saveFleetConfig();
3918
+ return true;
3919
+ }
3920
+ return this.classicChannels?.setDisplayNameByInstance(instanceName, displayName) ?? false;
3921
+ }
3922
+ setInstanceDescription(instanceName, description) {
3923
+ const fleetInstance = this.fleetConfig?.instances[instanceName];
3924
+ if (fleetInstance) {
3925
+ fleetInstance.description = description;
3926
+ this.saveFleetConfig();
3927
+ return true;
3928
+ }
3929
+ return this.classicChannels?.setDescriptionByInstance(instanceName, description) ?? false;
3906
3930
  }
3907
3931
  handleSetDisplayName(instanceName, msg) {
3908
3932
  const fleetRequestId = msg.fleetRequestId;
@@ -3915,8 +3939,10 @@ export class FleetManager {
3915
3939
  ipc.send({ type: "fleet_display_name_response", fleetRequestId, error: "Name must be 1-30 characters" });
3916
3940
  return;
3917
3941
  }
3918
- this.fleetConfig.instances[instanceName].display_name = displayName;
3919
- this.saveFleetConfig();
3942
+ if (!this.setInstanceDisplayName(instanceName, displayName)) {
3943
+ ipc.send({ type: "fleet_display_name_response", fleetRequestId, error: `Instance '${instanceName}' not found` });
3944
+ return;
3945
+ }
3920
3946
  this.logger.info({ instanceName, displayName }, "Display name set");
3921
3947
  ipc.send({ type: "fleet_display_name_response", fleetRequestId, result: { display_name: displayName } });
3922
3948
  }
@@ -3931,8 +3957,10 @@ export class FleetManager {
3931
3957
  ipc.send({ type: "fleet_description_response", fleetRequestId, error: "Description cannot be empty" });
3932
3958
  return;
3933
3959
  }
3934
- this.fleetConfig.instances[instanceName].description = description;
3935
- this.saveFleetConfig();
3960
+ if (!this.setInstanceDescription(instanceName, description)) {
3961
+ ipc.send({ type: "fleet_description_response", fleetRequestId, error: `Instance '${instanceName}' not found` });
3962
+ return;
3963
+ }
3936
3964
  this.logger.info({ instanceName, description: description.slice(0, 80) }, "Description set");
3937
3965
  ipc.send({ type: "fleet_description_response", fleetRequestId, result: { description } });
3938
3966
  }
@@ -4061,8 +4089,8 @@ export class FleetManager {
4061
4089
  return { error: "Fleet config not available" };
4062
4090
  if (!name || name.length > 30)
4063
4091
  return { error: "Name must be 1-30 characters" };
4064
- this.fleetConfig.instances[instance].display_name = name;
4065
- this.saveFleetConfig();
4092
+ if (!this.setInstanceDisplayName(instance, name))
4093
+ return { error: `Instance '${instance}' not found` };
4066
4094
  return { display_name: name };
4067
4095
  }
4068
4096
  async handleSetDescriptionHttp(instance, description) {
@@ -4070,8 +4098,8 @@ export class FleetManager {
4070
4098
  return { error: "Fleet config not available" };
4071
4099
  if (!description)
4072
4100
  return { error: "Description cannot be empty" };
4073
- this.fleetConfig.instances[instance].description = description;
4074
- this.saveFleetConfig();
4101
+ if (!this.setInstanceDescription(instance, description))
4102
+ return { error: `Instance '${instance}' not found` };
4075
4103
  return { description };
4076
4104
  }
4077
4105
  summarizeToolCall(tool, args) {
@@ -4203,7 +4231,8 @@ export class FleetManager {
4203
4231
  }
4204
4232
  /**
4205
4233
  * Patch only values changed in the effective config into the original YAML
4206
- * document. Unknown keys, explicit overrides and comments remain untouched.
4234
+ * document. Unknown keys and comments remain untouched; redundant
4235
+ * non-identity instance leaves are canonicalized to inheritance afterward.
4207
4236
  */
4208
4237
  saveFleetConfig(explicitPatches = []) {
4209
4238
  if (!this.fleetConfig || !this.configPath)
@@ -4219,9 +4248,9 @@ export class FleetManager {
4219
4248
  }
4220
4249
  this.rawFleetConfig = loadRawFleetConfig(this.configPath);
4221
4250
  this.patchFleetDocument(this.rawFleetDocument, [], this.savedFleetConfigSnapshot, this.fleetConfig);
4222
- // Settings edits are expressed against the raw config. Persist them even
4223
- // when the chosen override equals the inherited effective value, a case
4224
- // the before/after runtime diff cannot observe.
4251
+ // Settings edits are expressed against the raw config, so apply them before
4252
+ // canonicalization. A non-identity value equal to its inherited default is
4253
+ // intentionally stored as inheritance rather than an explicit duplicate.
4225
4254
  for (const patch of explicitPatches) {
4226
4255
  if (patch.remove) {
4227
4256
  // YAML's deleteIn throws when an inherited nested key has no raw parent
@@ -4236,7 +4265,24 @@ export class FleetManager {
4236
4265
  this.patchFleetDocument(this.rawFleetDocument, patch.path, before, patch.value);
4237
4266
  }
4238
4267
  }
4268
+ const rawAfterPatches = this.rawFleetDocument.toJS();
4269
+ const redundantPaths = collectRedundantInstanceDefaultPaths(rawAfterPatches);
4270
+ for (const path of redundantPaths) {
4271
+ if (this.rawFleetDocument.hasIn(path))
4272
+ this.rawFleetDocument.deleteIn(path);
4273
+ // Avoid leaving empty operational maps such as `terminal: {}` while
4274
+ // preserving the instance mapping itself and all surrounding comments.
4275
+ for (let depth = path.length - 1; depth > 2; depth--) {
4276
+ const parentPath = path.slice(0, depth);
4277
+ const parent = this.rawFleetDocument.getIn(parentPath, true);
4278
+ if (!isMap(parent) || parent.items.length > 0)
4279
+ break;
4280
+ this.rawFleetDocument.deleteIn(parentPath);
4281
+ }
4282
+ }
4239
4283
  const output = String(this.rawFleetDocument);
4284
+ if (redundantPaths.length > 0)
4285
+ this.writeFleetConfigBackup(source);
4240
4286
  const tempPath = `${this.configPath}.tmp-${process.pid}`;
4241
4287
  writeFileSync(tempPath, output, "utf-8");
4242
4288
  if (existsSync(this.configPath))
@@ -4244,7 +4290,36 @@ export class FleetManager {
4244
4290
  renameSync(tempPath, this.configPath);
4245
4291
  this.rawFleetConfig = loadRawFleetConfig(this.configPath);
4246
4292
  this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
4247
- this.logger.info({ path: this.configPath }, "Saved fleet config (lossless patch)");
4293
+ this.logger.info({ path: this.configPath, strippedDefaults: redundantPaths.length }, "Saved fleet config (lossless patch)");
4294
+ }
4295
+ /** One-time upgrade migration; invalid YAML is never rewritten. */
4296
+ slimFleetConfigAtStartup() {
4297
+ const redundantPaths = collectRedundantInstanceDefaultPaths(this.rawFleetConfig);
4298
+ if (redundantPaths.length === 0)
4299
+ return;
4300
+ const validation = validateFleetConfig(this.rawFleetConfig);
4301
+ if (!validation.valid) {
4302
+ this.logger.warn({ errors: validation.errors, redundantDefaults: redundantPaths.length }, "Skipping fleet.yaml default slimming because the raw config is invalid");
4303
+ return;
4304
+ }
4305
+ try {
4306
+ this.saveFleetConfig();
4307
+ this.logger.info({ strippedDefaults: redundantPaths.length, backup: `${this.configPath}.bak` }, "Slimmed redundant instance defaults in fleet.yaml");
4308
+ }
4309
+ catch (err) {
4310
+ // A migration must not turn a previously bootable fleet into an outage.
4311
+ this.logger.warn({ err }, "Could not slim fleet.yaml; continuing with the original config");
4312
+ }
4313
+ }
4314
+ writeFleetConfigBackup(source) {
4315
+ if (!this.configPath)
4316
+ return;
4317
+ const backupPath = `${this.configPath}.bak`;
4318
+ const tempPath = `${backupPath}.tmp-${process.pid}`;
4319
+ writeFileSync(tempPath, source, "utf-8");
4320
+ if (existsSync(this.configPath))
4321
+ chmodSync(tempPath, statSync(this.configPath).mode);
4322
+ renameSync(tempPath, backupPath);
4248
4323
  }
4249
4324
  patchFleetDocument(document, path, before, after) {
4250
4325
  if (Object.is(before, after))
@@ -4296,7 +4371,12 @@ export class FleetManager {
4296
4371
  currentNode.value = after;
4297
4372
  }
4298
4373
  else {
4299
- document.setIn(path, after);
4374
+ // Use a YAML collection node for newly-added objects. Passing the raw
4375
+ // object creates a scalar wrapper: it serializes, but nested hasIn /
4376
+ // deleteIn cannot traverse it (notably when slimming create_instance).
4377
+ document.setIn(path, after !== null && typeof after === "object"
4378
+ ? document.createNode(after)
4379
+ : after);
4300
4380
  }
4301
4381
  }
4302
4382
  }
@@ -7078,6 +7158,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
7078
7158
  return;
7079
7159
  const workDir = join(getAgendHome(), "workspaces", instanceName);
7080
7160
  ensureWorkspaceGit(workDir);
7161
+ const classicIdentity = this.classicChannels?.getAll().find(ch => ch.instanceName === instanceName);
7081
7162
  const config = {
7082
7163
  ...DEFAULT_INSTANCE_CONFIG,
7083
7164
  ...this.fleetConfig?.defaults,
@@ -7085,6 +7166,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
7085
7166
  lightweight: true,
7086
7167
  ...(backend ? { backend } : {}),
7087
7168
  ...(model ? { model } : {}),
7169
+ ...(classicIdentity?.displayName ? { display_name: classicIdentity.displayName } : {}),
7170
+ ...(classicIdentity?.description ? { description: classicIdentity.description } : {}),
7088
7171
  ...(autoPauseAfter !== undefined ? { auto_pause_after: autoPauseAfter } : {}),
7089
7172
  ...(preTaskCommand ? { pre_task_command: preTaskCommand } : {}),
7090
7173
  };