@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.
@@ -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;
@@ -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
- export function resolveBinary(name) {
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
- return execFileSync("which", [name], { encoding: "utf-8" }).trim();
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
- return name; // fallback to bare name
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;AAiFlD,4EAA4E;AAC5E,MAAM,UAAU,eAAe,CAAC,UAAkB;IAChD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,UAAU,EAAE,CAAC,WAAW,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC,CAAC;QAC/H,OAAO,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,SAAS,CAAC;IAC7D,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,SAAS,CAAC;IAAC,CAAC;AAC/B,CAAC;AAkHD;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACrE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC,CAAC,wBAAwB;IACvC,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,aAAa,GAAG,qBAAqB,CAAC;AAC5C,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CAAC,uBAAuB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,iBAAiB,aAAa,EAAE,CAAC,CAAC;IAChG,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,0GAA0G;AAC1G,MAAM,sBAAsB,GAA2B;IACrD,aAAa,EAAE,4CAA4C;IAC3D,UAAU,EAAE,mCAAmC;IAC/C,OAAO,EAAE,wBAAwB;IACjC,YAAY,EAAE,UAAU;IACxB,UAAU,EAAE,GAAG,EAAG,iDAAiD;IACnE,0EAA0E;IAC1E,8DAA8D;IAC9D,aAAa,EAAE,2BAA2B;IAC1C,MAAM,EAAE,QAAQ,EAAG,gDAAgD;CACpE,CAAC;AAEF,kEAAkE;AAClE,MAAM,UAAU,iBAAiB,CAAC,WAAmB,EAAE,KAAa;IAClE,MAAM,OAAO,GAAG,sBAAsB,CAAC,WAAW,CAAC,CAAC;IACpD,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC,CAAC,iCAAiC;IAC5D,OAAO,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC7B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,WAAmB,EAAE,KAAa;IACpE,IAAI,CAAC,iBAAiB,CAAC,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC;QAC3C,OAAO,CAAC,IAAI,CAAC,kBAAkB,KAAK,uCAAuC,WAAW,0BAA0B,CAAC,CAAC;IACpH,CAAC;AACH,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,UAAU,CAAC,CAAS;IAClC,OAAO,IAAI,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC;AACzC,CAAC"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/backend/types.ts"],"names":[],"mappings":"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"}
@@ -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;
@@ -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.loadConfig(this.configPath);
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();