@songsid/agend 2.3.0-beta.11 → 2.3.0-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.
- package/README.md +6 -0
- package/README.zh-TW.md +6 -0
- package/dist/agent-whats-new.js +14 -9
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/channel/adapters/discord-client-retirement.d.ts +5 -0
- package/dist/channel/adapters/discord-client-retirement.js +57 -0
- package/dist/channel/adapters/discord-client-retirement.js.map +1 -0
- package/dist/channel/adapters/discord.d.ts +2 -0
- package/dist/channel/adapters/discord.js +14 -4
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/cli.js +12 -79
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +12 -0
- package/dist/config.js +13 -4
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +3 -0
- package/dist/daemon.js +25 -0
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-manager.d.ts +44 -2
- package/dist/fleet-manager.js +229 -63
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-process-handlers.d.ts +18 -0
- package/dist/fleet-process-handlers.js +69 -0
- package/dist/fleet-process-handlers.js.map +1 -0
- package/dist/general-knowledge/skills/fleet-config/SKILL.md +2 -0
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +20 -3
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +27 -5
- package/dist/instance-lifecycle.d.ts +2 -0
- package/dist/instance-lifecycle.js +11 -6
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/outbound-schemas.d.ts +14 -1
- package/dist/outbound-schemas.js +8 -2
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +4 -0
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/restart-progress.d.ts +16 -1
- package/dist/restart-progress.js +74 -15
- package/dist/restart-progress.js.map +1 -1
- package/dist/settings-api.d.ts +4 -0
- package/dist/settings-api.js +27 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-confirmation.d.ts +3 -1
- package/dist/settings-confirmation.js +2 -0
- package/dist/settings-confirmation.js.map +1 -1
- package/dist/settings-effect.js +2 -1
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.d.ts +11 -0
- package/dist/settings-http-confirmation.js +10 -0
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/ui/app-effects.js +13 -1
- package/dist/ui/chat-effects.js +10 -3
- package/dist/ui/chat-store.js +32 -10
- package/dist/ui/chat-strings.js +18 -0
- package/dist/ui/chat-thread.js +5 -2
- package/dist/ui/effects-waker.js +70 -0
- package/dist/ui/fleet-canvas-layout.js +10 -4
- package/dist/ui/fleet-canvas.js +32 -2
- package/dist/ui/fleet-route-scene.js +207 -0
- package/dist/ui/fleet-route-worker.js +21 -0
- package/dist/ui/fleet-route.js +88 -0
- package/dist/ui/fleet-traffic.js +27 -11
- package/dist/ui/fleet-usage.js +35 -1
- package/dist/ui/panel-chat.js +67 -26
- package/dist/ui/panel-fleet.js +3 -3
- package/dist/ui/panel-settings.js +24 -0
- package/dist/ui/settings-appearance.js +2 -1
- package/dist/ui/settings-apply.js +3 -3
- package/dist/ui/settings-confirm.js +7 -2
- package/dist/ui/settings-owner-effects.js +6 -4
- package/dist/ui/settings-strings.js +2 -0
- package/dist/ui/shared/app-i18n.js +4 -0
- package/dist/ui/shared/app-prefs.js +12 -0
- package/dist/ui/shared/app-shell.js +38 -2
- package/dist/ui/shared/app-themes.js +116 -1
- package/dist/ui/shared/app.css +47 -3
- package/dist/ui/shared/card-strings.js +4 -0
- package/dist/ui/shared/effect-owner-source.js +8 -1
- package/dist/ui/shared/effect-registry.js +10 -2
- package/dist/ui/shared/identity-card.js +24 -6
- package/dist/ui/shared/identity-color.js +11 -7
- package/dist/ui/shared/owner-css.js +30 -1
- package/dist/ui/shared/ui-icons.js +2 -0
- package/dist/ui/shared/ui-menu.js +9 -3
- package/dist/ui/shared/web-authorize.js +47 -0
- package/dist/web-api.d.ts +6 -2
- package/dist/web-api.js +68 -15
- package/dist/web-api.js.map +1 -1
- package/dist/web-appearance.js +3 -1
- package/dist/web-appearance.js.map +1 -1
- package/dist/web-effect.d.ts +1 -1
- package/dist/web-host-guard.d.ts +11 -0
- package/dist/web-host-guard.js +11 -0
- package/dist/web-host-guard.js.map +1 -1
- package/dist/web-libavoid.d.ts +14 -0
- package/dist/web-libavoid.js +46 -0
- package/dist/web-libavoid.js.map +1 -0
- package/dist/web-owner-css-store.d.ts +44 -1
- package/dist/web-owner-css-store.js +70 -9
- package/dist/web-owner-css-store.js.map +1 -1
- package/dist/web-prefs.js +13 -0
- package/dist/web-prefs.js.map +1 -1
- package/dist/web-settings-tools.d.ts +5 -0
- package/dist/web-settings-tools.js +38 -7
- package/dist/web-settings-tools.js.map +1 -1
- package/dist/web-theme.d.ts +29 -0
- package/dist/web-theme.js +89 -1
- package/dist/web-theme.js.map +1 -1
- package/dist/yaml-shared-nodes.d.ts +25 -0
- package/dist/yaml-shared-nodes.js +185 -0
- package/dist/yaml-shared-nodes.js.map +1 -0
- package/package.json +2 -1
package/dist/fleet-manager.d.ts
CHANGED
|
@@ -245,6 +245,8 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
245
245
|
fleetConfig: FleetConfig | null;
|
|
246
246
|
private rawFleetConfig;
|
|
247
247
|
private rawFleetDocument;
|
|
248
|
+
/** #2208: the paths a saveFleetConfig() wrote, for its check that nothing else changed. */
|
|
249
|
+
private fleetDocumentWrites;
|
|
248
250
|
private savedFleetConfigSnapshot;
|
|
249
251
|
adapter: ChannelAdapter | null;
|
|
250
252
|
readonly worlds: Map<string, AdapterWorld>;
|
|
@@ -411,6 +413,7 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
411
413
|
/** #2175: this boot's progress message, whose completion may still be owed by its background retry. */
|
|
412
414
|
private startupProgress;
|
|
413
415
|
private adapterState;
|
|
416
|
+
private adapterRetryInFlight;
|
|
414
417
|
/** Web Settings secret rotation is deliberately separate from reconnect
|
|
415
418
|
* recovery: a rotation must build a fresh provider client with the new token,
|
|
416
419
|
* and stale callbacks from the old client must not win. */
|
|
@@ -623,6 +626,30 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
623
626
|
private classicSettingsOwners;
|
|
624
627
|
private queueSettingsOperation;
|
|
625
628
|
private settingsGate;
|
|
629
|
+
/**
|
|
630
|
+
* #2170: a held change confirmed by the request itself — only when ALL hold, else the chat card or the host CLI, as
|
|
631
|
+
* before:
|
|
632
|
+
* - the session is web-verified (#1939: its account confirmed this browser on Discord or Telegram), asked now;
|
|
633
|
+
* - the change's effect owners are known and on one platform (settingsChatAuthority — the chat card's own rule), the
|
|
634
|
+
* verified account is on that platform and an admin of every one of them;
|
|
635
|
+
* - a chat card could have been asked: a connected General on that platform the change does not affect (else the
|
|
636
|
+
* change goes to the host CLI, as before) — the General that then hears of it;
|
|
637
|
+
* - and, at execution (store.decide asks `current()` again): still web-verified under the same session and account
|
|
638
|
+
* (not expired, revoked, signed out or the switch turned off), still those owners' admin, still a General a card
|
|
639
|
+
* could go to, the fleet unchanged.
|
|
640
|
+
* What it rests on: the account's one confirmation of this browser (#1939), kept for the session until it is revoked,
|
|
641
|
+
* expires or signs out — not a fresh confirmation per Apply. Whoever uses that verified session (a browser left signed
|
|
642
|
+
* in, its cookie) confirms the same way. A public link never qualifies (evaluateWebAuthority refuses it). General
|
|
643
|
+
* still hears it: a notice, not a step.
|
|
644
|
+
*/
|
|
645
|
+
private settingsSelfConfirmation;
|
|
646
|
+
/** #2170: the Generals a chat card could be posted in (promptSettingsChange's rule): connected, on the platform, unaffected. */
|
|
647
|
+
private settingsNoticeGenerals;
|
|
648
|
+
/**
|
|
649
|
+
* #2170: General's notice of a change confirmed from the web — the first General a chat card could have been posted
|
|
650
|
+
* in, plain text, no mentions. Information only: nothing waits on it.
|
|
651
|
+
*/
|
|
652
|
+
private settingsWebConfirmedNotice;
|
|
626
653
|
/** Exact configured effect owners; this does not depend on prompt location. */
|
|
627
654
|
private settingsChatAuthority;
|
|
628
655
|
private promptSettingsChange;
|
|
@@ -1357,7 +1384,10 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
1357
1384
|
* snapshot use the fleet startup/retry state. */
|
|
1358
1385
|
private readyProgressAdapter;
|
|
1359
1386
|
/** Last-resort completion after RestartProgress could not deliver to its
|
|
1360
|
-
* adopted target
|
|
1387
|
+
* adopted target (or had no message: a small fleet, a failed first post). It
|
|
1388
|
+
* never wakes a not-ready gateway. #2213: an outage at fleet start no longer
|
|
1389
|
+
* loses it — bounded at startup, then retried in the background once the
|
|
1390
|
+
* adapter is ready again, never posted twice after an uncertain attempt. */
|
|
1361
1391
|
private sendFleetStartCompletionFallback;
|
|
1362
1392
|
stopInstance(name: string, transition?: TransitionHandle): Promise<{
|
|
1363
1393
|
backgroundWorkEnded: boolean;
|
|
@@ -1683,8 +1713,14 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
1683
1713
|
private sweepOrphanedWebUploads;
|
|
1684
1714
|
/** Start the shared channel adapter(s) for topic mode */
|
|
1685
1715
|
private startSharedAdapter;
|
|
1686
|
-
/**
|
|
1716
|
+
/** Keep trying while the fleet runs, with a five-minute cap even after a long outage. */
|
|
1687
1717
|
private scheduleAdapterRetry;
|
|
1718
|
+
/** One login per connection, shared by the background timer and the operator's Retry now. */
|
|
1719
|
+
private attemptAdapterRetry;
|
|
1720
|
+
retryAdapterNow(adapterId: string): {
|
|
1721
|
+
queued: boolean;
|
|
1722
|
+
already_running?: boolean;
|
|
1723
|
+
};
|
|
1688
1724
|
/** Notify admin about adapter failure (uses any available adapter). */
|
|
1689
1725
|
private notifyAdapterFailure;
|
|
1690
1726
|
/** Notify admin that a retried adapter reconnected. */
|
|
@@ -2126,6 +2162,12 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
2126
2162
|
private slimFleetConfigAtStartup;
|
|
2127
2163
|
private writeFleetConfigBackup;
|
|
2128
2164
|
private patchFleetDocument;
|
|
2165
|
+
/**
|
|
2166
|
+
* #2208: before every write to the raw document — the place is unshared first (an alias on the way becomes its own
|
|
2167
|
+
* copy; an anchor's other aliases get theirs), so the write changes that place only; and it is recorded for the
|
|
2168
|
+
* save's check that nothing else changed.
|
|
2169
|
+
*/
|
|
2170
|
+
private writingFleetDocument;
|
|
2129
2171
|
getSettingsConfigPath(): string;
|
|
2130
2172
|
removeInstance(name: string, authorization: ExplicitInstanceRemoval, execution?: SettingsExecution): Promise<void>;
|
|
2131
2173
|
private removeInstanceOwned;
|
package/dist/fleet-manager.js
CHANGED
|
@@ -38,6 +38,7 @@ import { readFleetMemory } from "./process-memory.js";
|
|
|
38
38
|
import { MemoryPressure } from "./memory-pressure.js";
|
|
39
39
|
import { replyDedupText, ReplyDeduper } from "./reply-dedup.js";
|
|
40
40
|
import { isMap, isScalar, parseDocument } from "yaml";
|
|
41
|
+
import { changedOutside, detachForWrite } from "./yaml-shared-nodes.js";
|
|
41
42
|
import { kiroLogGate } from "./kiro-log-gate.js";
|
|
42
43
|
const __filename = fileURLToPath(import.meta.url);
|
|
43
44
|
const __dirname = dirname(__filename);
|
|
@@ -227,7 +228,7 @@ import { authorizeSession, decideWebGate, loadOrCreateWebToken, readWebToken } f
|
|
|
227
228
|
import { bypassesWebGate, handleAuthRequest, serveSigninPage } from "./auth-api.js";
|
|
228
229
|
import { isWebPageNavigation } from "./web-shell-routes.js";
|
|
229
230
|
import { guestSessionPolicy, localSessionPolicy, tokenEpoch, WebSessionStore } from "./web-session.js";
|
|
230
|
-
import { authorityNow, evaluateWebAuthority, VERIFIED_SESSION_HOURS, webAuthorityPolicy } from "./authority/web-authority.js";
|
|
231
|
+
import { authorityNow, classifyWebPeer, evaluateWebAuthority, VERIFIED_SESSION_HOURS, webAuthorityPolicy } from "./authority/web-authority.js";
|
|
231
232
|
import { AuthorityStateStore } from "./authority/authority-state.js";
|
|
232
233
|
import { SessionAuthorizations } from "./authority/session-authorizations.js";
|
|
233
234
|
import { SessionAuthorizationRequests } from "./web-session-authorization.js";
|
|
@@ -250,7 +251,7 @@ import { CHAT_PROFILE_LOGIN_BACKENDS, credentialHomeSpec, credentialProfileHome,
|
|
|
250
251
|
import { carryClaudeConversation } from "./backend/claude-carry.js";
|
|
251
252
|
import { mergeBackendOptions, profileKey, profileName, profileOf } from "./classic-bindings.js";
|
|
252
253
|
import { classifyInstanceChange, CLASSIC_HOT_CONFIG_KEYS, HOT_INSTANCE_CONFIG_KEYS, hotConfigUpdate, } from "./instance-config-impact.js";
|
|
253
|
-
import { cleanupWatchVerdict, formatRestartProgressCompletion,
|
|
254
|
+
import { cleanupWatchVerdict, formatRestartProgressCompletion, RestartProgress, } from "./restart-progress.js";
|
|
254
255
|
import { launchFullRestartHelper } from "./full-restart.js";
|
|
255
256
|
import { SYSTEMD_RESTART_INDETERMINATE_EXIT_CODE } from "./service-restart-selection.js";
|
|
256
257
|
import { collectRedundantInstanceDefaultPaths } from "./fleet-yaml-slim.js";
|
|
@@ -589,6 +590,8 @@ export class FleetManager {
|
|
|
589
590
|
fleetConfig = null;
|
|
590
591
|
rawFleetConfig = {};
|
|
591
592
|
rawFleetDocument = null;
|
|
593
|
+
/** #2208: the paths a saveFleetConfig() wrote, for its check that nothing else changed. */
|
|
594
|
+
fleetDocumentWrites = null;
|
|
592
595
|
savedFleetConfigSnapshot = null;
|
|
593
596
|
adapter = null;
|
|
594
597
|
worlds = new Map();
|
|
@@ -784,6 +787,7 @@ export class FleetManager {
|
|
|
784
787
|
/** #2175: this boot's progress message, whose completion may still be owed by its background retry. */
|
|
785
788
|
startupProgress = null;
|
|
786
789
|
adapterState = new Map();
|
|
790
|
+
adapterRetryInFlight = new Map();
|
|
787
791
|
/** Web Settings secret rotation is deliberately separate from reconnect
|
|
788
792
|
* recovery: a rotation must build a fresh provider client with the new token,
|
|
789
793
|
* and stale callbacks from the old client must not win. */
|
|
@@ -1283,6 +1287,7 @@ export class FleetManager {
|
|
|
1283
1287
|
current: () => current() && sessions === this.webSessions && !!this.webToken
|
|
1284
1288
|
&& sessions.isCurrent(record, tokenEpoch(this.webToken)) && (!exposure || exposure.isCurrent()) };
|
|
1285
1289
|
}, baseline: () => baselines.read(), snapshot: () => baselines.snapshot(),
|
|
1290
|
+
selfConfirm: (req, id) => this.settingsSelfConfirmation(store, req, id),
|
|
1286
1291
|
job: async (id) => {
|
|
1287
1292
|
const settlement = this.settingsJobSettlements.get(id);
|
|
1288
1293
|
if (!settlement)
|
|
@@ -1297,6 +1302,86 @@ export class FleetManager {
|
|
|
1297
1302
|
this.settingsConfirmation = gate;
|
|
1298
1303
|
return gate;
|
|
1299
1304
|
}
|
|
1305
|
+
/**
|
|
1306
|
+
* #2170: a held change confirmed by the request itself — only when ALL hold, else the chat card or the host CLI, as
|
|
1307
|
+
* before:
|
|
1308
|
+
* - the session is web-verified (#1939: its account confirmed this browser on Discord or Telegram), asked now;
|
|
1309
|
+
* - the change's effect owners are known and on one platform (settingsChatAuthority — the chat card's own rule), the
|
|
1310
|
+
* verified account is on that platform and an admin of every one of them;
|
|
1311
|
+
* - a chat card could have been asked: a connected General on that platform the change does not affect (else the
|
|
1312
|
+
* change goes to the host CLI, as before) — the General that then hears of it;
|
|
1313
|
+
* - and, at execution (store.decide asks `current()` again): still web-verified under the same session and account
|
|
1314
|
+
* (not expired, revoked, signed out or the switch turned off), still those owners' admin, still a General a card
|
|
1315
|
+
* could go to, the fleet unchanged.
|
|
1316
|
+
* What it rests on: the account's one confirmation of this browser (#1939), kept for the session until it is revoked,
|
|
1317
|
+
* expires or signs out — not a fresh confirmation per Apply. Whoever uses that verified session (a browser left signed
|
|
1318
|
+
* in, its cookie) confirms the same way. A public link never qualifies (evaluateWebAuthority refuses it). General
|
|
1319
|
+
* still hears it: a notice, not a step.
|
|
1320
|
+
*/
|
|
1321
|
+
settingsSelfConfirmation(store, req, id) {
|
|
1322
|
+
const auth = authorizeSession(req, this.webToken, this.webSessions, { touch: false });
|
|
1323
|
+
if (auth.kind !== "ok")
|
|
1324
|
+
return null;
|
|
1325
|
+
const record = auth.session, gateway = !!gatewayRequestContext(req), peer = classifyWebPeer(req);
|
|
1326
|
+
const verified = () => {
|
|
1327
|
+
const now = this.webAuthority({ session: record, gateway, peer });
|
|
1328
|
+
// A refusal carries no authority (kind "none"); the handle pins the session, and with it its one account.
|
|
1329
|
+
return now.authority.kind === "web-verified" && now.authority.actor && now.authority.session?.handle === record.handle ? now.authority.actor : null;
|
|
1330
|
+
};
|
|
1331
|
+
const actor = verified();
|
|
1332
|
+
if (!actor)
|
|
1333
|
+
return null;
|
|
1334
|
+
const chat = this.settingsChatAuthority(store, id);
|
|
1335
|
+
if (!chat)
|
|
1336
|
+
return null;
|
|
1337
|
+
const channels = this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : []);
|
|
1338
|
+
const own = channels.filter(ch => (ch.id ?? ch.type) === actor.adapter_id);
|
|
1339
|
+
if (own.length !== 1 || own[0].type !== chat.platform || !chat.admin(actor.user_id))
|
|
1340
|
+
return null;
|
|
1341
|
+
const affected = [...store.affectedConnections(id)]; // read now: a finished change no longer has them
|
|
1342
|
+
if (!this.settingsNoticeGenerals(affected, chat.platform).length)
|
|
1343
|
+
return null;
|
|
1344
|
+
const name = this.webPerson({ adapterId: actor.adapter_id, userId: actor.user_id }).name;
|
|
1345
|
+
return {
|
|
1346
|
+
label: `${name} (web-verified)`,
|
|
1347
|
+
// admin() asks chat.current() too; a General a card could go to must still be there (else the host CLI's).
|
|
1348
|
+
current: () => !!verified() && chat.admin(actor.user_id) && this.settingsNoticeGenerals(affected, chat.platform).length > 0,
|
|
1349
|
+
done: view => this.settingsWebConfirmedNotice(view, affected, name, chat.platform),
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
/** #2170: the Generals a chat card could be posted in (promptSettingsChange's rule): connected, on the platform, unaffected. */
|
|
1353
|
+
settingsNoticeGenerals(affected, platform) {
|
|
1354
|
+
const excluded = new Set(affected), found = [];
|
|
1355
|
+
for (const [name, config] of Object.entries(this.fleetConfig?.instances ?? {})) {
|
|
1356
|
+
if (!config.general_topic || !this.daemons.get(name))
|
|
1357
|
+
continue;
|
|
1358
|
+
const adapterId = this.getInstanceAdapterId(name), adapter = this.getAdapterForInstance(name);
|
|
1359
|
+
if (!adapterId || !adapter || excluded.has(adapterId) || adapter.type !== platform || this.adapterState.get(adapterId)?.status !== "connected")
|
|
1360
|
+
continue;
|
|
1361
|
+
const chatId = this.getGroupIdForInstance(name), topic = String(config.topic_id ?? "");
|
|
1362
|
+
if (chatId && topic)
|
|
1363
|
+
found.push({ adapterId, adapter, chatId, topic });
|
|
1364
|
+
}
|
|
1365
|
+
return found;
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* #2170: General's notice of a change confirmed from the web — the first General a chat card could have been posted
|
|
1369
|
+
* in, plain text, no mentions. Information only: nothing waits on it.
|
|
1370
|
+
*/
|
|
1371
|
+
async settingsWebConfirmedNotice(view, affected, who, platform) {
|
|
1372
|
+
const outcome = view.outcome ? view.outcome.message : view.state;
|
|
1373
|
+
const text = `✅ Settings change confirmed from the web by ${who} (web-verified)\nSource: ${view.source}\nRequester: ${view.requested_by}\n${view.summary.join("\n")}\nOutcome: ${outcome}`;
|
|
1374
|
+
for (const { adapterId, adapter, chatId, topic } of this.settingsNoticeGenerals(affected, platform)) {
|
|
1375
|
+
try {
|
|
1376
|
+
await adapter.sendText(chatId, text, { threadId: topic, allowedMentions: { parse: [] } });
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
catch {
|
|
1380
|
+
this.logger.info({ id: view.id, adapterId }, "Settings web-confirmation notice unavailable; trying another General");
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
this.logger.info({ id: view.id }, "Settings change confirmed from the web: no General reachable for the notice");
|
|
1384
|
+
}
|
|
1300
1385
|
/** Exact configured effect owners; this does not depend on prompt location. */
|
|
1301
1386
|
settingsChatAuthority(store, id) {
|
|
1302
1387
|
const scope = store.authorityOf(id);
|
|
@@ -5297,34 +5382,18 @@ export class FleetManager {
|
|
|
5297
5382
|
return this.adapterState.get(adapterId)?.status === "connected" ? adapter : undefined;
|
|
5298
5383
|
}
|
|
5299
5384
|
/** Last-resort completion after RestartProgress could not deliver to its
|
|
5300
|
-
* adopted target
|
|
5385
|
+
* adopted target (or had no message: a small fleet, a failed first post). It
|
|
5386
|
+
* never wakes a not-ready gateway. #2213: an outage at fleet start no longer
|
|
5387
|
+
* loses it — bounded at startup, then retried in the background once the
|
|
5388
|
+
* adapter is ready again, never posted twice after an uncertain attempt. */
|
|
5301
5389
|
async sendFleetStartCompletionFallback(chatId, text, threadId) {
|
|
5302
5390
|
const adapterId = this.getPrimaryAdapterId();
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
this.logger.error({ adapterId }, "Fleet start completion fallback skipped because the primary adapter is not ready");
|
|
5391
|
+
if (!adapterId) {
|
|
5392
|
+
this.logger.error({ adapterId }, "Fleet start completion fallback skipped because there is no primary adapter");
|
|
5306
5393
|
return false;
|
|
5307
5394
|
}
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
timer = setTimeout(() => resolve({ status: "timeout" }), RESTART_PROGRESS_TERMINAL_TIMEOUT_MS);
|
|
5311
|
-
timer.unref?.();
|
|
5312
|
-
});
|
|
5313
|
-
const delivery = Promise.resolve()
|
|
5314
|
-
.then(() => adapter.sendText(chatId, text, { threadId }))
|
|
5315
|
-
.then(() => ({ status: "sent" }), err => ({ status: "failed", err }));
|
|
5316
|
-
const result = await Promise.race([delivery, timeout]);
|
|
5317
|
-
if (timer)
|
|
5318
|
-
clearTimeout(timer);
|
|
5319
|
-
if (result.status === "sent")
|
|
5320
|
-
return true;
|
|
5321
|
-
if (result.status === "failed") {
|
|
5322
|
-
this.logger.error({ err: result.err }, "Failed to send fleet start completion fallback");
|
|
5323
|
-
}
|
|
5324
|
-
else {
|
|
5325
|
-
this.logger.error({ timeout_ms: RESTART_PROGRESS_TERMINAL_TIMEOUT_MS }, "Timed out sending fleet start completion fallback");
|
|
5326
|
-
}
|
|
5327
|
-
return false;
|
|
5395
|
+
const poster = new RestartProgress(0, Date.now(), this.logger);
|
|
5396
|
+
return poster.postCompletion({ resolveAdapter: () => this.readyProgressAdapter(adapterId), chatId, threadId }, text);
|
|
5328
5397
|
}
|
|
5329
5398
|
async stopInstance(name, transition) {
|
|
5330
5399
|
this.explicitStopGeneration.set(name, (this.explicitStopGeneration.get(name) ?? 0) + 1);
|
|
@@ -7537,48 +7606,109 @@ export class FleetManager {
|
|
|
7537
7606
|
const err = results[i].reason;
|
|
7538
7607
|
this.logger.error({ adapterId, err: err?.message ?? err }, "Adapter startup failed — scheduling background retry");
|
|
7539
7608
|
this.adapterState.set(adapterId, { status: "retrying", retryCount: 0, lastError: err?.message ?? String(err) });
|
|
7540
|
-
this.scheduleAdapterRetry(adapterId
|
|
7609
|
+
this.scheduleAdapterRetry(adapterId);
|
|
7541
7610
|
// Notify admin via whichever adapter is already up
|
|
7542
7611
|
this.notifyAdapterFailure(adapterId, err?.message ?? String(err));
|
|
7543
7612
|
}
|
|
7544
7613
|
}
|
|
7545
7614
|
}
|
|
7546
|
-
/**
|
|
7547
|
-
scheduleAdapterRetry(adapterId
|
|
7548
|
-
const MAX_RETRIES = 10;
|
|
7615
|
+
/** Keep trying while the fleet runs, with a five-minute cap even after a long outage. */
|
|
7616
|
+
scheduleAdapterRetry(adapterId) {
|
|
7549
7617
|
const INITIAL_DELAY_MS = 5_000;
|
|
7550
7618
|
const MAX_DELAY_MS = 5 * 60_000;
|
|
7551
7619
|
const state = this.adapterState.get(adapterId);
|
|
7552
|
-
if (!state || state.
|
|
7553
|
-
if (state) {
|
|
7554
|
-
state.status = "failed";
|
|
7555
|
-
this.logger.error({ adapterId, retries: state.retryCount }, "Adapter retry exhausted — giving up");
|
|
7556
|
-
this.notifyAdapterFailure(adapterId, `Retry exhausted after ${state.retryCount} attempts. Check token/network and restart fleet.`);
|
|
7557
|
-
}
|
|
7620
|
+
if (this.shuttingDown || !state || state.retryTimer || this.adapterRetryInFlight.has(adapterId))
|
|
7558
7621
|
return;
|
|
7559
|
-
|
|
7560
|
-
const delay = Math.min(INITIAL_DELAY_MS * Math.pow(2, state.retryCount), MAX_DELAY_MS);
|
|
7622
|
+
const delay = Math.min(INITIAL_DELAY_MS * Math.pow(2, Math.min(state.retryCount, 6)), MAX_DELAY_MS);
|
|
7561
7623
|
this.logger.info({ adapterId, attempt: state.retryCount + 1, delay_ms: delay }, "Scheduling adapter retry");
|
|
7562
|
-
|
|
7624
|
+
const timer = setTimeout(() => {
|
|
7625
|
+
const current = this.adapterState.get(adapterId);
|
|
7626
|
+
if (this.shuttingDown || current?.retryTimer !== timer)
|
|
7627
|
+
return;
|
|
7628
|
+
current.retryTimer = undefined;
|
|
7629
|
+
void this.attemptAdapterRetry(adapterId);
|
|
7630
|
+
}, delay);
|
|
7631
|
+
state.retryTimer = timer;
|
|
7632
|
+
timer.unref?.();
|
|
7633
|
+
}
|
|
7634
|
+
/** One login per connection, shared by the background timer and the operator's Retry now. */
|
|
7635
|
+
attemptAdapterRetry(adapterId) {
|
|
7636
|
+
const pending = this.adapterRetryInFlight.get(adapterId);
|
|
7637
|
+
if (pending)
|
|
7638
|
+
return pending;
|
|
7639
|
+
if (this.shuttingDown || !this.secureConnectionChannel(adapterId))
|
|
7640
|
+
return Promise.resolve();
|
|
7641
|
+
const attempt = Promise.resolve().then(async () => {
|
|
7642
|
+
const channel = this.secureConnectionChannel(adapterId);
|
|
7643
|
+
if (this.shuttingDown || !channel)
|
|
7644
|
+
return;
|
|
7645
|
+
const generation = this.settingsGeneration;
|
|
7646
|
+
const binding = JSON.stringify([channel, this.getPrimaryAdapterId()]);
|
|
7647
|
+
const token = process.env[channel.bot_token_env];
|
|
7648
|
+
const operation = { owner: Symbol("adapter-retry"), assert: () => {
|
|
7649
|
+
if (this.shuttingDown || generation !== this.settingsGeneration
|
|
7650
|
+
|| JSON.stringify([this.secureConnectionChannel(adapterId), this.getPrimaryAdapterId()]) !== binding
|
|
7651
|
+
|| process.env[channel.bot_token_env] !== token)
|
|
7652
|
+
throw new Error("adapter retry superseded");
|
|
7653
|
+
} };
|
|
7654
|
+
const state = this.adapterState.get(adapterId) ?? { status: "retrying", retryCount: 0 };
|
|
7655
|
+
state.status = "retrying";
|
|
7563
7656
|
state.retryCount++;
|
|
7657
|
+
this.adapterState.set(adapterId, state);
|
|
7564
7658
|
try {
|
|
7565
|
-
if (
|
|
7566
|
-
await this.startSingleAdapter(
|
|
7567
|
-
|
|
7568
|
-
|
|
7569
|
-
|
|
7570
|
-
|
|
7571
|
-
|
|
7572
|
-
|
|
7659
|
+
if (this.getPrimaryAdapterId() === adapterId)
|
|
7660
|
+
await this.startSingleAdapter(this.fleetConfig, channel, undefined, operation);
|
|
7661
|
+
else
|
|
7662
|
+
await this.startAdditionalAdapter(channel, true, undefined, operation);
|
|
7663
|
+
if (this.shuttingDown || generation !== this.settingsGeneration)
|
|
7664
|
+
return;
|
|
7665
|
+
if (!this.adapters.has(adapterId))
|
|
7666
|
+
throw new Error("Adapter token is not configured");
|
|
7667
|
+
// Health events can replace the state object during login. Update its current owner.
|
|
7668
|
+
const current = this.adapterState.get(adapterId);
|
|
7669
|
+
current.status = "connected";
|
|
7670
|
+
current.lastError = undefined;
|
|
7573
7671
|
this.logger.info({ adapterId, attempts: state.retryCount }, "Adapter reconnected on retry");
|
|
7574
7672
|
this.notifyAdapterRecovery(adapterId, state.retryCount);
|
|
7575
7673
|
}
|
|
7576
7674
|
catch (err) {
|
|
7577
|
-
|
|
7578
|
-
|
|
7579
|
-
this.
|
|
7675
|
+
if (this.shuttingDown || generation !== this.settingsGeneration)
|
|
7676
|
+
return;
|
|
7677
|
+
const current = this.adapterState.get(adapterId);
|
|
7678
|
+
current.status = "retrying";
|
|
7679
|
+
current.lastError = err?.message ?? String(err);
|
|
7680
|
+
this.logger.warn({ adapterId, attempt: state.retryCount, err: current.lastError }, "Adapter retry failed");
|
|
7580
7681
|
}
|
|
7581
|
-
}
|
|
7682
|
+
}).finally(() => {
|
|
7683
|
+
if (this.adapterRetryInFlight.get(adapterId) !== attempt)
|
|
7684
|
+
return;
|
|
7685
|
+
this.adapterRetryInFlight.delete(adapterId);
|
|
7686
|
+
const channel = this.secureConnectionChannel(adapterId);
|
|
7687
|
+
if (!this.shuttingDown && channel && this.adapterState.get(adapterId)?.status !== "connected") {
|
|
7688
|
+
this.scheduleAdapterRetry(adapterId);
|
|
7689
|
+
}
|
|
7690
|
+
});
|
|
7691
|
+
this.adapterRetryInFlight.set(adapterId, attempt);
|
|
7692
|
+
return attempt;
|
|
7693
|
+
}
|
|
7694
|
+
retryAdapterNow(adapterId) {
|
|
7695
|
+
if (this.shuttingDown)
|
|
7696
|
+
throw new Error("The fleet is stopping");
|
|
7697
|
+
if (!this.secureConnectionChannel(adapterId))
|
|
7698
|
+
throw new Error("Connection not found");
|
|
7699
|
+
if (this.adapterRetryInFlight.has(adapterId) || this.adapterRestarting.has(adapterId))
|
|
7700
|
+
return { queued: true, already_running: true };
|
|
7701
|
+
const state = this.adapterState.get(adapterId);
|
|
7702
|
+
if (state?.retryTimer) {
|
|
7703
|
+
clearTimeout(state.retryTimer);
|
|
7704
|
+
state.retryTimer = undefined;
|
|
7705
|
+
}
|
|
7706
|
+
const adapter = this.adapters.get(adapterId);
|
|
7707
|
+
if (adapter && this.connectionStatus(adapterId, state, this.worlds.get(adapterId)).status === "connected")
|
|
7708
|
+
return { queued: false };
|
|
7709
|
+
const run = adapter ? this.restartAdapter(adapter, adapterId) : this.attemptAdapterRetry(adapterId);
|
|
7710
|
+
void run.catch(err => this.logger.warn({ adapterId, err }, "Requested adapter retry failed"));
|
|
7711
|
+
return { queued: true };
|
|
7582
7712
|
}
|
|
7583
7713
|
/** Notify admin about adapter failure (uses any available adapter). */
|
|
7584
7714
|
notifyAdapterFailure(adapterId, error) {
|
|
@@ -7633,6 +7763,7 @@ export class FleetManager {
|
|
|
7633
7763
|
: snapshot.status === "stopped" ? "failed"
|
|
7634
7764
|
: "retrying";
|
|
7635
7765
|
this.adapterState.set(adapterId, {
|
|
7766
|
+
...previous,
|
|
7636
7767
|
status,
|
|
7637
7768
|
retryCount: previous?.retryCount ?? 0,
|
|
7638
7769
|
lastError: status === "connected" ? undefined : snapshot.lastReconnectReason ?? previous?.lastError,
|
|
@@ -9458,6 +9589,16 @@ export class FleetManager {
|
|
|
9458
9589
|
respond(null, permitted.message);
|
|
9459
9590
|
return;
|
|
9460
9591
|
}
|
|
9592
|
+
// Fleet tools resolve their own targets. A missing chat world must not prevent local
|
|
9593
|
+
// delivery, inspection or receipt lookup; only channel tools need reply routing below.
|
|
9594
|
+
const handler = outboundHandlers.get(tool);
|
|
9595
|
+
if (handler && !usesChannelAddress(tool)) {
|
|
9596
|
+
this.eventLog?.logActivity("tool_call", senderSessionName ?? instanceName, this.summarizeToolCall(tool, args));
|
|
9597
|
+
await handler(this, args, respond, {
|
|
9598
|
+
instanceName, requestId, fleetRequestId, senderSessionName, operationId, sourceDaemonBootId,
|
|
9599
|
+
});
|
|
9600
|
+
return;
|
|
9601
|
+
}
|
|
9461
9602
|
// A fleet with no chat platform at all is driven from the web dashboard alone: a reply has nowhere
|
|
9462
9603
|
// else to go, and "retry shortly" would have the agent retry forever. It goes to the web chat.
|
|
9463
9604
|
const webOnlyReply = tool === "reply" && this.worlds.size === 0 && this.isWebOnlyFleet();
|
|
@@ -9649,19 +9790,14 @@ export class FleetManager {
|
|
|
9649
9790
|
if (routeToolCall(outAdapter, tool, args, threadId, respond)) {
|
|
9650
9791
|
return;
|
|
9651
9792
|
}
|
|
9652
|
-
// Log tool calls for activity visualization
|
|
9653
|
-
const senderLabel = senderSessionName ?? instanceName;
|
|
9654
|
-
this.eventLog?.logActivity("tool_call", senderLabel, this.summarizeToolCall(tool, args));
|
|
9655
|
-
// Dispatch fleet-specific tools via handler map
|
|
9656
|
-
const handler = outboundHandlers.get(tool);
|
|
9657
9793
|
if (handler) {
|
|
9794
|
+
this.eventLog?.logActivity("tool_call", senderSessionName ?? instanceName, this.summarizeToolCall(tool, args));
|
|
9658
9795
|
await handler(this, args, respond, {
|
|
9659
9796
|
instanceName, requestId, fleetRequestId, senderSessionName, operationId, sourceDaemonBootId,
|
|
9660
9797
|
});
|
|
9661
9798
|
}
|
|
9662
|
-
else
|
|
9799
|
+
else
|
|
9663
9800
|
respond(null, `Unknown tool: ${tool}`);
|
|
9664
|
-
}
|
|
9665
9801
|
}
|
|
9666
9802
|
/** Side effects of a routed reply: cancel-button lifecycle, logs, SSE, chat log. */
|
|
9667
9803
|
/** No chat platform is configured at all: the web dashboard is the fleet's only surface. */
|
|
@@ -11013,6 +11149,10 @@ export class FleetManager {
|
|
|
11013
11149
|
throw new Error(`Refusing to overwrite invalid fleet.yaml: ${this.rawFleetDocument.errors[0].message}`);
|
|
11014
11150
|
}
|
|
11015
11151
|
this.rawFleetConfig = loadRawFleetConfig(this.configPath);
|
|
11152
|
+
// #2208: what the file says before this save, aliases expanded — every write below is recorded, and anything that
|
|
11153
|
+
// changes outside them (a place sharing a node with one written) refuses the save.
|
|
11154
|
+
const fileBefore = this.rawFleetDocument.toJS({ maxAliasCount: -1 });
|
|
11155
|
+
const writes = this.fleetDocumentWrites = []; // a throw below leaves it to the next save
|
|
11016
11156
|
this.patchFleetDocument(this.rawFleetDocument, [], this.savedFleetConfigSnapshot, this.fleetConfig);
|
|
11017
11157
|
// Settings edits are expressed against the raw config, so apply them before
|
|
11018
11158
|
// canonicalization. A non-identity value equal to its inherited default is
|
|
@@ -11023,6 +11163,7 @@ export class FleetManager {
|
|
|
11023
11163
|
// (or a legacy scalar occupies that parent). Removing an override which
|
|
11024
11164
|
// is already absent is an idempotent no-op, not a failed Settings save.
|
|
11025
11165
|
if (this.rawFleetDocument.hasIn(patch.path)) {
|
|
11166
|
+
this.writingFleetDocument(this.rawFleetDocument, patch.path);
|
|
11026
11167
|
this.rawFleetDocument.deleteIn(patch.path);
|
|
11027
11168
|
}
|
|
11028
11169
|
}
|
|
@@ -11034,8 +11175,10 @@ export class FleetManager {
|
|
|
11034
11175
|
const rawAfterPatches = this.rawFleetDocument.toJS();
|
|
11035
11176
|
const redundantPaths = collectRedundantInstanceDefaultPaths(rawAfterPatches);
|
|
11036
11177
|
for (const path of redundantPaths) {
|
|
11037
|
-
if (this.rawFleetDocument.hasIn(path))
|
|
11178
|
+
if (this.rawFleetDocument.hasIn(path)) {
|
|
11179
|
+
this.writingFleetDocument(this.rawFleetDocument, path);
|
|
11038
11180
|
this.rawFleetDocument.deleteIn(path);
|
|
11181
|
+
}
|
|
11039
11182
|
// Avoid leaving empty operational maps such as `terminal: {}` while
|
|
11040
11183
|
// preserving the instance mapping itself and all surrounding comments.
|
|
11041
11184
|
for (let depth = path.length - 1; depth > 2; depth--) {
|
|
@@ -11043,9 +11186,14 @@ export class FleetManager {
|
|
|
11043
11186
|
const parent = this.rawFleetDocument.getIn(parentPath, true);
|
|
11044
11187
|
if (!isMap(parent) || parent.items.length > 0)
|
|
11045
11188
|
break;
|
|
11189
|
+
this.writingFleetDocument(this.rawFleetDocument, parentPath);
|
|
11046
11190
|
this.rawFleetDocument.deleteIn(parentPath);
|
|
11047
11191
|
}
|
|
11048
11192
|
}
|
|
11193
|
+
this.fleetDocumentWrites = null;
|
|
11194
|
+
const stray = changedOutside(fileBefore, this.rawFleetDocument.toJS({ maxAliasCount: -1 }), writes);
|
|
11195
|
+
if (stray)
|
|
11196
|
+
throw new Error(`Refusing to save fleet.yaml: this change would also change ${stray}, which shares a value with what it edits. Give each its own value in fleet.yaml.`);
|
|
11049
11197
|
noteSettingsWrite(this.configPath, this.savedFleetConfigSnapshot, this.fleetConfig);
|
|
11050
11198
|
const output = String(this.rawFleetDocument);
|
|
11051
11199
|
if (redundantPaths.length > 0)
|
|
@@ -11156,11 +11304,15 @@ export class FleetManager {
|
|
|
11156
11304
|
// Remove from the end so YAML sequence indexes do not shift underneath us.
|
|
11157
11305
|
for (let i = before.length - 1; i >= after.length; i--) {
|
|
11158
11306
|
const itemPath = [...path, i];
|
|
11159
|
-
if (document.hasIn(itemPath))
|
|
11307
|
+
if (document.hasIn(itemPath)) {
|
|
11308
|
+
this.writingFleetDocument(document, itemPath);
|
|
11160
11309
|
document.deleteIn(itemPath);
|
|
11310
|
+
}
|
|
11161
11311
|
}
|
|
11162
|
-
for (let i = shared; i < after.length; i++)
|
|
11312
|
+
for (let i = shared; i < after.length; i++) {
|
|
11313
|
+
this.writingFleetDocument(document, [...path, i]);
|
|
11163
11314
|
document.setIn([...path, i], after[i]);
|
|
11315
|
+
}
|
|
11164
11316
|
return;
|
|
11165
11317
|
}
|
|
11166
11318
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -11176,6 +11328,7 @@ export class FleetManager {
|
|
|
11176
11328
|
if (isMap(fresh)) {
|
|
11177
11329
|
const list = document.createNode([]);
|
|
11178
11330
|
list.items.push(fresh.clone());
|
|
11331
|
+
this.writingFleetDocument(document, ["channels"]);
|
|
11179
11332
|
document.setIn(["channels"], list);
|
|
11180
11333
|
}
|
|
11181
11334
|
}
|
|
@@ -11197,13 +11350,17 @@ export class FleetManager {
|
|
|
11197
11350
|
// Effective config contains inherited objects that may not exist in the
|
|
11198
11351
|
// raw YAML at all. yaml.deleteIn() is not idempotent for a missing nested
|
|
11199
11352
|
// parent, so guard it explicitly.
|
|
11200
|
-
if (document.hasIn(path))
|
|
11353
|
+
if (document.hasIn(path)) {
|
|
11354
|
+
this.writingFleetDocument(document, path);
|
|
11201
11355
|
document.deleteIn(path);
|
|
11356
|
+
}
|
|
11202
11357
|
}
|
|
11203
11358
|
else if (path.length === 0) {
|
|
11359
|
+
this.writingFleetDocument(document, path);
|
|
11204
11360
|
document.contents = document.createNode(after);
|
|
11205
11361
|
}
|
|
11206
11362
|
else {
|
|
11363
|
+
this.writingFleetDocument(document, path);
|
|
11207
11364
|
const currentNode = document.getIn(path, true);
|
|
11208
11365
|
if (isScalar(currentNode) && (after === null || typeof after !== "object")) {
|
|
11209
11366
|
currentNode.value = after;
|
|
@@ -11218,6 +11375,15 @@ export class FleetManager {
|
|
|
11218
11375
|
}
|
|
11219
11376
|
}
|
|
11220
11377
|
}
|
|
11378
|
+
/**
|
|
11379
|
+
* #2208: before every write to the raw document — the place is unshared first (an alias on the way becomes its own
|
|
11380
|
+
* copy; an anchor's other aliases get theirs), so the write changes that place only; and it is recorded for the
|
|
11381
|
+
* save's check that nothing else changed.
|
|
11382
|
+
*/
|
|
11383
|
+
writingFleetDocument(document, path) {
|
|
11384
|
+
detachForWrite(document, path);
|
|
11385
|
+
this.fleetDocumentWrites?.push([...path]);
|
|
11386
|
+
}
|
|
11221
11387
|
getSettingsConfigPath() { return this.configPath; }
|
|
11222
11388
|
async removeInstance(name, authorization, execution) {
|
|
11223
11389
|
if (execution)
|
|
@@ -20493,7 +20659,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20493
20659
|
all = false;
|
|
20494
20660
|
const message = err?.message ?? String(err);
|
|
20495
20661
|
this.adapterState.set(id, { status: "retrying", retryCount: 0, lastError: message });
|
|
20496
|
-
this.scheduleAdapterRetry(id
|
|
20662
|
+
this.scheduleAdapterRetry(id);
|
|
20497
20663
|
}
|
|
20498
20664
|
}
|
|
20499
20665
|
this.classicChannels?.configureAdapters(this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : []));
|