@songsid/agend 2.1.0 → 2.1.1-beta.10

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.
Files changed (65) hide show
  1. package/README.md +1 -0
  2. package/README.zh-TW.md +1 -0
  3. package/dist/agent-cli-instructions.md +9 -0
  4. package/dist/backend/antigravity.d.ts +4 -1
  5. package/dist/backend/antigravity.js +15 -2
  6. package/dist/backend/antigravity.js.map +1 -1
  7. package/dist/backend/codex.d.ts +1 -0
  8. package/dist/backend/codex.js +30 -2
  9. package/dist/backend/codex.js.map +1 -1
  10. package/dist/backend/grok.d.ts +1 -0
  11. package/dist/backend/grok.js +8 -1
  12. package/dist/backend/grok.js.map +1 -1
  13. package/dist/backend/kiro.d.ts +7 -0
  14. package/dist/backend/kiro.js +15 -6
  15. package/dist/backend/kiro.js.map +1 -1
  16. package/dist/backend/types.d.ts +11 -6
  17. package/dist/backend/types.js +48 -4
  18. package/dist/backend/types.js.map +1 -1
  19. package/dist/cli.js +16 -8
  20. package/dist/cli.js.map +1 -1
  21. package/dist/config-validator.js +18 -0
  22. package/dist/config-validator.js.map +1 -1
  23. package/dist/config.js +5 -0
  24. package/dist/config.js.map +1 -1
  25. package/dist/daemon.d.ts +11 -0
  26. package/dist/daemon.js +56 -32
  27. package/dist/daemon.js.map +1 -1
  28. package/dist/fleet-context.d.ts +2 -0
  29. package/dist/fleet-manager.d.ts +25 -2
  30. package/dist/fleet-manager.js +127 -22
  31. package/dist/fleet-manager.js.map +1 -1
  32. package/dist/general-knowledge/skills/tui-effort/SKILL.md +115 -0
  33. package/dist/instance-lifecycle.d.ts +8 -0
  34. package/dist/instance-lifecycle.js +57 -4
  35. package/dist/instance-lifecycle.js.map +1 -1
  36. package/dist/instructions.js +10 -3
  37. package/dist/instructions.js.map +1 -1
  38. package/dist/locale.js +2 -0
  39. package/dist/locale.js.map +1 -1
  40. package/dist/sd-notify.d.ts +18 -0
  41. package/dist/sd-notify.js +41 -2
  42. package/dist/sd-notify.js.map +1 -1
  43. package/dist/service-installer.d.ts +7 -0
  44. package/dist/service-installer.js +43 -3
  45. package/dist/service-installer.js.map +1 -1
  46. package/dist/tmux-control.js +7 -1
  47. package/dist/tmux-control.js.map +1 -1
  48. package/dist/tmux-manager.d.ts +16 -1
  49. package/dist/tmux-manager.js +69 -3
  50. package/dist/tmux-manager.js.map +1 -1
  51. package/dist/topic-commands.js +6 -2
  52. package/dist/topic-commands.js.map +1 -1
  53. package/dist/types.d.ts +13 -0
  54. package/dist/ui/view.html +170 -33
  55. package/dist/update-check.d.ts +22 -0
  56. package/dist/update-check.js +44 -0
  57. package/dist/update-check.js.map +1 -0
  58. package/dist/view-api.d.ts +7 -0
  59. package/dist/view-api.js +56 -12
  60. package/dist/view-api.js.map +1 -1
  61. package/package.json +1 -1
  62. package/templates/systemd.service.ejs +9 -1
  63. package/dist/fleet-system-prompt.d.ts +0 -11
  64. package/dist/fleet-system-prompt.js +0 -61
  65. package/dist/fleet-system-prompt.js.map +0 -1
@@ -42,6 +42,7 @@ import { handleSettingsRequest } from "./settings-api.js";
42
42
  import { setLocale, detectLocale, t } from "./locale.js";
43
43
  import { handleAgentRequest } from "./agent-endpoint.js";
44
44
  import { ClassicChannelManager, getClassicBackendChoices, isSelectableClassicBackend, readClassicLastActivityAt } from "./classic-channel-manager.js";
45
+ import { validateFleetConfig } from "./config-validator.js";
45
46
  import { readLastInboundAt } from "./daemon.js";
46
47
  import { clearPausedMarker } from "./pause-marker.js";
47
48
  import { getTmuxSession } from "./config.js";
@@ -88,6 +89,8 @@ const MODEL_SELECT_CALLBACK_PREFIX = "model-select:";
88
89
  const CLI_ENV_TTL_MS = 24 * 60 * 60 * 1000; // /model reads cached CLI env within 24h
