@songsid/agend 2.3.4-beta.8 → 2.3.4-beta.9
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/THIRD_PARTY_NOTICES.md +37 -0
- package/dist/agent-cli-instructions.md +1 -1
- package/dist/agent-credentials.d.ts +24 -0
- package/dist/agent-credentials.js +66 -0
- package/dist/agent-credentials.js.map +1 -0
- package/dist/agent-endpoint.d.ts +3 -1
- package/dist/agent-endpoint.js +13 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/authority/session-authorizations.d.ts +28 -4
- package/dist/authority/session-authorizations.js +63 -17
- package/dist/authority/session-authorizations.js.map +1 -1
- package/dist/backend/antigravity.js +3 -1
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/codex-footer-hints.d.ts +4 -0
- package/dist/backend/codex-footer-hints.js +27 -0
- package/dist/backend/codex-footer-hints.js.map +1 -0
- package/dist/backend/codex.js +8 -3
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/muse.js +5 -3
- package/dist/backend/muse.js.map +1 -1
- package/dist/cache-ledger.d.ts +85 -0
- package/dist/cache-ledger.js +236 -1
- package/dist/cache-ledger.js.map +1 -1
- package/dist/cache-service.d.ts +5 -0
- package/dist/cache-service.js +16 -4
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.js +11 -3
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.js +4 -3
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +1 -1
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/cross-instance-notice.d.ts +10 -0
- package/dist/cross-instance-notice.js +67 -5
- package/dist/cross-instance-notice.js.map +1 -1
- package/dist/daemon.d.ts +3 -0
- package/dist/daemon.js +11 -4
- package/dist/daemon.js.map +1 -1
- package/dist/export-import.d.ts +7 -0
- package/dist/export-import.js +44 -10
- package/dist/export-import.js.map +1 -1
- package/dist/fleet-context.d.ts +2 -0
- package/dist/fleet-manager.d.ts +82 -4
- package/dist/fleet-manager.js +361 -29
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +1 -0
- package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +1 -1
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
- package/dist/instance-lifecycle.d.ts +6 -2
- package/dist/instance-lifecycle.js +11 -6
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instance-topic-delete.d.ts +77 -0
- package/dist/instance-topic-delete.js +123 -0
- package/dist/instance-topic-delete.js.map +1 -0
- package/dist/instructions.d.ts +1 -1
- package/dist/instructions.js +1 -1
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +23 -1
- package/dist/locale.js.map +1 -1
- package/dist/log-steps.d.ts +4 -3
- package/dist/log-steps.js +32 -7
- package/dist/log-steps.js.map +1 -1
- package/dist/outbound-handlers.js +3 -3
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.js +5 -2
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/scheduler/db.d.ts +2 -1
- package/dist/scheduler/db.js +23 -9
- package/dist/scheduler/db.js.map +1 -1
- package/dist/settings-api.d.ts +13 -0
- package/dist/settings-api.js +12 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/tool-progress.d.ts +2 -0
- package/dist/tool-progress.js +19 -3
- package/dist/tool-progress.js.map +1 -1
- package/dist/topic-commands.js +4 -0
- package/dist/topic-commands.js.map +1 -1
- package/dist/ui/chat-strings.js +2 -2
- package/dist/ui/fleet-usage.js +90 -27
- package/dist/ui/panel-chat.js +2 -0
- package/dist/ui/settings-strings.js +2 -2
- package/dist/ui/shared/app.css +15 -0
- package/dist/ui/shared/usage-dialog.js +18 -1
- package/dist/ui/shared/view-strings.js +16 -0
- package/dist/usage/capacity.d.ts +69 -0
- package/dist/usage/capacity.js +119 -0
- package/dist/usage/capacity.js.map +1 -0
- package/dist/usage/i18n-keys.d.ts +1 -1
- package/dist/usage/i18n-keys.js +3 -0
- package/dist/usage/i18n-keys.js.map +1 -1
- package/dist/usage/profile-timeline.d.ts +16 -0
- package/dist/usage/profile-timeline.js +58 -0
- package/dist/usage/profile-timeline.js.map +1 -0
- package/dist/usage/quota-history.d.ts +43 -0
- package/dist/usage/quota-history.js +168 -0
- package/dist/usage/quota-history.js.map +1 -0
- package/dist/usage/usage-api.d.ts +31 -0
- package/dist/usage/usage-api.js +80 -1
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/usage-report.d.ts +25 -1
- package/dist/usage-report.js +55 -4
- package/dist/usage-report.js.map +1 -1
- package/dist/usage-rollup.d.ts +4 -1
- package/dist/usage-rollup.js +12 -3
- package/dist/usage-rollup.js.map +1 -1
- package/dist/web-api.d.ts +6 -0
- package/dist/web-api.js +8 -2
- package/dist/web-api.js.map +1 -1
- package/dist/web-session-authorization.d.ts +13 -2
- package/dist/web-session-authorization.js +10 -3
- package/dist/web-session-authorization.js.map +1 -1
- package/package.json +1 -1
package/dist/fleet-manager.js
CHANGED
|
@@ -3,6 +3,7 @@ import { listKiroV1Sessions, readKiroClassicLinks } from "./backend/kiro-identit
|
|
|
3
3
|
import { readKiroV1SavedModel } from "./backend/kiro-retired-model.js";
|
|
4
4
|
import { sharedKiroV2StoreLane } from "./backend/kiro-v2-store.js";
|
|
5
5
|
import { SEPARATE_FEATURE_KEYS, hasLegacyOffice, normalizeOfficeStorage } from "./dashboard-permissions.js";
|
|
6
|
+
import { maskSecrets } from "./log-steps.js";
|
|
6
7
|
import { loginNoticeMode, normalizeNoticeDelivery, setNoticeMode } from "./notice-delivery.js";
|
|
7
8
|
import { DEFAULT_REPLY_TRIGGERS } from "./reply-triggers.js";
|
|
8
9
|
import { WorkingRecoveryCoordinator, WORKING_RECOVERY_KIND, buildWorkingRecoveryPrompt } from "./working-recovery.js";
|
|
@@ -75,6 +76,7 @@ import { isProbeableRouteTarget } from "./fleet-context.js";
|
|
|
75
76
|
import { getEffectiveInstanceDefaults, loadFleetConfig, loadRawFleetConfig, loadFleetConfigSource, loadRawFleetConfigSource, DEFAULT_COST_GUARD, DEFAULT_DAILY_SUMMARY, DEFAULT_INSTANCE_CONFIG, autoGeneralInstanceConfig } from "./config.js";
|
|
76
77
|
import { EventLog } from "./event-log.js";
|
|
77
78
|
import { binaryProbe } from "./binary-probe.js";
|
|
79
|
+
import { deletePlannedTopic, planInstanceTopicDelete, topicDestination, topicMustStay } from "./instance-topic-delete.js";
|
|
78
80
|
import { AVAILABILITY_STALE_MS, BackendAvailabilityStore, hostAvailabilityProbes, findExecutableIn, resolveBackendBinary } from "./backend-availability.js";
|
|
79
81
|
import { classifySqliteOpenError } from "./sqlite-open-errors.js";
|
|
80
82
|
import { OutboxOpenError } from "./outbox-open-error.js";
|
|
@@ -201,7 +203,7 @@ import { envFileKeys } from "./token-env-name.js";
|
|
|
201
203
|
import { discordGeneralChannelId } from "./discord-general.js";
|
|
202
204
|
import { parseEnvText } from "./env-file.js";
|
|
203
205
|
import { isDisallowedIntentsError, isRejectedTokenError } from "./discord-permissions.js";
|
|
204
|
-
import { filterUsageProviders, formatDiscordUsageActivity, getUsageSnapshot, handleUsageRequest, isUsagePath, usageProviderIdForBackend } from "./usage/usage-api.js";
|
|
206
|
+
import { filterUsageProviders, formatDiscordUsageActivity, getUsageSnapshot, handleUsageRequest, isUsagePath, setCapacityLookup, usageProviderIdForBackend } from "./usage/usage-api.js";
|
|
205
207
|
import { LOGIN_FLOWS, checkAuthStatus, LOGIN_BACKEND_ALIASES } from "./login-flows.js";
|
|
206
208
|
import { LoginSession } from "./login-manager.js";
|
|
207
209
|
import { runOnLoginShell } from "./login-shell.js";
|
|
@@ -232,6 +234,8 @@ import { remoteCardAction, remoteCardChoiceLabel, remoteCardOutcomeText, remoteC
|
|
|
232
234
|
import { recentChatContext } from "./log-tail.js";
|
|
233
235
|
import { describeSignalSource, recordInternalRequest, withOrigin } from "./fleet-control-audit.js";
|
|
234
236
|
import { handleAgentRequest, ToolNotPermittedError } from "./agent-endpoint.js";
|
|
237
|
+
import { AgentCredentials, agentCredentialPolicy } from "./agent-credentials.js";
|
|
238
|
+
import { writeSecretFile } from "./secret-file.js";
|
|
235
239
|
import { ConfigHistory, ConfigSyntaxError, configIssueText } from "./config-history.js";
|
|
236
240
|
import { ClassicChannelManager, classicWebEchoOn, getClassicBackendChoices, inferClassicChannelType, isSelectableClassicBackend, readClassicLastActivityAt, readClassicRoomNames } from "./classic-channel-manager.js";
|
|
237
241
|
import { assertExplicitInstanceRemoval } from "./instance-removal.js";
|
|
@@ -290,7 +294,7 @@ import { PROVIDER_SECRET_SPECS, PUBLIC_TUNNEL_SECRET_SPEC, providerSecretSpec, p
|
|
|
290
294
|
const SELF_RESTART_DEADLINE_MS = 300_000;
|
|
291
295
|
import { APPLY_FLEET_TARGET, ApplyJobStore, viewOf, } from "./apply-job.js";
|
|
292
296
|
import { kiroHome } from "./backend/kiro-kas-store.js";
|
|
293
|
-
import { CHAT_PROFILE_LOGIN_BACKENDS, credentialHomeSpec, credentialProfileHome, credentialProfileLogin, credentialProfileLoginCommand, credentialProfileStoreHome, credentialSwitchCarriesConversation, credentialSwitchStartsFresh, instanceCredentialProfile } from "./backend/credential-profile.js";
|
|
297
|
+
import { CHAT_PROFILE_LOGIN_BACKENDS, credentialHomeSpec, credentialProfileHome, credentialProfileLogin, credentialProfileLoginCommand, credentialProfileStoreHome, credentialSwitchCarriesConversation, credentialSwitchStartsFresh, instanceCredentialProfile, listConfiguredProfiles } from "./backend/credential-profile.js";
|
|
294
298
|
import { carryClaudeConversation } from "./backend/claude-carry.js";
|
|
295
299
|
import { mergeBackendOptions, profileKey, profileName, profileOf } from "./classic-bindings.js";
|
|
296
300
|
import { classifyInstanceChange, CLASSIC_HOT_CONFIG_KEYS, HOT_INSTANCE_CONFIG_KEYS, hotConfigUpdate, } from "./instance-config-impact.js";
|
|
@@ -306,6 +310,12 @@ import { BackendOutageTracker } from "./backend-outage.js";
|
|
|
306
310
|
import { canUnlockAdvancedTips, DailyTipScheduler, selectTip, visibleTipLevels, } from "./tips.js";
|
|
307
311
|
import { getTmuxSession } from "./config.js";
|
|
308
312
|
import { OwnStatusMessages, discordMessageKey } from "./chat-log-ids.js";
|
|
313
|
+
import { PROFILE_TIMELINE_FILE, PROFILE_UNRECORDED, recordProfileLaunch } from "./usage/profile-timeline.js";
|
|
314
|
+
import { readQuotaHistory } from "./usage/quota-history.js";
|
|
315
|
+
import { estimateCapacity } from "./usage/capacity.js";
|
|
316
|
+
import { covered, hourSources } from "./usage-rollup.js";
|
|
317
|
+
import { KEEP_DAYS } from "./cache-ledger.js";
|
|
318
|
+
import { ratesFor } from "./cache-prices.js";
|
|
309
319
|
// Closed migration cohort: these 11 names existed immediately before the first
|
|
310
320
|
// managed-skill publisher (a1ac499f; parent e267e684). Never grow this set when
|
|
311
321
|
// adding a bundled skill: its new name is not proof of old AgEnD ownership.
|
|
@@ -1031,6 +1041,9 @@ export class FleetManager {
|
|
|
1031
1041
|
discordPresenceEagerPending = false;
|
|
1032
1042
|
discordPresenceInFlight = null;
|
|
1033
1043
|
static DISCORD_PRESENCE_REFRESH_MS = 15 * 60_000;
|
|
1044
|
+
/** #3066 §1d: how often a fleet with no Discord bot reads the quota for its history (the user's choice: 30 min). */
|
|
1045
|
+
static QUOTA_BACKGROUND_READ_MS = 30 * 60_000;
|
|
1046
|
+
quotaReadTimer = null;
|
|
1034
1047
|
/** Days of event/activity history to keep. */
|
|
1035
1048
|
static EVENT_LOG_RETENTION_DAYS = 30;
|
|
1036
1049
|
watchdogTimer = null;
|
|
@@ -2154,6 +2167,22 @@ export class FleetManager {
|
|
|
2154
2167
|
* startup-only key at startup. See checkStartupSignatureConsistency(). */
|
|
2155
2168
|
fleetSignatureMismatch = null;
|
|
2156
2169
|
healthServerListening = false;
|
|
2170
|
+
agentCredentialsCache;
|
|
2171
|
+
get agentCredentials() {
|
|
2172
|
+
return this.agentCredentialsCache ??= new AgentCredentials(name => agentCredentialPolicy(this.fleetConfig, name, this.isClassicInstance(name)), name => {
|
|
2173
|
+
try {
|
|
2174
|
+
const token = randomBytes(32).toString("hex");
|
|
2175
|
+
const result = writeSecretFile(join(this.getInstanceDir(name), "agent.token"), token);
|
|
2176
|
+
if (!result.ok)
|
|
2177
|
+
this.logger.error({ name, mode: result.mode, reason: result.reason }, "Agent token file is not owner-only");
|
|
2178
|
+
return token;
|
|
2179
|
+
}
|
|
2180
|
+
catch {
|
|
2181
|
+
this.logger.error({ name }, "Agent credential renewal failed; prior generation remains retired");
|
|
2182
|
+
return null;
|
|
2183
|
+
}
|
|
2184
|
+
});
|
|
2185
|
+
}
|
|
2157
2186
|
constructor(dataDir) {
|
|
2158
2187
|
this.dataDir = dataDir;
|
|
2159
2188
|
FleetManager.signalTarget = this;
|
|
@@ -2534,6 +2563,10 @@ export class FleetManager {
|
|
|
2534
2563
|
const daemon = this.daemons.get(name);
|
|
2535
2564
|
if (daemon?.bootId === daemonBootId)
|
|
2536
2565
|
this.listenForWorkingState(name, daemon);
|
|
2566
|
+
// #3066 §1d: a Codex launch records the credential profile it runs under, so its usage is told by account.
|
|
2567
|
+
if (daemon?.bootId === daemonBootId && this.backendNameOf(name) === "codex") {
|
|
2568
|
+
void recordProfileLaunch(join(this.getInstanceDir(name), PROFILE_TIMELINE_FILE), this.credentialProfileOf(name, "codex") ?? "default");
|
|
2569
|
+
}
|
|
2537
2570
|
// #2802: no post-restart notices. The outbox still delivers or fails a row accepted before the restart, and an open
|
|
2538
2571
|
// request is reminded by the ordinary turn-ended nudge (sweepReplyObligations) once the owner is idle.
|
|
2539
2572
|
this.scheduleDeliveryOutboxPump();
|
|
@@ -3371,6 +3404,7 @@ export class FleetManager {
|
|
|
3371
3404
|
* guild channel.
|
|
3372
3405
|
*/
|
|
3373
3406
|
reregisterClassicChannels() {
|
|
3407
|
+
this.agentCredentialsCache?.observePolicies();
|
|
3374
3408
|
if (!this.classicChannels)
|
|
3375
3409
|
return;
|
|
3376
3410
|
const channels = this.classicChannels.getAll();
|
|
@@ -8591,6 +8625,9 @@ export class FleetManager {
|
|
|
8591
8625
|
if (others.length > 0 || classicAtBoot.length > 0) {
|
|
8592
8626
|
await this.startInstancesWithConcurrency(others, topicMode, () => startupProgress.markReady(), classicAtBoot);
|
|
8593
8627
|
}
|
|
8628
|
+
// #3066 §1d (Fable r1): on every fleet — a web-only one (#2910) has no chat connection at all, and is the clearest
|
|
8629
|
+
// case with no Discord bot reading. Each tick decides for itself whether to read (backgroundQuotaRead).
|
|
8630
|
+
this.startQuotaBackgroundReads();
|
|
8594
8631
|
if (topicMode && (fleet.channel || fleet.channels?.length)) {
|
|
8595
8632
|
await adapterStartup;
|
|
8596
8633
|
this.startDiscordUsagePresence();
|
|
@@ -8762,6 +8799,178 @@ export class FleetManager {
|
|
|
8762
8799
|
this.logger.warn({ health }, "Fleet started with problems — see /health");
|
|
8763
8800
|
}
|
|
8764
8801
|
}
|
|
8802
|
+
/**
|
|
8803
|
+
* #3066 §1d (the user: yes, every 30 min): the quota history (usage/quota-history.ts) records only the readings AgEnD
|
|
8804
|
+
* makes. A Discord bot's presence refresh reads every 15 min; without one, nothing reads while nobody looks. So, with no
|
|
8805
|
+
* Discord bot and while some account has a running agent, read every 30 min — the same cached read (its 5-minute TTL
|
|
8806
|
+
* still bounds the vendor calls), recorded like any other.
|
|
8807
|
+
*/
|
|
8808
|
+
startQuotaBackgroundReads() {
|
|
8809
|
+
if (this.quotaReadTimer)
|
|
8810
|
+
clearInterval(this.quotaReadTimer);
|
|
8811
|
+
this.quotaReadTimer = setInterval(() => { void this.backgroundQuotaRead(); }, FleetManager.QUOTA_BACKGROUND_READ_MS);
|
|
8812
|
+
this.quotaReadTimer.unref?.();
|
|
8813
|
+
// #3066 §1: the measured capacity, published for the usage reads (get_usage, the page) — refreshed every 10 min.
|
|
8814
|
+
setCapacityLookup((provider, metric) => this.capacityEstimates.get(`${provider}\u0000${metric}`) ?? null);
|
|
8815
|
+
if (this.capacityTimer)
|
|
8816
|
+
clearInterval(this.capacityTimer);
|
|
8817
|
+
void this.refreshCapacity();
|
|
8818
|
+
this.capacityTimer = setInterval(() => { void this.refreshCapacity(); }, FleetManager.CAPACITY_REFRESH_MS);
|
|
8819
|
+
this.capacityTimer.unref?.();
|
|
8820
|
+
}
|
|
8821
|
+
/** #3066 §1: the latest estimates, by `provider\0metric` (what usage-api's lookup reads, synchronously). */
|
|
8822
|
+
capacityEstimates = new Map();
|
|
8823
|
+
capacityTimer = null;
|
|
8824
|
+
static CAPACITY_REFRESH_MS = 10 * 60_000;
|
|
8825
|
+
/**
|
|
8826
|
+
* #3066 §1: estimate each account's weekly 100% from the quota history (§1d) and the usage ledger's hours — both read
|
|
8827
|
+
* asynchronously, the arithmetic small (usage/capacity.ts). An account is its usage provider id ("claude",
|
|
8828
|
+
* "claude:hsinlin", "codex:yanxianchen"): its hours are the ledger sources of that backend under that credential profile
|
|
8829
|
+
* (Codex's "unrecorded" hours are no account's). A per-model weekly ("Sonnet (weekly)") counts only that model's tokens.
|
|
8830
|
+
*
|
|
8831
|
+
* Prism #3073: no estimate rather than a wrong one —
|
|
8832
|
+
* - a window is measured only where the ledger has its hours: kept (31 days) and scanned (`days.coverage`) for every
|
|
8833
|
+
* instance of the account's backend, and every instance whose hours count for it; the quota history goes back 12 weeks;
|
|
8834
|
+
* - a profile literally named "default", "unknown" or "unrecorded" (configured, on disk, or in the quota history) makes
|
|
8835
|
+
* those hours ambiguous: neither the host
|
|
8836
|
+
* account nor the named one is estimated (each would be credited the other's use, or lose its own);
|
|
8837
|
+
* - only a metric the vendor marks per-model (`scope: "model"`) is filtered by model; another named weekly pool
|
|
8838
|
+
* ("Luna Reserve (weekly)", Antigravity pools) is no model name and is not estimated.
|
|
8839
|
+
*/
|
|
8840
|
+
async refreshCapacity() {
|
|
8841
|
+
try {
|
|
8842
|
+
const readings = await readQuotaHistory();
|
|
8843
|
+
const groups = new Map();
|
|
8844
|
+
for (const r of readings) {
|
|
8845
|
+
const k = `${r.provider}\u0000${r.metric}`;
|
|
8846
|
+
(groups.get(k) ?? groups.set(k, []).get(k)).push(r);
|
|
8847
|
+
}
|
|
8848
|
+
if (!groups.size) {
|
|
8849
|
+
this.capacityEstimates = new Map();
|
|
8850
|
+
return 0;
|
|
8851
|
+
}
|
|
8852
|
+
const data = await this.cacheServiceFor().usageData();
|
|
8853
|
+
const now = Date.now(), DAY = 24 * 3_600_000, HOUR = 3_600_000;
|
|
8854
|
+
const keptFrom = now - KEEP_DAYS * DAY + HOUR; // the oldest hour still in every ledger (an hour of margin)
|
|
8855
|
+
// Profiles whose names are also the ledger's own tags: their hours cannot be told from the host's / unattributed.
|
|
8856
|
+
// Any trace of such a profile counts (Prism #3073 r2), not only today's config — a binding removed or switched
|
|
8857
|
+
// leaves its hours in the ledger: configured now, its directory on this host, or its account in the quota history.
|
|
8858
|
+
const RESERVED = new Set(["default", "unknown", PROFILE_UNRECORDED]);
|
|
8859
|
+
const historyProviders = new Set(readings.map((r) => r.provider));
|
|
8860
|
+
const reservedNamed = new Map();
|
|
8861
|
+
const reservedFor = (backend) => {
|
|
8862
|
+
const known = reservedNamed.get(backend);
|
|
8863
|
+
if (known)
|
|
8864
|
+
return known;
|
|
8865
|
+
const found = new Set(listConfiguredProfiles(this.fleetConfig, backend).filter((n) => RESERVED.has(n)));
|
|
8866
|
+
const base = usageProviderIdForBackend(backend);
|
|
8867
|
+
for (const n of RESERVED) {
|
|
8868
|
+
if (base && historyProviders.has(`${base}:${n}`))
|
|
8869
|
+
found.add(n);
|
|
8870
|
+
if (credentialHomeSpec(backend) && existsSync(credentialProfileHome(this.dataDir, backend, n)))
|
|
8871
|
+
found.add(n);
|
|
8872
|
+
}
|
|
8873
|
+
return reservedNamed.set(backend, found).get(backend);
|
|
8874
|
+
};
|
|
8875
|
+
// Every instance of a backend may have used any of its accounts (bindings change): an account is measured only
|
|
8876
|
+
// where all of them were scanned (Prism #3073 r2) — one still waiting for its first scan has no hours yet, not none.
|
|
8877
|
+
const daysOfBase = new Map();
|
|
8878
|
+
for (const inst of data.instances) {
|
|
8879
|
+
const base = usageProviderIdForBackend(inst.inst.backend);
|
|
8880
|
+
if (base)
|
|
8881
|
+
(daysOfBase.get(base) ?? daysOfBase.set(base, []).get(base)).push(inst.days);
|
|
8882
|
+
}
|
|
8883
|
+
// provider id → hour → model key → totals; and the instances whose hours count for it (their scan coverage).
|
|
8884
|
+
const byProvider = new Map();
|
|
8885
|
+
const daysOf = new Map();
|
|
8886
|
+
for (const inst of data.instances) {
|
|
8887
|
+
for (const [hk, hour] of Object.entries(inst.ledger.hours)) {
|
|
8888
|
+
for (const src of hourSources(hour)) {
|
|
8889
|
+
const base = usageProviderIdForBackend(src.backend);
|
|
8890
|
+
if (!base)
|
|
8891
|
+
continue;
|
|
8892
|
+
// Ambiguous (a named profile with the tag's name), or no account's: counted for no one, so an account whose
|
|
8893
|
+
// hours these may be has none — and gets no estimate, never a low one.
|
|
8894
|
+
if (reservedFor(src.backend).has(src.profile) || src.profile === "unknown" || src.profile === PROFILE_UNRECORDED)
|
|
8895
|
+
continue;
|
|
8896
|
+
const provider = src.profile === "default" ? base : `${base}:${src.profile}`;
|
|
8897
|
+
const list = daysOf.get(provider) ?? daysOf.set(provider, []).get(provider);
|
|
8898
|
+
if (!list.includes(inst.days))
|
|
8899
|
+
list.push(inst.days);
|
|
8900
|
+
const hours = byProvider.get(provider) ?? byProvider.set(provider, new Map()).get(provider);
|
|
8901
|
+
const into = hours.get(Number(hk)) ?? hours.set(Number(hk), Object.create(null)).get(Number(hk));
|
|
8902
|
+
for (const [mk, tot] of Object.entries(src.m)) {
|
|
8903
|
+
const cur = into[mk] ??= [0, 0, 0, 0, 0, 0, 0];
|
|
8904
|
+
for (let i = 0; i < 7; i++)
|
|
8905
|
+
cur[i] = (cur[i] ?? 0) + (tot[i] ?? 0);
|
|
8906
|
+
}
|
|
8907
|
+
}
|
|
8908
|
+
}
|
|
8909
|
+
}
|
|
8910
|
+
const next = new Map();
|
|
8911
|
+
for (const [key, list] of groups) {
|
|
8912
|
+
const [provider, metric] = key.split("\u0000");
|
|
8913
|
+
// A per-model weekly ("Sonnet (weekly)", the vendor's scope "model"): that model's tokens only. Another named
|
|
8914
|
+
// pool is not a model: no estimate. "Weekly", "Weekly limit": everything.
|
|
8915
|
+
const named = /^(.+) \((?:weekly|monthly|\d+d)\)$/i.exec(metric)?.[1]?.toLowerCase() ?? null;
|
|
8916
|
+
const perModel = list.some((r) => r.scope === "model");
|
|
8917
|
+
if (named && !perModel)
|
|
8918
|
+
continue;
|
|
8919
|
+
const hours = [];
|
|
8920
|
+
for (const [hour, models] of byProvider.get(provider) ?? []) {
|
|
8921
|
+
let api = 0, output = 0, all = 0;
|
|
8922
|
+
for (const [mk, tot] of Object.entries(models)) {
|
|
8923
|
+
const bar = mk.lastIndexOf("|"), model = mk.slice(0, bar), long = mk.slice(bar + 1) === "1";
|
|
8924
|
+
if (named && !model.toLowerCase().includes(named))
|
|
8925
|
+
continue;
|
|
8926
|
+
const [, uncached, read, w5, w1h, w, out] = tot;
|
|
8927
|
+
const r = ratesFor(model, long ? Number.MAX_SAFE_INTEGER : 0);
|
|
8928
|
+
api = api === null || !r ? null : api + (uncached * r.input + read * r.read + w5 * r.write5m + w1h * r.write1h + w * r.write + out * r.output) / 1_000_000;
|
|
8929
|
+
output += out;
|
|
8930
|
+
all += uncached + read + w5 + w1h + w + out;
|
|
8931
|
+
}
|
|
8932
|
+
if (all > 0)
|
|
8933
|
+
hours.push({ hour, api, output, all });
|
|
8934
|
+
}
|
|
8935
|
+
const base = provider.split(":")[0];
|
|
8936
|
+
const scanned = [...new Set([...(daysOf.get(provider) ?? []), ...(daysOfBase.get(base) ?? [])])];
|
|
8937
|
+
const available = (from, to) => {
|
|
8938
|
+
if (from < keptFrom || !scanned.length)
|
|
8939
|
+
return false;
|
|
8940
|
+
for (let h = from; h <= to; h += HOUR)
|
|
8941
|
+
if (!scanned.every((d) => covered(d, h)))
|
|
8942
|
+
return false;
|
|
8943
|
+
return true;
|
|
8944
|
+
};
|
|
8945
|
+
const est = estimateCapacity(list, hours, available);
|
|
8946
|
+
if (est)
|
|
8947
|
+
next.set(key, est);
|
|
8948
|
+
}
|
|
8949
|
+
this.capacityEstimates = next;
|
|
8950
|
+
return next.size;
|
|
8951
|
+
}
|
|
8952
|
+
catch (err) {
|
|
8953
|
+
this.logger.debug({ err }, "capacity estimate refresh failed");
|
|
8954
|
+
return -1;
|
|
8955
|
+
}
|
|
8956
|
+
}
|
|
8957
|
+
/** One background quota read, when nothing else reads (no Discord bot) and an account has a running agent. */
|
|
8958
|
+
async backgroundQuotaRead() {
|
|
8959
|
+
if (this.shuttingDown)
|
|
8960
|
+
return false;
|
|
8961
|
+
if ([...this.adapters.values()].some(a => a.type === "discord" && typeof a.setActivity === "function"))
|
|
8962
|
+
return false;
|
|
8963
|
+
const active = this.getActiveUsageProviderIds();
|
|
8964
|
+
if (active.size === 0)
|
|
8965
|
+
return false;
|
|
8966
|
+
try {
|
|
8967
|
+
await getUsageSnapshot(false, active);
|
|
8968
|
+
return true;
|
|
8969
|
+
}
|
|
8970
|
+
catch {
|
|
8971
|
+
return false;
|
|
8972
|
+
}
|
|
8973
|
+
}
|
|
8765
8974
|
/** Keep Discord profile activity aligned with the same cached usage source as /usage. */
|
|
8766
8975
|
startDiscordUsagePresence() {
|
|
8767
8976
|
if (this.discordPresenceTimer)
|
|
@@ -11749,6 +11958,11 @@ export class FleetManager {
|
|
|
11749
11958
|
let result;
|
|
11750
11959
|
switch (msg.type) {
|
|
11751
11960
|
case "fleet_decision_create": {
|
|
11961
|
+
const supersedes = this.decisionSupersedes("ipc-typed", instanceName, payload.supersedes);
|
|
11962
|
+
if ("error" in supersedes) {
|
|
11963
|
+
ipc.send({ type: "fleet_decision_response", fleetRequestId, error: supersedes.error });
|
|
11964
|
+
return;
|
|
11965
|
+
}
|
|
11752
11966
|
// Prune expired decisions on create
|
|
11753
11967
|
db.pruneExpiredDecisions();
|
|
11754
11968
|
result = db.createDecision({
|
|
@@ -11759,7 +11973,7 @@ export class FleetManager {
|
|
|
11759
11973
|
tags: payload.tags,
|
|
11760
11974
|
ttl_days: payload.ttl_days,
|
|
11761
11975
|
created_by: instanceName,
|
|
11762
|
-
supersedes:
|
|
11976
|
+
supersedes: supersedes.id,
|
|
11763
11977
|
});
|
|
11764
11978
|
break;
|
|
11765
11979
|
}
|
|
@@ -11936,6 +12150,21 @@ export class FleetManager {
|
|
|
11936
12150
|
return "ok";
|
|
11937
12151
|
}
|
|
11938
12152
|
}
|
|
12153
|
+
/**
|
|
12154
|
+
* `post_decision` with `supersedes` changes a decision someone else may have recorded (it stops being active): the
|
|
12155
|
+
* same permission as `update_decision`, asked through the same live check (tool set + dashboard switches), on both
|
|
12156
|
+
* create paths. An empty or absent value supersedes nothing and needs nothing; anything else must be an id.
|
|
12157
|
+
*/
|
|
12158
|
+
decisionSupersedes(sink, instanceName, value) {
|
|
12159
|
+
if (value === undefined || value === null || value === "")
|
|
12160
|
+
return { id: undefined };
|
|
12161
|
+
if (typeof value !== "string")
|
|
12162
|
+
return { error: "supersedes must be a decision id" };
|
|
12163
|
+
const permitted = this.checkToolPermission(sink, instanceName, "update_decision");
|
|
12164
|
+
if (!permitted.allowed)
|
|
12165
|
+
return { error: `Superseding a decision needs update_decision. ${permitted.message}` };
|
|
12166
|
+
return { id: value };
|
|
12167
|
+
}
|
|
11939
12168
|
async handleDecisionCrudHttp(instance, op, args) {
|
|
11940
12169
|
if (!this.scheduler)
|
|
11941
12170
|
return { error: "Scheduler not available" };
|
|
@@ -11951,16 +12180,24 @@ export class FleetManager {
|
|
|
11951
12180
|
if (!title || !content)
|
|
11952
12181
|
return { error: "title and content are required" };
|
|
11953
12182
|
const scope = args.scope === "fleet" ? "fleet" : "project";
|
|
11954
|
-
|
|
11955
|
-
|
|
11956
|
-
|
|
11957
|
-
|
|
11958
|
-
|
|
11959
|
-
|
|
11960
|
-
|
|
11961
|
-
|
|
11962
|
-
|
|
11963
|
-
|
|
12183
|
+
const supersedes = this.decisionSupersedes("agent-endpoint", instance, args.supersedes);
|
|
12184
|
+
if ("error" in supersedes)
|
|
12185
|
+
return { error: supersedes.error };
|
|
12186
|
+
try {
|
|
12187
|
+
return db.createDecision({
|
|
12188
|
+
project_root: projectRoot,
|
|
12189
|
+
scope,
|
|
12190
|
+
title,
|
|
12191
|
+
content,
|
|
12192
|
+
tags: asStrArr(args.tags),
|
|
12193
|
+
ttl_days: asNum(args.ttl_days),
|
|
12194
|
+
supersedes: supersedes.id,
|
|
12195
|
+
created_by: instance,
|
|
12196
|
+
});
|
|
12197
|
+
}
|
|
12198
|
+
catch (err) {
|
|
12199
|
+
return { error: err.message }; // a refused predecessor: nothing was inserted
|
|
12200
|
+
}
|
|
11964
12201
|
}
|
|
11965
12202
|
// #2152: titles by default (agend-agent decision-list); one decision's full text by id (decision-get <id>).
|
|
11966
12203
|
// The CLI sends include_archived (snake case, as the MCP tool): both spellings are read.
|
|
@@ -12249,16 +12486,89 @@ export class FleetManager {
|
|
|
12249
12486
|
}
|
|
12250
12487
|
return adapter.createTopic(topicName);
|
|
12251
12488
|
}
|
|
12252
|
-
|
|
12253
|
-
|
|
12254
|
-
|
|
12255
|
-
|
|
12256
|
-
|
|
12257
|
-
|
|
12258
|
-
|
|
12259
|
-
|
|
12260
|
-
this.
|
|
12261
|
-
|
|
12489
|
+
/**
|
|
12490
|
+
* Delete an agent and, unless asked not to, its own chat topic (user decision: by default). The topic is planned
|
|
12491
|
+
* before the removal (its id and bot go with the fleet.yaml entry) and deleted through the agent's OWN bot after it;
|
|
12492
|
+
* the outcome is the platform's answer (instance-topic-delete.ts). A topic that cannot go never fails the removal.
|
|
12493
|
+
*/
|
|
12494
|
+
async removeInstanceAndTopic(name, authorization, opts) {
|
|
12495
|
+
const plan = opts.deleteTopic ? this.planInstanceTopicDelete(name) : null;
|
|
12496
|
+
if (opts.execution)
|
|
12497
|
+
await this.removeInstance(name, authorization, opts.execution);
|
|
12498
|
+
else
|
|
12499
|
+
await this.removeInstance(name, authorization);
|
|
12500
|
+
// #3055 review (Prism P1): asked again right before the platform call — the fleet as it is NOW, not as planned
|
|
12501
|
+
// before the removal was awaited (a new owner of the topic, or the bot rebuilt).
|
|
12502
|
+
return deletePlannedTopic(plan, this.logger, (p) => {
|
|
12503
|
+
if (this.instanceTopicAdapter(p.adapterId) !== p.adapter)
|
|
12504
|
+
return "the agent's bot changed while it was being deleted, so its topic was left";
|
|
12505
|
+
return topicMustStay(p.destination, this.topicOwnership(), null); // the original is gone: every owner now counts
|
|
12506
|
+
}, { begin: (d) => this.topicDeletionsInFlight.add(d), end: (d) => this.topicDeletionsInFlight.delete(d) });
|
|
12507
|
+
}
|
|
12508
|
+
/** #3055: destinations whose platform deletion is running now; a binding to one waits until it is over. */
|
|
12509
|
+
topicDeletionsInFlight = new Set();
|
|
12510
|
+
/** True while a topic with this id is being deleted on any connection (a new binding of it is refused meanwhile). */
|
|
12511
|
+
isTopicDeletionInFlight(topicId) {
|
|
12512
|
+
const tail = `:${topicId}`;
|
|
12513
|
+
return [...this.topicDeletionsInFlight].some(d => d.endsWith(tail));
|
|
12514
|
+
}
|
|
12515
|
+
/**
|
|
12516
|
+
* #3055 review r2 (Prism P1-3): would this instance config bind a topic whose deletion is running now? Compared by
|
|
12517
|
+
* physical destination, so the same thread number in another Telegram group is not held. Settings asks it at commit.
|
|
12518
|
+
*/
|
|
12519
|
+
topicBindingInFlight(inst) {
|
|
12520
|
+
if (this.topicDeletionsInFlight.size === 0 || inst.topic_id == null || inst.topic_id === "")
|
|
12521
|
+
return false;
|
|
12522
|
+
const adapterId = typeof inst.channel_id === "string" && inst.channel_id ? inst.channel_id : this.getPrimaryAdapterId();
|
|
12523
|
+
return this.topicDeletionsInFlight.has(this.destinationFor(adapterId, inst.topic_id));
|
|
12524
|
+
}
|
|
12525
|
+
/** Any topic deletion running now (a connection's group or General may not be re-pointed meanwhile). */
|
|
12526
|
+
topicDeletionPending() { return this.topicDeletionsInFlight.size > 0; }
|
|
12527
|
+
/** The physical destination of `topicId` on the bot `adapterId`. */
|
|
12528
|
+
destinationFor(adapterId, topicId) {
|
|
12529
|
+
const world = adapterId ? this.worlds.get(adapterId) : undefined;
|
|
12530
|
+
const platform = world?.adapter?.type ?? this.getChannelConfig(adapterId)?.type;
|
|
12531
|
+
const groupId = world?.groupId ?? String(this.getChannelConfig(adapterId)?.group_id ?? "");
|
|
12532
|
+
return topicDestination(platform, groupId, adapterId, topicId);
|
|
12533
|
+
}
|
|
12534
|
+
/** The adapter of the bot `adapterId` names — never a substitute for a bot that is not there. */
|
|
12535
|
+
instanceTopicAdapter(adapterId) {
|
|
12536
|
+
return (adapterId ? this.worlds.get(adapterId)?.adapter : this.worlds.size === 0 ? this.adapter ?? undefined : undefined) ?? undefined;
|
|
12537
|
+
}
|
|
12538
|
+
/**
|
|
12539
|
+
* Topic ownership by physical destination (#3055 review, Prism P1): an instance's topic is where its bot's platform
|
|
12540
|
+
* puts it (a Telegram group + thread, a Discord channel), so two bots in one group, or two instances on one
|
|
12541
|
+
* channel, are seen to share it; and every connection's own General is protected, with or without a General agent.
|
|
12542
|
+
*/
|
|
12543
|
+
topicOwnership() {
|
|
12544
|
+
const channels = this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : []);
|
|
12545
|
+
const destinationOf = (n) => {
|
|
12546
|
+
const cfg = this.fleetConfig?.instances?.[n];
|
|
12547
|
+
if (!cfg || cfg.topic_id == null || cfg.topic_id === "")
|
|
12548
|
+
return null;
|
|
12549
|
+
return this.destinationFor(this.getInstanceAdapterId(n), cfg.topic_id);
|
|
12550
|
+
};
|
|
12551
|
+
const generalDestinations = () => {
|
|
12552
|
+
const out = new Set();
|
|
12553
|
+
for (const ch of channels) {
|
|
12554
|
+
const id = ch.id ?? ch.type;
|
|
12555
|
+
const groupId = this.worlds.get(id)?.groupId ?? (ch.group_id != null ? String(ch.group_id) : undefined);
|
|
12556
|
+
if (ch.type === "telegram")
|
|
12557
|
+
out.add(topicDestination("telegram", groupId, id, 1));
|
|
12558
|
+
if (ch.type === "discord") {
|
|
12559
|
+
const gcid = discordGeneralChannelId(ch);
|
|
12560
|
+
if (gcid)
|
|
12561
|
+
out.add(topicDestination("discord", groupId, id, gcid));
|
|
12562
|
+
}
|
|
12563
|
+
}
|
|
12564
|
+
return out;
|
|
12565
|
+
};
|
|
12566
|
+
return { instances: this.fleetConfig?.instances, destinationOf, generalDestinations };
|
|
12567
|
+
}
|
|
12568
|
+
/** What deleting `name`'s topic would act on: its own bot only — a bot that is not there is never substituted. */
|
|
12569
|
+
planInstanceTopicDelete(name) {
|
|
12570
|
+
const adapterId = this.getInstanceAdapterId(name);
|
|
12571
|
+
return planInstanceTopicDelete({ name, adapterId, adapter: this.instanceTopicAdapter(adapterId), ...this.topicOwnership() });
|
|
12262
12572
|
}
|
|
12263
12573
|
getForumTopicDeleter(adapterId) {
|
|
12264
12574
|
const adapter = (adapterId ? this.worlds.get(adapterId)?.adapter : undefined) ?? this.adapter;
|
|
@@ -12659,6 +12969,7 @@ export class FleetManager {
|
|
|
12659
12969
|
Object.assign(current, resolved);
|
|
12660
12970
|
}
|
|
12661
12971
|
this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
|
|
12972
|
+
this.agentCredentialsCache?.observePolicies();
|
|
12662
12973
|
this.refreshSavedDashboardPermissions();
|
|
12663
12974
|
this.logger.info({ path: this.configPath, strippedDefaults: redundantPaths.length }, "Saved fleet config (lossless patch)");
|
|
12664
12975
|
}
|
|
@@ -14976,6 +15287,7 @@ export class FleetManager {
|
|
|
14976
15287
|
const fleet = Object.entries(this.fleetConfig?.instances ?? {}).map(([name, cfg]) => ({
|
|
14977
15288
|
name, backend: this.backendNameOf(name), workingDirectory: cfg.working_directory, ledgerPath: join(this.getInstanceDir(name), "cache-ledger.json"),
|
|
14978
15289
|
claudeConfigDir: this.claudeConfigDirOf(name), profile: this.usageProfileOf(name, this.backendNameOf(name)),
|
|
15290
|
+
...(this.backendNameOf(name) === "codex" ? { profileTimelinePath: join(this.getInstanceDir(name), PROFILE_TIMELINE_FILE) } : {}),
|
|
14979
15291
|
kiroDbPath: this.kiroDbPathOf(name, this.backendNameOf(name)),
|
|
14980
15292
|
}));
|
|
14981
15293
|
const taken = new Set(fleet.map(i => i.name));
|
|
@@ -14987,6 +15299,9 @@ export class FleetManager {
|
|
|
14987
15299
|
claudeConfigDir: this.claudeConfigDirOf(ch.instanceName),
|
|
14988
15300
|
profile: this.usageProfileOf(ch.instanceName, this.classicChannels?.getBackendByInstance(ch.instanceName, this.fleetConfig?.defaults?.backend) ?? this.fleetConfig?.defaults?.backend ?? "claude-code"),
|
|
14989
15301
|
kiroDbPath: this.kiroDbPathOf(ch.instanceName, this.classicChannels?.getBackendByInstance(ch.instanceName, this.fleetConfig?.defaults?.backend) ?? this.fleetConfig?.defaults?.backend ?? "claude-code"),
|
|
15302
|
+
// #3066 §1d (Fable r1 #2): a ClassicBot Codex room records its launches too (onDaemonReady) — read them the same way.
|
|
15303
|
+
...((this.classicChannels?.getBackendByInstance(ch.instanceName, this.fleetConfig?.defaults?.backend) ?? this.fleetConfig?.defaults?.backend) === "codex"
|
|
15304
|
+
? { profileTimelinePath: join(this.getInstanceDir(ch.instanceName), PROFILE_TIMELINE_FILE) } : {}),
|
|
14990
15305
|
}));
|
|
14991
15306
|
return [...fleet, ...new Map(classic.map(c => [c.name, c])).values()];
|
|
14992
15307
|
}
|
|
@@ -15938,8 +16253,9 @@ export class FleetManager {
|
|
|
15938
16253
|
return r.ok ? { ok: true, until: Math.min(r.authorization.until, record.absoluteExpiry) } : { ok: false, refusal: r.refusal };
|
|
15939
16254
|
},
|
|
15940
16255
|
signOut: (handle) => {
|
|
15941
|
-
this.webSessions?.revokeByHandle(handle);
|
|
15942
|
-
this.sessionAuthorizationStore()?.prune(a => a.handle !== handle);
|
|
16256
|
+
const session = this.webSessions?.revokeByHandle(handle) ?? { found: false, durable: true };
|
|
16257
|
+
const authorization = this.sessionAuthorizationStore()?.prune(a => a.handle !== handle) ?? { dropped: 0, durable: true };
|
|
16258
|
+
return { durable: session.durable, authorizationDurable: authorization.durable };
|
|
15943
16259
|
},
|
|
15944
16260
|
event: (type, payload) => { try {
|
|
15945
16261
|
this.eventLog?.insert("web", type, payload);
|
|
@@ -15952,6 +16268,7 @@ export class FleetManager {
|
|
|
15952
16268
|
expired: (c) => t("web_auth.card_expired", c.label),
|
|
15953
16269
|
refused: (c) => t("web_auth.card_refused", c.label, t(`web_auth.refusal_${c.refusal}`)),
|
|
15954
16270
|
revoked: (c) => t("web_auth.card_revoked", c.label),
|
|
16271
|
+
notSaved: (c) => t("web_auth.card_not_saved", c.label),
|
|
15955
16272
|
ended: (c) => t("web_auth.card_ended", c.label),
|
|
15956
16273
|
generalHint: () => t("web_auth.general_hint"),
|
|
15957
16274
|
confirmLabel: () => t("web_auth.confirm"),
|
|
@@ -17018,14 +17335,16 @@ export class FleetManager {
|
|
|
17018
17335
|
* Make a tool summary safe to paste into a channel message.
|
|
17019
17336
|
*
|
|
17020
17337
|
* The text is agent-controlled (it is built from tool inputs — file paths,
|
|
17021
|
-
* shell commands), so it gets flattened to one line, capped,
|
|
17022
|
-
* the two Discord mass-mention triggers.
|
|
17023
|
-
*
|
|
17338
|
+
* shell commands), so it gets flattened to one line, secrets masked, capped,
|
|
17339
|
+
* and stripped of the two Discord mass-mention triggers. Masking comes BEFORE
|
|
17340
|
+
* the cap: a cap can cut a mask's context but must never be what hides (or
|
|
17341
|
+
* fails to hide) a secret. Neither channel renders it with a parse mode, so no
|
|
17342
|
+
* markup escaping is needed beyond that.
|
|
17024
17343
|
*/
|
|
17025
17344
|
static sanitizeActivity(activity) {
|
|
17026
17345
|
if (!activity)
|
|
17027
17346
|
return null;
|
|
17028
|
-
const flat = activity.replace(/\s+/g, " ").replace(/@(everyone|here)/g, "@$1").trim();
|
|
17347
|
+
const flat = maskSecrets(activity.replace(/\s+/g, " ")).replace(/@(everyone|here)/g, "@$1").trim();
|
|
17029
17348
|
if (!flat)
|
|
17030
17349
|
return null;
|
|
17031
17350
|
return flat.length > PROGRESS_ACTIVITY_MAX_CHARS
|
|
@@ -21621,6 +21940,15 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
21621
21940
|
clearInterval(this.discordPresenceTimer);
|
|
21622
21941
|
this.discordPresenceTimer = null;
|
|
21623
21942
|
}
|
|
21943
|
+
if (this.quotaReadTimer) {
|
|
21944
|
+
clearInterval(this.quotaReadTimer);
|
|
21945
|
+
this.quotaReadTimer = null;
|
|
21946
|
+
}
|
|
21947
|
+
if (this.capacityTimer) {
|
|
21948
|
+
clearInterval(this.capacityTimer);
|
|
21949
|
+
this.capacityTimer = null;
|
|
21950
|
+
}
|
|
21951
|
+
setCapacityLookup(null);
|
|
21624
21952
|
if (this.discordPresenceEagerTimer) {
|
|
21625
21953
|
clearTimeout(this.discordPresenceEagerTimer);
|
|
21626
21954
|
this.discordPresenceEagerTimer = null;
|
|
@@ -21925,6 +22253,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
21925
22253
|
// mark every row done and tell the user "changes applied".
|
|
21926
22254
|
return { rejected: why };
|
|
21927
22255
|
}
|
|
22256
|
+
this.agentCredentialsCache?.observePolicies();
|
|
21928
22257
|
if (this.loadedFleetSource)
|
|
21929
22258
|
this.getConfigHistory().accepted("fleet.yaml", this.configPath, this.loadedFleetSource);
|
|
21930
22259
|
this.migrateOfficeStorage();
|
|
@@ -22825,6 +23154,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
22825
23154
|
}
|
|
22826
23155
|
async runConnectionBindingApply(job, binding, execution) {
|
|
22827
23156
|
try {
|
|
23157
|
+
// #3055 review r2: a connection is not re-pointed while one of its topics is being deleted (nothing is changed).
|
|
23158
|
+
if (this.topicDeletionPending())
|
|
23159
|
+
throw new Error("a topic deletion is running; try again in a moment");
|
|
22828
23160
|
await this.rebuildAdapterForBinding(job.connectionId, binding, execution);
|
|
22829
23161
|
job.result = "applied";
|
|
22830
23162
|
}
|