@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.
- package/README.md +1 -0
- package/README.zh-TW.md +1 -0
- package/dist/agent-cli-instructions.md +9 -0
- package/dist/backend/antigravity.d.ts +4 -1
- package/dist/backend/antigravity.js +15 -2
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/codex.d.ts +1 -0
- package/dist/backend/codex.js +30 -2
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/grok.d.ts +1 -0
- package/dist/backend/grok.js +8 -1
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro.d.ts +7 -0
- package/dist/backend/kiro.js +15 -6
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +11 -6
- package/dist/backend/types.js +48 -4
- package/dist/backend/types.js.map +1 -1
- package/dist/cli.js +16 -8
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +18 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +5 -0
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +11 -0
- package/dist/daemon.js +56 -32
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-context.d.ts +2 -0
- package/dist/fleet-manager.d.ts +25 -2
- package/dist/fleet-manager.js +127 -22
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/tui-effort/SKILL.md +115 -0
- package/dist/instance-lifecycle.d.ts +8 -0
- package/dist/instance-lifecycle.js +57 -4
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.js +10 -3
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +2 -0
- package/dist/locale.js.map +1 -1
- package/dist/sd-notify.d.ts +18 -0
- package/dist/sd-notify.js +41 -2
- package/dist/sd-notify.js.map +1 -1
- package/dist/service-installer.d.ts +7 -0
- package/dist/service-installer.js +43 -3
- package/dist/service-installer.js.map +1 -1
- package/dist/tmux-control.js +7 -1
- package/dist/tmux-control.js.map +1 -1
- package/dist/tmux-manager.d.ts +16 -1
- package/dist/tmux-manager.js +69 -3
- package/dist/tmux-manager.js.map +1 -1
- package/dist/topic-commands.js +6 -2
- package/dist/topic-commands.js.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/ui/view.html +170 -33
- package/dist/update-check.d.ts +22 -0
- package/dist/update-check.js +44 -0
- package/dist/update-check.js.map +1 -0
- package/dist/view-api.d.ts +7 -0
- package/dist/view-api.js +56 -12
- package/dist/view-api.js.map +1 -1
- package/package.json +1 -1
- package/templates/systemd.service.ejs +9 -1
- package/dist/fleet-system-prompt.d.ts +0 -11
- package/dist/fleet-system-prompt.js +0 -61
- package/dist/fleet-system-prompt.js.map +0 -1
package/dist/fleet-manager.js
CHANGED
|
@@ -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
|
-
/**
|
|
4262
|
-
|
|
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
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
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
|
-
|
|
4276
|
-
|
|
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
|
-
|
|
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: **${
|
|
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.
|
|
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: ${
|
|
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.
|
|
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();
|