89
90
  export class FleetManager {
90
91
  dataDir;
92
+ static signalTarget = null;
93
+ static sighupHandlerInstalled = false;
91
94
  children = new Map();
92
95
  lifecycle;
93
96
  /** @deprecated Use lifecycle.daemons — kept for backward compat */
@@ -112,6 +115,12 @@ export class FleetManager {
112
115
  instanceIpcClients = new Map();
113
116
  scheduler = null;
114
117
  configPath = "";
118
+ /** SIGHUPs received before startAll finishes are replayed once startup is safe. */
119
+ startupComplete = false;
120
+ /** Coalesces one or more SIGHUPs into at most one follow-up reconciliation. */
121
+ reloadPending = false;
122
+ /** A running reconciliation; only one may mutate lifecycle/config state at a time. */
123
+ reconcileInFlight = null;
115
124
  logger = createLogger("info");
116
125
  topicCommands;
117
126
  // sessionName → instanceName mapping for external sessions
@@ -174,11 +183,46 @@ export class FleetManager {
174
183
  viewToken = null;
175
184
  constructor(dataDir) {
176
185
  this.dataDir = dataDir;
186
+ FleetManager.signalTarget = this;
187
+ if (!FleetManager.sighupHandlerInstalled) {
188
+ process.on("SIGHUP", () => FleetManager.signalTarget?.handleSighup());
189
+ FleetManager.sighupHandlerInstalled = true;
190
+ }
177
191
  this.lifecycle = new InstanceLifecycle(this);
178
192
  this.topicCommands = new TopicCommands(this);
179
193
  this.topicArchiver = new TopicArchiver(this);
180
194
  this.statuslineWatcher = new StatuslineWatcher(this);
181
195
  }
196
+ handleSighup() {
197
+ this.logger.info("Received SIGHUP, hot-reloading config...");
198
+ if (!this.startupComplete) {
199
+ this.reloadPending = true;
200
+ this.logger.info("Fleet startup is still in progress — queued config reload");
201
+ return;
202
+ }
203
+ this.scheduleReconcile();
204
+ }
205
+ scheduleReconcile() {
206
+ if (this.reconcileInFlight) {
207
+ this.reloadPending = true;
208
+ this.logger.info("Config reconciliation already running — coalesced reload request");
209
+ return;
210
+ }
211
+ this.reloadPending = false;
212
+ this.reconcileInFlight = this.reconcileInstances()
213
+ .catch(err => this.logger.error({ err }, "SIGHUP config reload failed"))
214
+ .finally(() => {
215
+ this.reconcileInFlight = null;
216
+ if (this.reloadPending && this.startupComplete) {
217
+ this.scheduleReconcile();
218
+ }
219
+ });
220
+ }
221
+ finishStartup() {
222
+ this.startupComplete = true;
223
+ if (this.reloadPending)
224
+ this.scheduleReconcile();
225
+ }
182
226
  // ── ArchiverContext bridge ────────────────────────────────────────────
183
227
  lastActivityMs(name) {
184
228
  return this.lastActivity.get(name) ?? 0;
@@ -796,6 +840,8 @@ export class FleetManager {
796
840
  }
797
841
  /** Start all instances from fleet config */
798
842
  async startAll(configPath) {
843
+ FleetManager.signalTarget = this;
844
+ this.startupComplete = false;
799
845
  this.configPath = configPath;
800
846
  this.loadEnvFile();
801
847
  // Rotate fleet.log if oversized (before any logging)
@@ -1184,14 +1230,6 @@ export class FleetManager {
1184
1230
  this.checkForUpdates();
1185
1231
  this.updateCheckTimer = setInterval(() => this.checkForUpdates(), 24 * 60 * 60 * 1000);
1186
1232
  }, 60 * 60 * 1000);
1187
- // SIGHUP: hot-reload instance config (add/remove/restart instances)
1188
- const onSighup = () => {
1189
- this.logger.info("Received SIGHUP, hot-reloading config...");
1190
- this.reconcileInstances()
1191
- .catch(err => this.logger.error({ err }, "SIGHUP config reload failed"));
1192
- process.once("SIGHUP", onSighup);
1193
- };
1194
- process.once("SIGHUP", onSighup);
1195
1233
  const onRestart = () => {
1196
1234
  this.logger.info("Received SIGUSR2, initiating graceful restart...");
1197
1235
  this.restartInstances()
@@ -1213,6 +1251,10 @@ export class FleetManager {
1213
1251
  });
1214
1252
  };
1215
1253
  process.once("SIGUSR1", onFullRestart);
1254
+ // A SIGHUP may arrive after the PID/general is available but before the
1255
+ // rest of startup finishes. Replay one coalesced reload only after all
1256
+ // startup-owned lifecycle work and signal handlers are in place.
1257
+ this.finishStartup();
1216
1258
  }
1217
1259
  /**
1218
1260
  * Delete inbox files older than retentionDays (by mtime). Cleans the shared
@@ -4258,22 +4300,49 @@ When users create specialized instances, suggest these configurations:
4258
4300
  catch { /* missing / stale / corrupt */ }
4259
4301
  return null;
4260
4302
  }
