@songsid/agend 2.1.0 → 2.1.1-beta.2
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/backend/types.d.ts +1 -6
- package/dist/backend/types.js +48 -4
- package/dist/backend/types.js.map +1 -1
- package/dist/fleet-manager.d.ts +11 -0
- package/dist/fleet-manager.js +86 -9
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/tui-effort/SKILL.md +115 -0
- package/dist/service-installer.d.ts +7 -0
- package/dist/service-installer.js +43 -3
- package/dist/service-installer.js.map +1 -1
- package/package.json +1 -1
package/dist/backend/types.d.ts
CHANGED
|
@@ -165,12 +165,7 @@ export interface CliBackend {
|
|
|
165
165
|
models: ModelOption[];
|
|
166
166
|
}>;
|
|
167
167
|
}
|
|
168
|
-
|
|
169
|
-
* Resolve the full path to a CLI binary.
|
|
170
|
-
* tmux new-window runs commands in a minimal shell without user PATH,
|
|
171
|
-
* so we resolve at daemon startup time when the full PATH is available.
|
|
172
|
-
*/
|
|
173
|
-
export declare function resolveBinary(name: string): string;
|
|
168
|
+
export declare function resolveBinary(name: string, fallbackDirs?: readonly string[]): string;
|
|
174
169
|
export declare function validateModel(model: string): string;
|
|
175
170
|
/** Check if a model name is compatible with the given backend. */
|
|
176
171
|
export declare function isModelCompatible(backendName: string, model: string): boolean;
|
package/dist/backend/types.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { accessSync, constants, readdirSync, statSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
2
5
|
/** Best-effort `<binary> --version` (first line, trimmed). Never throws. */
|
|
3
6
|
export function probeCliVersion(binaryPath) {
|
|
4
7
|
try {
|
|
@@ -14,13 +17,54 @@ export function probeCliVersion(binaryPath) {
|
|
|
14
17
|
* tmux new-window runs commands in a minimal shell without user PATH,
|
|
15
18
|
* so we resolve at daemon startup time when the full PATH is available.
|
|
16
19
|
*/
|
|
17
|
-
|
|
20
|
+
function commonBinaryDirs() {
|
|
21
|
+
const dirs = [
|
|
22
|
+
dirname(process.execPath),
|
|
23
|
+
join(homedir(), ".local", "bin"),
|
|
24
|
+
join(homedir(), ".npm-global", "bin"),
|
|
25
|
+
"/usr/local/bin",
|
|
26
|
+
"/usr/bin",
|
|
27
|
+
"/bin",
|
|
28
|
+
];
|
|
29
|
+
// Root installations often retain Codex under an nvm prefix while sudo's
|
|
30
|
+
// secure_path omits it. Probe every installed nvm Node bin as a fallback.
|
|
18
31
|
try {
|
|
19
|
-
|
|
32
|
+
const nvmVersions = join(homedir(), ".nvm", "versions", "node");
|
|
33
|
+
for (const version of readdirSync(nvmVersions).sort().reverse()) {
|
|
34
|
+
dirs.push(join(nvmVersions, version, "bin"));
|
|
35
|
+
}
|
|
20
36
|
}
|
|
21
|
-
catch {
|
|
22
|
-
|
|
37
|
+
catch { /* nvm is optional */ }
|
|
38
|
+
// Also cover custom npm prefixes such as ~/.npm-global.
|
|
39
|
+
try {
|
|
40
|
+
const prefix = execFileSync("npm", ["prefix", "-g"], {
|
|
41
|
+
encoding: "utf-8",
|
|
42
|
+
timeout: 3000,
|
|
43
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
44
|
+
}).trim();
|
|
45
|
+
if (prefix)
|
|
46
|
+
dirs.push(join(prefix, "bin"));
|
|
47
|
+
}
|
|
48
|
+
catch { /* npm may not be in the restricted PATH */ }
|
|
49
|
+
return [...new Set(dirs)];
|
|
50
|
+
}
|
|
51
|
+
export function resolveBinary(name, fallbackDirs) {
|
|
52
|
+
try {
|
|
53
|
+
const resolved = execFileSync("which", [name], { encoding: "utf-8" }).trim();
|
|
54
|
+
if (resolved)
|
|
55
|
+
return resolved;
|
|
56
|
+
}
|
|
57
|
+
catch { /* search common absolute locations below */ }
|
|
58
|
+
for (const dir of fallbackDirs ?? commonBinaryDirs()) {
|
|
59
|
+
const candidate = join(dir, name);
|
|
60
|
+
try {
|
|
61
|
+
accessSync(candidate, constants.X_OK);
|
|
62
|
+
if (statSync(candidate).isFile())
|
|
63
|
+
return candidate;
|
|
64
|
+
}
|
|
65
|
+
catch { /* try next candidate */ }
|
|
23
66
|
}
|
|
67
|
+
return name; // final fallback keeps the previous shell-PATH behavior
|
|
24
68
|
}
|
|
25
69
|
/**
|
|
26
70
|
* Whitelist for model names embedded into the shell command line.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/backend/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/backend/types.ts"],"names":[],"mappings":"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"}
|
package/dist/fleet-manager.d.ts
CHANGED
|
@@ -44,6 +44,8 @@ export interface DeliveryOptions {
|
|
|
44
44
|
}
|
|
45
45
|
export declare class FleetManager implements FleetContext, LifecycleContext, ArchiverContext, StatuslineWatcherContext, OutboundContext, AgentEndpointContext {
|
|
46
46
|
dataDir: string;
|
|
47
|
+
private static signalTarget;
|
|
48
|
+
private static sighupHandlerInstalled;
|
|
47
49
|
private children;
|
|
48
50
|
readonly lifecycle: InstanceLifecycle;
|
|
49
51
|
/** @deprecated Use lifecycle.daemons — kept for backward compat */
|
|
@@ -66,6 +68,12 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
66
68
|
instanceIpcClients: Map<string, IpcClient>;
|
|
67
69
|
scheduler: Scheduler | null;
|
|
68
70
|
private configPath;
|
|
71
|
+
/** SIGHUPs received before startAll finishes are replayed once startup is safe. */
|
|
72
|
+
private startupComplete;
|
|
73
|
+
/** Coalesces one or more SIGHUPs into at most one follow-up reconciliation. */
|
|
74
|
+
private reloadPending;
|
|
75
|
+
/** A running reconciliation; only one may mutate lifecycle/config state at a time. */
|
|
76
|
+
private reconcileInFlight;
|
|
69
77
|
logger: Logger;
|
|
70
78
|
private topicCommands;
|
|
71
79
|
sessionRegistry: Map<string, string>;
|
|
@@ -113,6 +121,9 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
113
121
|
private webToken;
|
|
114
122
|
private viewToken;
|
|
115
123
|
constructor(dataDir: string);
|
|
124
|
+
private handleSighup;
|
|
125
|
+
private scheduleReconcile;
|
|
126
|
+
private finishStartup;
|
|
116
127
|
lastActivityMs(name: string): number;
|
|
117
128
|
private getInstanceIdle;
|
|
118
129
|
webhookEmit(event: string, name: string, data?: Record<string, unknown>): void;
|
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
|
|
@@ -4744,6 +4786,8 @@ When users create specialized instances, suggest these configurations:
|
|
|
4744
4786
|
return t("classic.stopped");
|
|
4745
4787
|
}
|
|
4746
4788
|
async stopAll() {
|
|
4789
|
+
this.startupComplete = false;
|
|
4790
|
+
this.reloadPending = false;
|
|
4747
4791
|
this.ipcStoppingInstances.add("__fleet_stopping__");
|
|
4748
4792
|
sdNotify("STOPPING=1");
|
|
4749
4793
|
if (this.watchdogTimer) {
|
|
@@ -4951,7 +4995,40 @@ When users create specialized instances, suggest these configurations:
|
|
|
4951
4995
|
if (!this.configPath)
|
|
4952
4996
|
return;
|
|
4953
4997
|
const oldConfig = this.fleetConfig;
|
|
4954
|
-
this.
|
|
4998
|
+
const previousRawConfig = this.rawFleetConfig;
|
|
4999
|
+
const previousRawDocument = this.rawFleetDocument;
|
|
5000
|
+
const previousSavedSnapshot = this.savedFleetConfigSnapshot;
|
|
5001
|
+
try {
|
|
5002
|
+
this.loadConfig(this.configPath);
|
|
5003
|
+
}
|
|
5004
|
+
catch (err) {
|
|
5005
|
+
this.fleetConfig = oldConfig;
|
|
5006
|
+
this.rawFleetConfig = previousRawConfig;
|
|
5007
|
+
this.rawFleetDocument = previousRawDocument;
|
|
5008
|
+
this.savedFleetConfigSnapshot = previousSavedSnapshot;
|
|
5009
|
+
throw err;
|
|
5010
|
+
}
|
|
5011
|
+
const validation = validateFleetConfig(this.rawFleetConfig);
|
|
5012
|
+
const oldCount = Object.keys(oldConfig?.instances ?? {}).length;
|
|
5013
|
+
const newCount = Object.keys(this.fleetConfig?.instances ?? {}).length;
|
|
5014
|
+
const removedRatio = oldCount > 0 && newCount < oldCount
|
|
5015
|
+
? (oldCount - newCount) / oldCount
|
|
5016
|
+
: 0;
|
|
5017
|
+
const unsafeEmpty = oldCount > 0 && newCount === 0;
|
|
5018
|
+
const unsafeBulkRemoval = removedRatio > 0.5;
|
|
5019
|
+
if (!validation.valid || unsafeEmpty || unsafeBulkRemoval) {
|
|
5020
|
+
this.fleetConfig = oldConfig;
|
|
5021
|
+
this.rawFleetConfig = previousRawConfig;
|
|
5022
|
+
this.rawFleetDocument = previousRawDocument;
|
|
5023
|
+
this.savedFleetConfigSnapshot = previousSavedSnapshot;
|
|
5024
|
+
this.logger.error({
|
|
5025
|
+
oldCount,
|
|
5026
|
+
newCount,
|
|
5027
|
+
removedRatio,
|
|
5028
|
+
validationErrors: validation.errors,
|
|
5029
|
+
}, "Refusing unsafe fleet config reload; running configuration was kept");
|
|
5030
|
+
return;
|
|
5031
|
+
}
|
|
4955
5032
|
this.routing.rebuild(this.fleetConfig);
|
|
4956
5033
|
this.reregisterClassicChannels();
|
|
4957
5034
|
this.scheduler?.reload();
|