@songsid/agend 2.3.0-beta.12 → 2.3.0-beta.14
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/backend/codex-metadata.d.ts +8 -0
- package/dist/backend/codex-metadata.js +74 -17
- package/dist/backend/codex-metadata.js.map +1 -1
- package/dist/config.d.ts +13 -1
- package/dist/config.js +14 -5
- package/dist/config.js.map +1 -1
- package/dist/fleet-manager.d.ts +36 -1
- package/dist/fleet-manager.js +135 -43
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/fleet-config/SKILL.md +2 -0
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +21 -3
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +23 -5
- 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.js +4 -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/topic-commands.d.ts +10 -0
- package/dist/topic-commands.js +27 -15
- package/dist/topic-commands.js.map +1 -1
- package/dist/ui/chat-effects.js +10 -3
- package/dist/ui/chat-strings.js +2 -0
- 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 +51 -24
- package/dist/ui/panel-fleet.js +3 -3
- 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/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 +38 -3
- package/dist/ui/shared/card-strings.js +4 -0
- package/dist/ui/shared/identity-card.js +26 -8
- package/dist/ui/shared/identity-color.js +11 -7
- package/dist/ui/shared/ui-icons.js +1 -0
- package/dist/ui/shared/ui-menu.js +20 -7
- package/dist/ui/shared/web-authorize.js +47 -0
- package/dist/web-api.d.ts +0 -3
- package/dist/web-api.js +80 -14
- 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 +3 -2
- package/dist/web-owner-css-store.js +9 -4
- 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 +34 -6
- 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.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);
|
|
@@ -73,7 +74,7 @@ import { routeToolCall } from "./channel/tool-router.js";
|
|
|
73
74
|
import { Scheduler } from "./scheduler/index.js";
|
|
74
75
|
import { escapeTelegramHtml, scheduleClock, scheduleRetryLabel } from "./scheduler/retry-label.js";
|
|
75
76
|
import { DEFAULT_SCHEDULER_CONFIG } from "./scheduler/index.js";
|
|
76
|
-
import { TopicCommands, saveCommandForBackend, parseSaveFilename, parsePauseWakeCommand, parseCompactCommand, SAVE_FILENAME_RE, resolveInstanceContext, forgetInstanceContext,
|
|
77
|
+
import { TopicCommands, saveCommandForBackend, parseSaveFilename, parsePauseWakeCommand, parseCompactCommand, SAVE_FILENAME_RE, resolveInstanceContext, forgetInstanceContext, readStatuslineJson, statuslineModel } from "./topic-commands.js";
|
|
77
78
|
import { DailySummary } from "./daily-summary.js";
|
|
78
79
|
import { WebhookEmitter } from "./webhook-emitter.js";
|
|
79
80
|
import { TmuxControlClient } from "./tmux-control.js";
|
|
@@ -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();
|
|
@@ -1284,6 +1287,7 @@ export class FleetManager {
|
|
|
1284
1287
|
current: () => current() && sessions === this.webSessions && !!this.webToken
|
|
1285
1288
|
&& sessions.isCurrent(record, tokenEpoch(this.webToken)) && (!exposure || exposure.isCurrent()) };
|
|
1286
1289
|
}, baseline: () => baselines.read(), snapshot: () => baselines.snapshot(),
|
|
1290
|
+
selfConfirm: (req, id) => this.settingsSelfConfirmation(store, req, id),
|
|
1287
1291
|
job: async (id) => {
|
|
1288
1292
|
const settlement = this.settingsJobSettlements.get(id);
|
|
1289
1293
|
if (!settlement)
|
|
@@ -1298,6 +1302,86 @@ export class FleetManager {
|
|
|
1298
1302
|
this.settingsConfirmation = gate;
|
|
1299
1303
|
return gate;
|
|
1300
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
|
+
}
|
|
1301
1385
|
/** Exact configured effect owners; this does not depend on prompt location. */
|
|
1302
1386
|
settingsChatAuthority(store, id) {
|
|
1303
1387
|
const scope = store.authorityOf(id);
|
|
@@ -5298,34 +5382,18 @@ export class FleetManager {
|
|
|
5298
5382
|
return this.adapterState.get(adapterId)?.status === "connected" ? adapter : undefined;
|
|
5299
5383
|
}
|
|
5300
5384
|
/** Last-resort completion after RestartProgress could not deliver to its
|
|
5301
|
-
* 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. */
|
|
5302
5389
|
async sendFleetStartCompletionFallback(chatId, text, threadId) {
|
|
5303
5390
|
const adapterId = this.getPrimaryAdapterId();
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
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");
|
|
5307
5393
|
return false;
|
|
5308
5394
|
}
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
timer = setTimeout(() => resolve({ status: "timeout" }), RESTART_PROGRESS_TERMINAL_TIMEOUT_MS);
|
|
5312
|
-
timer.unref?.();
|
|
5313
|
-
});
|
|
5314
|
-
const delivery = Promise.resolve()
|
|
5315
|
-
.then(() => adapter.sendText(chatId, text, { threadId }))
|
|
5316
|
-
.then(() => ({ status: "sent" }), err => ({ status: "failed", err }));
|
|
5317
|
-
const result = await Promise.race([delivery, timeout]);
|
|
5318
|
-
if (timer)
|
|
5319
|
-
clearTimeout(timer);
|
|
5320
|
-
if (result.status === "sent")
|
|
5321
|
-
return true;
|
|
5322
|
-
if (result.status === "failed") {
|
|
5323
|
-
this.logger.error({ err: result.err }, "Failed to send fleet start completion fallback");
|
|
5324
|
-
}
|
|
5325
|
-
else {
|
|
5326
|
-
this.logger.error({ timeout_ms: RESTART_PROGRESS_TERMINAL_TIMEOUT_MS }, "Timed out sending fleet start completion fallback");
|
|
5327
|
-
}
|
|
5328
|
-
return false;
|
|
5395
|
+
const poster = new RestartProgress(0, Date.now(), this.logger);
|
|
5396
|
+
return poster.postCompletion({ resolveAdapter: () => this.readyProgressAdapter(adapterId), chatId, threadId }, text);
|
|
5329
5397
|
}
|
|
5330
5398
|
async stopInstance(name, transition) {
|
|
5331
5399
|
this.explicitStopGeneration.set(name, (this.explicitStopGeneration.get(name) ?? 0) + 1);
|
|
@@ -11081,6 +11149,10 @@ export class FleetManager {
|
|
|
11081
11149
|
throw new Error(`Refusing to overwrite invalid fleet.yaml: ${this.rawFleetDocument.errors[0].message}`);
|
|
11082
11150
|
}
|
|
11083
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
|
|
11084
11156
|
this.patchFleetDocument(this.rawFleetDocument, [], this.savedFleetConfigSnapshot, this.fleetConfig);
|
|
11085
11157
|
// Settings edits are expressed against the raw config, so apply them before
|
|
11086
11158
|
// canonicalization. A non-identity value equal to its inherited default is
|
|
@@ -11091,6 +11163,7 @@ export class FleetManager {
|
|
|
11091
11163
|
// (or a legacy scalar occupies that parent). Removing an override which
|
|
11092
11164
|
// is already absent is an idempotent no-op, not a failed Settings save.
|
|
11093
11165
|
if (this.rawFleetDocument.hasIn(patch.path)) {
|
|
11166
|
+
this.writingFleetDocument(this.rawFleetDocument, patch.path);
|
|
11094
11167
|
this.rawFleetDocument.deleteIn(patch.path);
|
|
11095
11168
|
}
|
|
11096
11169
|
}
|
|
@@ -11102,8 +11175,10 @@ export class FleetManager {
|
|
|
11102
11175
|
const rawAfterPatches = this.rawFleetDocument.toJS();
|
|
11103
11176
|
const redundantPaths = collectRedundantInstanceDefaultPaths(rawAfterPatches);
|
|
11104
11177
|
for (const path of redundantPaths) {
|
|
11105
|
-
if (this.rawFleetDocument.hasIn(path))
|
|
11178
|
+
if (this.rawFleetDocument.hasIn(path)) {
|
|
11179
|
+
this.writingFleetDocument(this.rawFleetDocument, path);
|
|
11106
11180
|
this.rawFleetDocument.deleteIn(path);
|
|
11181
|
+
}
|
|
11107
11182
|
// Avoid leaving empty operational maps such as `terminal: {}` while
|
|
11108
11183
|
// preserving the instance mapping itself and all surrounding comments.
|
|
11109
11184
|
for (let depth = path.length - 1; depth > 2; depth--) {
|
|
@@ -11111,9 +11186,14 @@ export class FleetManager {
|
|
|
11111
11186
|
const parent = this.rawFleetDocument.getIn(parentPath, true);
|
|
11112
11187
|
if (!isMap(parent) || parent.items.length > 0)
|
|
11113
11188
|
break;
|
|
11189
|
+
this.writingFleetDocument(this.rawFleetDocument, parentPath);
|
|
11114
11190
|
this.rawFleetDocument.deleteIn(parentPath);
|
|
11115
11191
|
}
|
|
11116
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.`);
|
|
11117
11197
|
noteSettingsWrite(this.configPath, this.savedFleetConfigSnapshot, this.fleetConfig);
|
|
11118
11198
|
const output = String(this.rawFleetDocument);
|
|
11119
11199
|
if (redundantPaths.length > 0)
|
|
@@ -11224,11 +11304,15 @@ export class FleetManager {
|
|
|
11224
11304
|
// Remove from the end so YAML sequence indexes do not shift underneath us.
|
|
11225
11305
|
for (let i = before.length - 1; i >= after.length; i--) {
|
|
11226
11306
|
const itemPath = [...path, i];
|
|
11227
|
-
if (document.hasIn(itemPath))
|
|
11307
|
+
if (document.hasIn(itemPath)) {
|
|
11308
|
+
this.writingFleetDocument(document, itemPath);
|
|
11228
11309
|
document.deleteIn(itemPath);
|
|
11310
|
+
}
|
|
11229
11311
|
}
|
|
11230
|
-
for (let i = shared; i < after.length; i++)
|
|
11312
|
+
for (let i = shared; i < after.length; i++) {
|
|
11313
|
+
this.writingFleetDocument(document, [...path, i]);
|
|
11231
11314
|
document.setIn([...path, i], after[i]);
|
|
11315
|
+
}
|
|
11232
11316
|
return;
|
|
11233
11317
|
}
|
|
11234
11318
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -11244,6 +11328,7 @@ export class FleetManager {
|
|
|
11244
11328
|
if (isMap(fresh)) {
|
|
11245
11329
|
const list = document.createNode([]);
|
|
11246
11330
|
list.items.push(fresh.clone());
|
|
11331
|
+
this.writingFleetDocument(document, ["channels"]);
|
|
11247
11332
|
document.setIn(["channels"], list);
|
|
11248
11333
|
}
|
|
11249
11334
|
}
|
|
@@ -11265,13 +11350,17 @@ export class FleetManager {
|
|
|
11265
11350
|
// Effective config contains inherited objects that may not exist in the
|
|
11266
11351
|
// raw YAML at all. yaml.deleteIn() is not idempotent for a missing nested
|
|
11267
11352
|
// parent, so guard it explicitly.
|
|
11268
|
-
if (document.hasIn(path))
|
|
11353
|
+
if (document.hasIn(path)) {
|
|
11354
|
+
this.writingFleetDocument(document, path);
|
|
11269
11355
|
document.deleteIn(path);
|
|
11356
|
+
}
|
|
11270
11357
|
}
|
|
11271
11358
|
else if (path.length === 0) {
|
|
11359
|
+
this.writingFleetDocument(document, path);
|
|
11272
11360
|
document.contents = document.createNode(after);
|
|
11273
11361
|
}
|
|
11274
11362
|
else {
|
|
11363
|
+
this.writingFleetDocument(document, path);
|
|
11275
11364
|
const currentNode = document.getIn(path, true);
|
|
11276
11365
|
if (isScalar(currentNode) && (after === null || typeof after !== "object")) {
|
|
11277
11366
|
currentNode.value = after;
|
|
@@ -11286,6 +11375,15 @@ export class FleetManager {
|
|
|
11286
11375
|
}
|
|
11287
11376
|
}
|
|
11288
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
|
+
}
|
|
11289
11387
|
getSettingsConfigPath() { return this.configPath; }
|
|
11290
11388
|
async removeInstance(name, authorization, execution) {
|
|
11291
11389
|
if (execution)
|
|
@@ -21732,15 +21830,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
21732
21830
|
this.logger.debug({ err }, "bots unreadable (getUiStatus)");
|
|
21733
21831
|
}
|
|
21734
21832
|
const instances = names.map(name => {
|
|
21735
|
-
|
|
21736
|
-
|
|
21737
|
-
|
|
21738
|
-
const data = JSON.parse(readFileSync(statusFile, "utf-8"));
|
|
21739
|
-
cost = data.cost?.total_cost_usd ?? 0;
|
|
21740
|
-
}
|
|
21741
|
-
catch (err) {
|
|
21742
|
-
this.logger.debug({ err, name }, "statusline.json read failed (getUiStatus)");
|
|
21743
|
-
}
|
|
21833
|
+
// #1956: read once for this instance; its cost, context and model come from it.
|
|
21834
|
+
const statusline = readStatuslineJson(this.dataDir, name);
|
|
21835
|
+
const cost = statusline?.cost?.total_cost_usd ?? 0;
|
|
21744
21836
|
// Align with /ctx: statusline for claude-code, pane scrape for kiro/grok/codex.
|
|
21745
21837
|
const classic = classicOnly.includes(name);
|
|
21746
21838
|
const backend = classic
|
|
@@ -21748,14 +21840,14 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
21748
21840
|
: (this.fleetConfig?.instances[name]?.backend
|
|
21749
21841
|
?? this.fleetConfig?.defaults?.backend
|
|
21750
21842
|
?? "claude-code");
|
|
21751
|
-
const { context } = resolveInstanceContext(this.dataDir, name, backend, { source: this.getPaneContextSource(name) });
|
|
21843
|
+
const { context } = resolveInstanceContext(this.dataDir, name, backend, { source: this.getPaneContextSource(name), statusline });
|
|
21752
21844
|
// context_pct: null when unavailable, not 0
|
|
21753
21845
|
const context_pct = context ?? null;
|
|
21754
21846
|
// Model: Only Claude Code has live statusline; others use the effective resolver.
|
|
21755
|
-
//
|
|
21847
|
+
// statuslineModel gives the /ctx-aligned display_name+id combo for Claude (readStatuslineModel's rule).
|
|
21756
21848
|
// Non-Claude backends must NOT read statusline.json model (may be stale from previous Claude run).
|
|
21757
21849
|
const resolved = this.resolveInstanceModel(name);
|
|
21758
|
-
const liveModel = backend === "claude-code" ?
|
|
21850
|
+
const liveModel = backend === "claude-code" ? statuslineModel(statusline) : null;
|
|
21759
21851
|
// Display value: live model for Claude, resolved.display for others (includes "auto (default)" for Kiro)
|
|
21760
21852
|
const model = liveModel ?? resolved.display;
|
|
21761
21853
|
// model_source: "live" when Claude statusline succeeded, else the resolver's source
|