4261
- /** Resolve the model currently configured for a fleet or ClassicBot instance. */
4262
- currentModelForInstance(instanceName) {
4303
+ /**
4304
+ * Resolve the effective model for a fleet or ClassicBot instance, plus where it
4305
+ * came from. Single source of truth for `/model` and `/ctx` — precedence:
4306
+ * per-instance → fleet defaults → classic channel → CLI's own default (from the
4307
+ * cli-env probe cache) → unresolved.
4308
+ */
4309
+ resolveInstanceModel(instanceName) {
4310
+ const done = (model, source, reason) => ({
4311
+ model,
4312
+ source,
4313
+ reason,
4314
+ // Make an inherited CLI default legible instead of the bare word "default".
4315
+ display: source === "cli-default" ? `${model} (default)`
4316
+ : source === "unresolved" ? `default (${reason ?? "unresolved"})`
4317
+ : model,
4318
+ });
4263
4319
  const fleetInstance = this.fleetConfig?.instances[instanceName];
4264
4320
  if (fleetInstance) {
4265
- const fleetModel = fleetInstance.model ?? this.fleetConfig?.defaults?.model;
4266
- if (fleetModel?.trim())
4267
- return fleetModel.trim();
4321
+ if (fleetInstance.model?.trim())
4322
+ return done(fleetInstance.model.trim(), "instance");
4323
+ const fleetDefault = this.fleetConfig?.defaults?.model;
4324
+ if (fleetDefault?.trim())
4325
+ return done(fleetDefault.trim(), "fleet-default");
4268
4326
  }
4269
4327
  const classic = this.classicChannels?.getAll().find(ch => ch.instanceName === instanceName);
4270
4328
  if (classic) {
4271
4329
  const classicModel = this.classicChannels?.getModel(classic.channelId, classic.adapterId, this.fleetConfig?.defaults?.model);
4272
4330
  if (classicModel?.trim())
4273
- return classicModel.trim();
4274
- }
4275
- const cachedModel = this.readCliEnv(this.backendNameForInstance(instanceName))?.currentModel;
4276
- return cachedModel?.trim() || "default";
4331
+ return done(classicModel.trim(), "classic");
4332
+ }
4333
+ // Nothing configured → show what the CLI itself defaults to (kiro default_model,
4334
+ // grok "Default model:", codex config.toml, agy settings.json), cached by the probe.
4335
+ const cliEnv = this.readCliEnv(this.backendNameForInstance(instanceName));
4336
+ const cachedModel = cliEnv?.currentModel;
4337
+ if (cachedModel?.trim())
4338
+ return done(cachedModel.trim(), "cli-default");
4339
+ // Say WHY it's unresolved: no fresh probe yet vs. the CLI not exposing a default
4340
+ // (e.g. claude-code's default is account-side, opencode's is provider-side).
4341
+ return done("default", "unresolved", cliEnv ? "this CLI does not report a default" : "not probed yet");
4342
+ }
4343
+ /** Human-readable effective model, e.g. `auto (default)`. Used by /ctx. */
4344
+ modelDisplayForInstance(instanceName) {
4345
+ return this.resolveInstanceModel(instanceName).display;
4277
4346
  }
4278
4347
  modelChoiceLabel(option, currentModel) {
4279
4348
  const label = option.description ? `${option.label} — ${option.description}` : option.label;
@@ -4353,7 +4422,8 @@ When users create specialized instances, suggest these configurations:
4353
4422
  await data.respond(`No model list available for ${name}. Type \`/model <name>\` to set one directly.`);
4354
4423
  return;
4355
4424
  }
4356
- const currentModel = this.currentModelForInstance(name);
4425
+ // Raw id for ✓-matching options; display resolves an inherited CLI default.
4426
+ const { model: currentModel, display: currentDisplay } = this.resolveInstanceModel(name);
4357
4427
  const nonce = randomBytes(6).toString("hex");
4358
4428
  const choices = options.slice(0, 25).map(o => ({
4359
4429
  id: `${MODEL_SELECT_CALLBACK_PREFIX}${nonce}:${o.id}`,
@@ -4363,7 +4433,7 @@ When users create specialized instances, suggest these configurations:
4363
4433
  timer.unref?.();
4364
4434
  this.pendingModelSelects.set(nonce, { instanceName: name, model: "", userId: data.userId, channelId: data.channelId, timer, respond: data.respond });
4365
4435
  try {
4366
- await data.respondChoices(`Current model: **${currentModel}**\nSelect a new model:`, choices);
4436
+ await data.respondChoices(`Current model: **${currentDisplay}**\nSelect a new model:`, choices);
4367
4437
  }
4368
4438
  catch (err) {
4369
4439
  this.pendingModelSelects.delete(nonce);
@@ -4382,7 +4452,7 @@ When users create specialized instances, suggest these configurations:
4382
4452
  if (options.length === 0) {
4383
4453
  return `No model list available for ${instanceName}. Use \`/model <name>\` to set one directly.`;
4384
4454
  }
4385
- const currentModel = this.currentModelForInstance(instanceName);
4455
+ const { model: currentModel, display: currentDisplay } = this.resolveInstanceModel(instanceName);
4386
4456
  const nonce = randomBytes(6).toString("hex");
4387
4457
  const choices = options.slice(0, 25).map(o => ({
4388
4458
  id: `${MODEL_SELECT_CALLBACK_PREFIX}${nonce}:${o.id}`,
@@ -4402,7 +4472,7 @@ When users create specialized instances, suggest these configurations:
4402
4472
  timer.unref?.();
4403
4473
  this.pendingModelSelects.set(nonce, { instanceName, model: "", userId, channelId, timer, respond, adapter, adapterChatId: chatId, adapterThreadId: threadId });
4404
4474
  try {
4405
- const menuMessageId = await adapter.promptUser(chatId, `Current model: ${currentModel}\nSelect a new model:`, choices, { threadId });
4475
+ const menuMessageId = await adapter.promptUser(chatId, `Current model: ${currentDisplay}\nSelect a new model:`, choices, { threadId });
4406
4476
  const pending = this.pendingModelSelects.get(nonce);
4407
4477
  if (pending)
4408
4478
  pending.menuMessageId = menuMessageId;
@@ -4744,6 +4814,8 @@ When users create specialized instances, suggest these configurations:
4744
4814
  return t("classic.stopped");
4745
4815
  }
4746
4816
  async stopAll() {
4817
+ this.startupComplete = false;
4818
+ this.reloadPending = false;
4747
4819
  this.ipcStoppingInstances.add("__fleet_stopping__");
4748
4820
  sdNotify("STOPPING=1");
4749
4821
  if (this.watchdogTimer) {
@@ -4951,7 +5023,40 @@ When users create specialized instances, suggest these configurations:
4951
5023
  if (!this.configPath)
4952
5024
  return;
4953
5025
  const oldConfig = this.fleetConfig;
4954
- this.loadConfig(this.configPath);
5026
+ const previousRawConfig = this.rawFleetConfig;
5027
+ const previousRawDocument = this.rawFleetDocument;
5028
+ const previousSavedSnapshot = this.savedFleetConfigSnapshot;
5029
+ try {
5030
+ this.loadConfig(this.configPath);
5031
+ }
5032
+ catch (err) {
5033
+ this.fleetConfig = oldConfig;
5034
+ this.rawFleetConfig = previousRawConfig;
5035
+ this.rawFleetDocument = previousRawDocument;
5036
+ this.savedFleetConfigSnapshot = previousSavedSnapshot;
5037
+ throw err;
5038
+ }
5039
+ const validation = validateFleetConfig(this.rawFleetConfig);
5040
+ const oldCount = Object.keys(oldConfig?.instances ?? {}).length;
5041
+ const newCount = Object.keys(this.fleetConfig?.instances ?? {}).length;
5042
+ const removedRatio = oldCount > 0 && newCount < oldCount
5043
+ ? (oldCount - newCount) / oldCount
5044
+ : 0;
5045
+ const unsafeEmpty = oldCount > 0 && newCount === 0;
5046
+ const unsafeBulkRemoval = removedRatio > 0.5;
5047
+ if (!validation.valid || unsafeEmpty || unsafeBulkRemoval) {
5048
+ this.fleetConfig = oldConfig;
5049
+ this.rawFleetConfig = previousRawConfig;
5050
+ this.rawFleetDocument = previousRawDocument;
5051
+ this.savedFleetConfigSnapshot = previousSavedSnapshot;
5052
+ this.logger.error({
5053
+ oldCount,
5054
+ newCount,
5055
+ removedRatio,
5056
+ validationErrors: validation.errors,
5057
+ }, "Refusing unsafe fleet config reload; running configuration was kept");
5058
+ return;
5059
+ }
4955
5060
  this.routing.rebuild(this.fleetConfig);
4956
5061
  this.reregisterClassicChannels();
4957
5062
  this.scheduler?.reload();