@songsid/agend 2.3.0-beta.7 → 2.3.0-beta.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/channel/mcp-tools.js +1 -1
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/daemon.d.ts +2 -0
- package/dist/daemon.js +29 -9
- package/dist/daemon.js.map +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +13 -10
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +15 -8
- package/dist/instance-config-impact.js +1 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/outbound-handlers.js +4 -1
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.js +1 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/settings-api.js +4 -0
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +4 -0
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +2 -0
- package/dist/settings-effect.js.map +1 -1
- package/dist/ui/app-effects.js +5 -5
- package/dist/ui/chat-ambient.js +8 -7
- package/dist/ui/chat-effect-posters.js +3 -2
- package/dist/ui/chat-effects.js +19 -13
- package/dist/ui/chat-strings.js +4 -2
- package/dist/ui/chat-thread.js +1 -1
- package/dist/ui/fleet-usage.js +401 -0
- package/dist/ui/panel-chat.js +9 -4
- package/dist/ui/panel-fleet.js +6 -5
- package/dist/ui/panel-settings.js +6 -1
- package/dist/ui/settings-owner-effects.js +1 -1
- package/dist/ui/settings-strings.js +2 -0
- package/dist/ui/shared/app-route.js +1 -1
- package/dist/ui/shared/app.css +67 -0
- package/dist/ui/shared/effect-app-runtime.js +3 -3
- package/dist/ui/shared/effect-chat-runtime.js +1 -1
- package/dist/ui/shared/effect-policy.js +3 -4
- package/dist/ui/shared/effect-profiler.js +31 -9
- package/dist/ui/shared/effect-registry.js +9 -26
- package/dist/ui/shared/effect-stock-trust.js +2 -2
- package/dist/ui/shared/fx-motion.js +12 -3
- package/dist/ui/shared/usage-dialog.js +8 -0
- package/dist/ui/shared/view-strings.js +4 -0
- package/dist/usage-report.d.ts +12 -1
- package/dist/usage-report.js +39 -8
- package/dist/usage-report.js.map +1 -1
- package/dist/web-api.d.ts +1 -0
- package/dist/web-api.js +7 -2
- package/dist/web-api.js.map +1 -1
- package/dist/web-settings-patch.d.ts +3 -1
- package/dist/web-settings-patch.js +4 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-shell-routes.d.ts +1 -1
- package/dist/web-shell-routes.js +1 -1
- package/dist/web-shell-routes.js.map +1 -1
- package/package.json +1 -1
package/dist/ui/shared/app.css
CHANGED
|
@@ -1341,6 +1341,73 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
|
|
|
1341
1341
|
.ch-bar .btn { min-height: var(--tap); }
|
|
1342
1342
|
}
|
|
1343
1343
|
|
|
1344
|
+
/* ── #2122: Fleet → Usage. A categorical palette kept apart from the state colours (Okabe–Ito hues, tuned per mode for
|
|
1345
|
+
contrast); every series also has a text label, so colour is never the only cue. ── */
|
|
1346
|
+
.us { --us-0: #56b4e9; --us-1: #e69f00; --us-2: #2bc48a; --us-3: #f0e442; --us-4: #e78ac3; --us-5: #94a3b8; --us-heat: #56b4e9;
|
|
1347
|
+
display: flex; flex-direction: column; gap: var(--s-3); min-width: 0; }
|
|
1348
|
+
@media (prefers-color-scheme: light) { :root:not([data-theme="dark"]) .us { --us-0: #0072b2; --us-1: #c25400; --us-2: #007a5a; --us-3: #8a6d00; --us-4: #b0457a; --us-5: #64748b; --us-heat: #0072b2; } }
|
|
1349
|
+
:root[data-theme="light"] .us { --us-0: #0072b2; --us-1: #c25400; --us-2: #007a5a; --us-3: #8a6d00; --us-4: #b0457a; --us-5: #64748b; --us-heat: #0072b2; }
|
|
1350
|
+
.us-bar-top { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2) var(--s-3); margin-top: var(--s-2); }
|
|
1351
|
+
.us-group { display: inline-flex; align-items: center; gap: var(--s-2); font-size: var(--fs-sm); color: var(--text-2); }
|
|
1352
|
+
.us-group select, .us-custom input { min-height: 32px; padding: 0 var(--s-2); border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--bg); color: var(--text); }
|
|
1353
|
+
.us-group select:focus, .us-custom input:focus { border-color: var(--focus); box-shadow: 0 0 0 3px var(--accent-dim); outline: none; }
|
|
1354
|
+
.us-csv { margin-left: auto; }
|
|
1355
|
+
.us-custom { display: flex; flex-wrap: wrap; align-items: end; gap: var(--s-2) var(--s-3); font-size: var(--fs-sm); }
|
|
1356
|
+
.us-custom label { display: inline-flex; flex-direction: column; gap: 2px; color: var(--text-2); }
|
|
1357
|
+
.us-basis { font-size: var(--fs-sm); color: var(--text); padding: var(--s-2) var(--s-3); border-left: 3px solid var(--accent); background: var(--surface-2); border-radius: var(--r-sm); }
|
|
1358
|
+
.us-fresh { font-size: var(--fs-xs); }
|
|
1359
|
+
.us-partial { font-size: var(--fs-sm); padding: var(--s-2) var(--s-3); border-left: 3px solid var(--warn); background: var(--warn-bg); border-radius: var(--r-sm); }
|
|
1360
|
+
.us-unpriced { font-size: var(--fs-xs); }
|
|
1361
|
+
.us-scan { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); }
|
|
1362
|
+
.us-totals { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--s-3); margin: 0; }
|
|
1363
|
+
.us-totals .ch-metric dd { font-size: var(--fs-lg); }
|
|
1364
|
+
.us-table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); font-variant-numeric: tabular-nums; }
|
|
1365
|
+
.us-table th, .us-table td { padding: var(--s-2); border-bottom: 1px solid var(--border); text-align: left; vertical-align: top; }
|
|
1366
|
+
.us-table thead th { font-size: 11px; font-weight: var(--fw-regular); color: var(--text-3); }
|
|
1367
|
+
.us-table .num { text-align: right; white-space: nowrap; }
|
|
1368
|
+
.us-name { min-width: 0; max-width: 360px; font-weight: var(--fw-regular); }
|
|
1369
|
+
.us-name .mono { display: block; font-weight: var(--fw-semibold); overflow-wrap: anywhere; }
|
|
1370
|
+
.us-bar { display: block; width: 100%; height: 6px; margin: 4px 0; }
|
|
1371
|
+
.us-bar-bg { fill: var(--surface-2); } .us-bar-fg { fill: var(--us-0); }
|
|
1372
|
+
.us-meta { display: block; font-size: var(--fs-xs); color: var(--text-3); overflow-wrap: anywhere; }
|
|
1373
|
+
.us-flags { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }
|
|
1374
|
+
.us-trend, .us-heat { display: flex; flex-direction: column; gap: var(--s-2); min-width: 0; }
|
|
1375
|
+
.us-trend h3, .us-heat h3, .us-rest h3 { font-size: var(--fs-sm); font-weight: var(--fw-semibold); }
|
|
1376
|
+
.us-scroll { overflow-x: auto; max-width: 100%; }
|
|
1377
|
+
.us-chart, .us-heatmap { display: block; }
|
|
1378
|
+
.us-axis { font-size: 9px; fill: var(--text-3); }
|
|
1379
|
+
.us-s0 { fill: var(--us-0); background: var(--us-0); } .us-s1 { fill: var(--us-1); background: var(--us-1); } .us-s2 { fill: var(--us-2); background: var(--us-2); }
|
|
1380
|
+
.us-s3 { fill: var(--us-3); background: var(--us-3); } .us-s4 { fill: var(--us-4); background: var(--us-4); } .us-s5 { fill: var(--us-5); background: var(--us-5); }
|
|
1381
|
+
.us-legend { display: flex; flex-wrap: wrap; gap: var(--s-1) var(--s-4); list-style: none; padding: 0; margin: 0; font-size: var(--fs-xs); }
|
|
1382
|
+
.us-legend li { display: inline-flex; align-items: center; gap: var(--s-1); min-width: 0; overflow-wrap: anywhere; }
|
|
1383
|
+
.us-sw, .us-h-sw { display: inline-block; width: 10px; height: 10px; border-radius: 2px; flex-shrink: 0; }
|
|
1384
|
+
.us-h { fill: var(--us-heat); }
|
|
1385
|
+
.us-h.l1, .us-h-sw.l1 { opacity: 0.45; } .us-h.l2 { opacity: 0.65; } .us-h.l3 { opacity: 0.85; } .us-h.l4, .us-h-sw.l4 { opacity: 1; }
|
|
1386
|
+
.us-h-sw.l4 { background: var(--us-heat); }
|
|
1387
|
+
/* Read with nothing used: a solid grey cell. Not collected: an empty dashed outline. */
|
|
1388
|
+
.us-h.idle { fill: var(--text-3); opacity: 0.35; }
|
|
1389
|
+
.us-h-sw.idle { background: var(--text-3); opacity: 0.35; }
|
|
1390
|
+
.us-h.gap { fill: none; stroke: var(--text-3); stroke-width: 1; stroke-dasharray: 2 2; }
|
|
1391
|
+
.us-h-sw.gap { border: 1px dashed var(--text-3); }
|
|
1392
|
+
.us-grid, .us-base { stroke: var(--border); stroke-width: 1; }
|
|
1393
|
+
.us-grid { stroke-dasharray: 3 3; }
|
|
1394
|
+
.us-scale, .us-earlier { font-size: var(--fs-xs); }
|
|
1395
|
+
.us-heat-key { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-1); font-size: var(--fs-xs); }
|
|
1396
|
+
.us-rest .row-item { font-size: var(--fs-xs); }
|
|
1397
|
+
.u-by-agent { margin-left: auto; }
|
|
1398
|
+
@media (max-width: 699px) {
|
|
1399
|
+
.us-totals { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
1400
|
+
.us-csv { margin-left: 0; }
|
|
1401
|
+
.us-bar-top .btn { min-height: var(--tap); }
|
|
1402
|
+
/* Rows become cards: each value with its label. */
|
|
1403
|
+
.us-table thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
|
|
1404
|
+
.us-table, .us-table tbody, .us-table tr, .us-table th, .us-table td { display: block; width: 100%; }
|
|
1405
|
+
.us-table tr { margin-bottom: var(--s-3); padding: var(--s-3); border: 1px solid var(--border); border-radius: var(--r-lg); background: var(--bg); }
|
|
1406
|
+
.us-table th, .us-table td { border: 0; padding: 2px 0; max-width: none; }
|
|
1407
|
+
.us-table td { display: flex; justify-content: space-between; gap: var(--s-3); }
|
|
1408
|
+
.us-table td::before { content: attr(data-label); color: var(--text-3); text-align: left; }
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1344
1411
|
/* ── Chat commands and quick actions (#1269) ── */
|
|
1345
1412
|
.hd-chip { margin-left: var(--s-2); padding: 0 var(--s-2); border: 1px solid var(--border); border-radius: var(--r-full); background: transparent; color: var(--text-2);
|
|
1346
1413
|
font-size: var(--fs-xs); line-height: 20px; cursor: pointer; max-width: 16em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: middle; }
|
|
@@ -31,7 +31,7 @@ export function createAppEffectRuntime({ window: host, current, observe, onChang
|
|
|
31
31
|
const effects = s?.metadata?.effects ?? [], known = registry?.snapshot?.() ?? [];
|
|
32
32
|
const rows = effects.map(p => {
|
|
33
33
|
const b = s.bindings.get(p.id), row = known.find(r => b && r.key === b.key && r.id === p.id);
|
|
34
|
-
if (row && !s.error && !playbackUnavailable) return row.reason == null && row.phase == null ? { ...row,
|
|
34
|
+
if (row && !s.error && !playbackUnavailable) return row.reason == null && row.phase == null ? { ...row, phase: "loading" } : row;
|
|
35
35
|
return { id: p.id, key: b?.key ?? null, verdict: "off", phase: null, quality: null,
|
|
36
36
|
reason: s.error ?? playbackUnavailable ?? effectPlaybackReason(lastContext) ?? (!p.manifest.regions.includes("chat") ? "region_unavailable" : "runtime_unavailable") };
|
|
37
37
|
});
|
|
@@ -141,7 +141,7 @@ export function createAppEffectRuntime({ window: host, current, observe, onChang
|
|
|
141
141
|
if (identity.scope === "owner") trial = make("trialSource", createEffectTrialSource)({ window: host, current: () => {
|
|
142
142
|
if (!alive()) return false; const v = observe(), c = v.context;
|
|
143
143
|
return alive() && c.enabled === true && c.show === true && c.transportReady === true
|
|
144
|
-
&& c.safe === false && c.publicLink === false && c.reducedMotion === false && c.
|
|
144
|
+
&& c.safe === false && c.publicLink === false && c.reducedMotion === false && c.hidden === false;
|
|
145
145
|
}, ownerCurrent: (s, name) => sourceAlive(s) && s.name === name,
|
|
146
146
|
stock: () => registry.frameRoot ? make("stock", stockEffectTrust)(host, { frameRoot: registry.frameRoot, ownerCss: observe().ownerCss }) : null,
|
|
147
147
|
readResource: (spec, signal) => typeof loader.trialResourceSettled === "function" ? loader.trialResourceSettled(spec, signal) : Promise.resolve(null),
|
|
@@ -210,7 +210,7 @@ export function createAppEffectRuntime({ window: host, current, observe, onChang
|
|
|
210
210
|
// Metadata describes placements, not executable resources. Its existing session/epoch/current and
|
|
211
211
|
// shared settled-lane fences apply even when this viewer has declined playback. No code is fetched here.
|
|
212
212
|
if (!pending) {
|
|
213
|
-
const ownerNeeds = identity.scope === "owner" && !c.publicLink && c.enabled && c.show && c.transportReady && !c.safe && !c.reducedMotion && !c.
|
|
213
|
+
const ownerNeeds = identity.scope === "owner" && !c.publicLink && c.enabled && c.show && c.transportReady && !c.safe && !c.reducedMotion && !c.hidden && personal?.needsMetadata() === true, chatNeeds = sourceAlive(s) && !s.error && !s.metadata;
|
|
214
214
|
if (!alive() || revision !== before || s !== source) { if (alive()) queueSync(); return; }
|
|
215
215
|
if (ownerNeeds && (!chatNeeds || lastMetadata !== "owner")) void readPersonal();
|
|
216
216
|
else if (chatNeeds) void read(s);
|
|
@@ -138,7 +138,7 @@ export function createChatEffectRuntime({ window: host, name, element, current,
|
|
|
138
138
|
const stamp = JSON.stringify(ctx);
|
|
139
139
|
if (stamp !== lastContext) { lastContext = stamp; registry.update(descriptors(), ctx); }
|
|
140
140
|
emit();
|
|
141
|
-
if (!metadata && !active && ctx.enabled && ctx.show && ctx.transportReady && !ctx.safe && !ctx.publicLink && !ctx.reducedMotion && !ctx.
|
|
141
|
+
if (!metadata && !active && ctx.enabled && ctx.show && ctx.transportReady && !ctx.safe && !ctx.publicLink && !ctx.reducedMotion && !ctx.hidden) void read();
|
|
142
142
|
}
|
|
143
143
|
async function read() {
|
|
144
144
|
if (!alive() || error || active) return;
|
|
@@ -66,8 +66,8 @@ export const EFFECT_BLOCK_REASONS = Object.freeze([
|
|
|
66
66
|
|
|
67
67
|
/** Diagnostic names only; the admission policy below still validates every required flag. */
|
|
68
68
|
export function effectPlaybackReason(c) {
|
|
69
|
-
if (!c || ["safe", "publicLink", "reducedMotion", "
|
|
70
|
-
return c.safe ? "safe_mode" : c.reducedMotion ? "reduced_motion"
|
|
69
|
+
if (!c || ["safe", "publicLink", "reducedMotion", "hidden", "enabled", "show", "transportReady"].some(k => typeof c[k] !== "boolean")) return "gates_unknown";
|
|
70
|
+
return c.safe ? "safe_mode" : c.reducedMotion ? "reduced_motion"
|
|
71
71
|
: c.hidden ? "hidden" : !c.enabled ? "unsupported_host" : !c.show ? "disabled" : !c.transportReady ? "transport_unavailable" : null;
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -78,12 +78,11 @@ export function allocateEffectFrames(descriptors, context) {
|
|
|
78
78
|
const keys = new Set(), duplicate = new Set();
|
|
79
79
|
for (const d of descriptors) if (d && typeof d.key === "string") { if (keys.has(d.key)) duplicate.add(d.key); keys.add(d.key); }
|
|
80
80
|
const result = descriptors.map(d => ({ key: d?.key, verdict: "off", reason: "unknown", queuePos: null }));
|
|
81
|
-
const gates = ["safe", "publicLink", "reducedMotion", "
|
|
81
|
+
const gates = ["safe", "publicLink", "reducedMotion", "hidden", "enabled", "show", "transportReady", "constrained", "modal"];
|
|
82
82
|
if (gates.some(k => typeof context[k] !== "boolean") || context.openInstance !== null && !instanceName(context.openInstance)) return result;
|
|
83
83
|
let global = null;
|
|
84
84
|
if (context.safe === true) global = "safe_mode";
|
|
85
85
|
else if (context.reducedMotion === true) global = "reduced_motion";
|
|
86
|
-
else if (context.saveData === true) global = "save_data";
|
|
87
86
|
else if (context.hidden === true) global = "hidden";
|
|
88
87
|
else if (context.enabled !== true) global = "unsupported_host";
|
|
89
88
|
else if (context.show !== true) global = "disabled";
|
|
@@ -12,10 +12,14 @@ const id = v => typeof v === "string" && /^[0-9a-f]{32}$/.test(v);
|
|
|
12
12
|
/** One host-owned mount. The registry must remove the frame when remove is true, never merely hide it. */
|
|
13
13
|
export class EffectBudget {
|
|
14
14
|
constructor({ device, baselineFps, clock = () => performance.now(), longTasksSupported = false } = {}) {
|
|
15
|
-
if (!deviceClass(device) || !finite(baselineFps) || baselineFps <= 0 || baselineFps > 1000 || typeof clock !== "function" || typeof longTasksSupported !== "boolean") throw new TypeError("effect budget options");
|
|
15
|
+
if (!deviceClass(device) || baselineFps !== undefined && (!finite(baselineFps) || baselineFps <= 0 || baselineFps > 1000) || typeof clock !== "function" || typeof longTasksSupported !== "boolean") throw new TypeError("effect budget options");
|
|
16
16
|
this.device = device;
|
|
17
|
-
//
|
|
18
|
-
|
|
17
|
+
// Legacy host measurements may still be supplied by pure probes. Product playback starts
|
|
18
|
+
// immediately and learns cadence concurrently from host rAF, never Worker timing.
|
|
19
|
+
// Freeze before the first report: a mount's profile floor must remain coherent.
|
|
20
|
+
this.fpsFloor = baselineFps === undefined ? null : Math.min(EFFECT_BUDGETS[device].fps, baselineFps * 0.9);
|
|
21
|
+
this.cadence = [];
|
|
22
|
+
this.cadenceAt = null;
|
|
19
23
|
this.clock = clock;
|
|
20
24
|
this.longTasksSupported = longTasksSupported;
|
|
21
25
|
this.last = null;
|
|
@@ -58,7 +62,16 @@ export class EffectBudget {
|
|
|
58
62
|
frame() {
|
|
59
63
|
const now = this.time();
|
|
60
64
|
if (now === null) return this.stop("clock_unavailable");
|
|
61
|
-
if (this.verdict === "paused" || this.hiddenAt !== null
|
|
65
|
+
if (this.verdict === "paused" || this.hiddenAt !== null) return this.snapshot();
|
|
66
|
+
if (this.fpsFloor === null) {
|
|
67
|
+
const interval = this.cadenceAt === null ? null : now - this.cadenceAt;
|
|
68
|
+
this.cadenceAt = now;
|
|
69
|
+
if (interval !== null && interval >= 1 && interval <= 1000) {
|
|
70
|
+
if (this.cadence.length === 256) this.cadence.shift();
|
|
71
|
+
this.cadence.push(interval);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (now < this.epoch + 1000) return this.snapshot();
|
|
62
75
|
this.trim(now);
|
|
63
76
|
if (this.frames.at(-1) !== now) {
|
|
64
77
|
if (this.frames.length >= 2048) this.overflow = true;
|
|
@@ -88,10 +101,10 @@ export class EffectBudget {
|
|
|
88
101
|
const now = this.time();
|
|
89
102
|
if (now === null) return this.stop("clock_unavailable");
|
|
90
103
|
if (!plain(raw)) return this.stop("gates_unknown");
|
|
91
|
-
const { hidden, reducedMotion,
|
|
92
|
-
if ([hidden, reducedMotion,
|
|
104
|
+
const { hidden, reducedMotion, show } = raw;
|
|
105
|
+
if ([hidden, reducedMotion, show].some(v => typeof v !== "boolean")) return this.stop("gates_unknown");
|
|
93
106
|
if (this.verdict === "paused") return this.snapshot();
|
|
94
|
-
if (reducedMotion ||
|
|
107
|
+
if (reducedMotion || !show) return this.stop(reducedMotion ? "reduced_motion" : "viewer_off");
|
|
95
108
|
if (hidden && this.hiddenAt === null) this.hiddenAt = now;
|
|
96
109
|
if (hidden && now - this.hiddenAt >= 10000) return this.stop("hidden");
|
|
97
110
|
if (!hidden && this.hiddenAt !== null) {
|
|
@@ -105,6 +118,7 @@ export class EffectBudget {
|
|
|
105
118
|
this.overflow = false;
|
|
106
119
|
this.badWindows = 0;
|
|
107
120
|
this.metrics = null;
|
|
121
|
+
this.cadence = []; this.cadenceAt = null;
|
|
108
122
|
}
|
|
109
123
|
return this.snapshot();
|
|
110
124
|
}
|
|
@@ -123,8 +137,16 @@ export class EffectBudget {
|
|
|
123
137
|
const hostMs = this.longTasksSupported ? sum(null) : null;
|
|
124
138
|
const ownMs = this.longTasksSupported && this.tasks.some(t => t.attributed) ? sum(true) : null;
|
|
125
139
|
const limits = EFFECT_BUDGETS[this.device];
|
|
126
|
-
|
|
127
|
-
|
|
140
|
+
if (this.fpsFloor === null && this.cadence.length >= 3) {
|
|
141
|
+
// Faster observed intervals distinguish intermittent jank from a genuinely slower
|
|
142
|
+
// display. This is a live reference, not an empty-page health certification.
|
|
143
|
+
const sorted = [...this.cadence].sort((a, b) => a - b);
|
|
144
|
+
const interval = sorted[Math.ceil(sorted.length * (sorted.length < 8 ? 0.5 : 0.2)) - 1];
|
|
145
|
+
this.fpsFloor = Math.min(limits.fps, 900 / interval);
|
|
146
|
+
this.cadence = []; this.cadenceAt = null;
|
|
147
|
+
}
|
|
148
|
+
const bad = this.overflow || this.fpsFloor === null || fps < this.fpsFloor || hostMs !== null && hostMs > limits.longTaskMsPerSecond;
|
|
149
|
+
this.metrics = this.fpsFloor === null ? null : { at: now, hostFps: fps, hostLongTaskMsPerSecond: hostMs, attributedLongTaskMsPerSecond: ownMs,
|
|
128
150
|
attribution: ownMs === null ? "unknown" : "observed", fpsFloor: this.fpsFloor, windowMs: 2000 };
|
|
129
151
|
this.overflow = false;
|
|
130
152
|
if (bad) {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/** #1629 B2: one host-owned Canvas Worker pool. Product adapters are deliberately separate. */
|
|
2
2
|
import { allocateEffectFrames, effectEvent, effectPlaybackReason } from "./effect-policy.js";
|
|
3
|
-
import { captureEffectBaseline } from "./effect-baseline.js";
|
|
4
3
|
import { createOpaqueEffectFrame } from "./effect-frame.js";
|
|
5
4
|
import { createEffectGeometry, hostEffectRect } from "./effect-geometry.js";
|
|
6
5
|
import { effectStockSnapshot, effectStockAllows } from "./effect-stock-snapshot.js";
|
|
@@ -10,7 +9,7 @@ import { EffectBudget } from "./effect-profiler.js";
|
|
|
10
9
|
const registries = new WeakMap();
|
|
11
10
|
const plain = o => !!o && (Object.getPrototypeOf(o) === Object.prototype || Object.getPrototypeOf(o) === null);
|
|
12
11
|
const finite = v => typeof v === "number" && Number.isFinite(v);
|
|
13
|
-
const FLAGS = ["safe", "publicLink", "reducedMotion", "
|
|
12
|
+
const FLAGS = ["safe", "publicLink", "reducedMotion", "hidden", "enabled", "show", "transportReady", "constrained", "modal"];
|
|
14
13
|
const TYPES = ["state", "reply", "theme"];
|
|
15
14
|
function unsafeSnapshotDescriptor(d) {
|
|
16
15
|
if (!plain(d) || !plain(d.params ?? {})) return null;
|
|
@@ -41,7 +40,7 @@ function snapshotDescriptor(d) { try { return unsafeSnapshotDescriptor(d); } cat
|
|
|
41
40
|
*/
|
|
42
41
|
export function createEffectRegistry({ window: host, origin, boot, load, safety, stockSnapshot, onChange = () => {}, onProfile = () => {}, onMount = () => {}, onRetire = () => {}, clock = () => host.performance.now() } = {}) {
|
|
43
42
|
try { if (!host?.document || !host.document.body || registries.has(host) || typeof load !== "function" || typeof stockSnapshot !== "function" || typeof onChange !== "function" || typeof onProfile !== "function" || typeof onMount !== "function" || typeof onRetire !== "function" || typeof clock !== "function" || !safety || ["allowed", "begin", "finish", "pause", "resume"].some(k => typeof safety[k] !== "function") || ["requestAnimationFrame", "cancelAnimationFrame", "setTimeout", "clearTimeout", "MutationObserver", "ResizeObserver", "AbortController"].some(k => typeof host[k] !== "function")) return { ok: false, reason: "registry_unsupported" }; } catch { return { ok: false, reason: "registry_unsupported" }; }
|
|
44
|
-
let dead = false, blocked = null, revision = 0, last = null, lastView = null, frameSerial = 0, context = null, descriptors = [],
|
|
43
|
+
let dead = false, blocked = null, revision = 0, last = null, lastView = null, frameSerial = 0, context = null, descriptors = [], raf = null, timer = null;
|
|
45
44
|
const slots = new Map(), loads = new Set(), paused = new Map(), offs = [], observed = new Set(), failures = new Map(), results = new Map();
|
|
46
45
|
let readingStock = false;
|
|
47
46
|
let root, geometry, observer, resize, longTasks = null, longTasksSupported = false;
|
|
@@ -119,7 +118,7 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
119
118
|
if (s.request) { s.request.controller.abort(); if (s.request.timer !== null) host.clearTimeout(s.request.timer); }
|
|
120
119
|
// A closed/unknown viewer gate revokes paint, not the effect identity. Fresh known gates
|
|
121
120
|
// may admit it again; resource failures and actual safety pauses retain their latches.
|
|
122
|
-
if (!["disposed", "superseded", "hidden", "human_resume", "
|
|
121
|
+
if (!["disposed", "superseded", "hidden", "human_resume", "geometry_changed", "gates_unknown"].includes(why)) failures.set(s.d.key, { identity: signature(s.d), reason: why });
|
|
123
122
|
s.stageOff?.(); s.stageOff = null;
|
|
124
123
|
s.frame?.remove(why); s.geometry?.remove();
|
|
125
124
|
if (s.loadOff) s.loadOff();
|
|
@@ -138,12 +137,11 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
138
137
|
retire(s, why);
|
|
139
138
|
if (revision !== proposed || dead !== wasDead) return false;
|
|
140
139
|
}
|
|
141
|
-
if (calibration) { calibration.cancel(); calibration = null; }
|
|
142
140
|
return true;
|
|
143
141
|
}
|
|
144
142
|
function gatesKnown() { return context && FLAGS.every(k => typeof context[k] === "boolean") && (context.openInstance === null || typeof context.openInstance === "string" && context.openInstance.length >= 1 && context.openInstance.length <= 128); }
|
|
145
143
|
function eligible(d = null) {
|
|
146
|
-
return gatesKnown() && !context.safe && !context.reducedMotion &&
|
|
144
|
+
return gatesKnown() && !context.safe && !context.reducedMotion && context.enabled && context.show && context.transportReady
|
|
147
145
|
&& (!d || !context.publicLink || d.region === "chat" && !d.trialStage);
|
|
148
146
|
}
|
|
149
147
|
const globalAllowed = (d = null) => eligible(d) && host.document.visibilityState === "visible";
|
|
@@ -221,10 +219,9 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
221
219
|
if (!chosen.has(s.d.key) || !d || signature(d) !== signature(s.d) || d.element !== s.d.element || s.device !== device()) retire(s, "superseded");
|
|
222
220
|
if (dead || proposal !== revision) return;
|
|
223
221
|
}
|
|
224
|
-
if (chosen.size === 0) {
|
|
222
|
+
if (chosen.size === 0) { tell(); return; }
|
|
225
223
|
if (hidden) {
|
|
226
224
|
hideGeometry();
|
|
227
|
-
if (calibration) { calibration.cancel(); calibration = null; }
|
|
228
225
|
for (const s of [...slots.values()]) {
|
|
229
226
|
if (!s.frame) retire(s, "hidden");
|
|
230
227
|
else { if (s.hiddenSince === null) { s.hiddenSince = last; s.healthySince = null; } if (s.budget.gates({ hidden: true, reducedMotion: false, saveData: false, show: true }).remove) retire(s, "hidden"); }
|
|
@@ -238,20 +235,6 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
238
235
|
if (dead || proposal !== revision) return;
|
|
239
236
|
}
|
|
240
237
|
try { observe(); } catch { stopAll("trust_geometry_unknown"); tell(); return; }
|
|
241
|
-
if (baseline === null || baselineDevice !== device()) {
|
|
242
|
-
if (calibration && baselineDevice === device()) { tell(); return; }
|
|
243
|
-
if (!stopAll("calibrating") || dead || proposal !== revision) return;
|
|
244
|
-
const epoch = ++serial;
|
|
245
|
-
const job = captureEffectBaseline({ window: host, empty: () => !dead && slots.size === 0 && host.document.querySelectorAll("iframe.agend-effect-frame").length === 0 && globalAllowed() && !context.hidden && !!coverage(), clock: nativeClock });
|
|
246
|
-
calibration = job; baselineDevice = device();
|
|
247
|
-
job.promise.then(r => {
|
|
248
|
-
if (dead || calibration !== job || epoch !== serial) return;
|
|
249
|
-
calibration = null;
|
|
250
|
-
if (!r.ok) { blocked = r.reason; tell(); return; }
|
|
251
|
-
baseline = r.baselineFps; reconcile();
|
|
252
|
-
});
|
|
253
|
-
tell(); return;
|
|
254
|
-
}
|
|
255
238
|
const max = context.constrained ? 1 : 3;
|
|
256
239
|
for (const d of descriptors) {
|
|
257
240
|
if (!chosen.has(d.key) || slots.has(d.key)) continue;
|
|
@@ -321,7 +304,7 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
321
304
|
if (d.trialStage && !local || !current(s)) { if (current(s)) retire(s, "admission_revoked"); return; }
|
|
322
305
|
if (local) fresh = local.rect;
|
|
323
306
|
s.rect = fresh;
|
|
324
|
-
s.budget = new EffectBudget({ device: s.device,
|
|
307
|
+
s.budget = new EffectBudget({ device: s.device, clock: nativeClock, longTasksSupported });
|
|
325
308
|
const frame = createOpaqueEffectFrame({ window: host, root: local?.root ?? root, origin, boot, clock: nativeClock,
|
|
326
309
|
render: { code: resource.code, params: resource.params, assets: resource.assets, wants: resource.wants, instance: d.instance, width: fresh.width, height: fresh.height, dpr: dpr(s.device) },
|
|
327
310
|
onReady() { if (!current(s)) return; if (!globalAllowed(d) || context.hidden || !coverage(d)) { retire(s, "hidden"); tell(); return; } const n = time(); if (n === null) { blocked = "clock_unavailable"; stopAll(blocked); tell(); return; } s.ready = true; s.readyAt = n; s.healthySince = n; publishEnv(s); schedule(); tell(); },
|
|
@@ -511,11 +494,11 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
511
494
|
revalidateSafety,
|
|
512
495
|
/** Called only by the host's explicit human Resume UI. A descriptor update never clears this latch. */
|
|
513
496
|
resume(key) {
|
|
514
|
-
if (dead || reentrantStock() || blocked
|
|
497
|
+
if (dead || reentrantStock() || blocked || typeof key !== "string") return false;
|
|
515
498
|
const proposed = revision, d = descriptors.find(d => d.key === key); if (!d || d.trialStage && !coverage(d) || revision !== proposed || receipt(safety.resume, d.id, device()) !== true || revision !== proposed) return false;
|
|
516
499
|
revision++;
|
|
517
500
|
if (!stopAll("human_resume")) return false;
|
|
518
|
-
if (blocked
|
|
501
|
+
if (blocked) return false; paused.delete(key); failures.delete(key); reconcile(); return true;
|
|
519
502
|
},
|
|
520
503
|
/** Host-only content-free signal API. Raw pointer publishing is refused; native pointer metadata is coalesced here. */
|
|
521
504
|
publish(type, fields, instance = null) {
|
|
@@ -531,7 +514,7 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
|
|
|
531
514
|
return sent;
|
|
532
515
|
},
|
|
533
516
|
dispose() {
|
|
534
|
-
if (dead) return; dead = true; revision++;
|
|
517
|
+
if (dead) return; dead = true; revision++; stopAll("disposed");
|
|
535
518
|
if (raf !== null) host.cancelAnimationFrame(raf); if (timer !== null) host.clearTimeout(timer);
|
|
536
519
|
observer.disconnect(); resize.disconnect(); longTasks?.disconnect(); for (const off of offs) off();
|
|
537
520
|
geometry.dispose(); root.remove(); observed.clear(); if (loads.size === 0 && registries.get(host) === api) registries.delete(host);
|
|
@@ -11,7 +11,7 @@ const SPECS = [
|
|
|
11
11
|
["confirmation", ".shell > .pending-stack, .shell .p-settings > .panel-body > .col > .pending-stack.inline"], ["preview", ".shell .chat-split > .pv-panel, .shell .chat-split > .pv-divider"],
|
|
12
12
|
["preview-menu", ".shell .thread details.pv-menu"], ["palette", ".shell .dock .cmd-palette"],
|
|
13
13
|
["drop", ".shell .p-chat > .drop-overlay"],
|
|
14
|
-
["thread-control", ".shell .p-chat .jump-latest, .shell .thread .msg-tools, .shell .thread .st-more, .shell .thread .cb-head, .shell .thread .cb-more, .shell .thread .tc-head, .shell .thread .tc-more, .shell .thread .pv-head, .shell .thread .pv-note, .shell .thread .pv-banner, .shell .thread .effect-trial-offer, .shell .thread .click-chip"],
|
|
14
|
+
["thread-control", ".shell .chat-main > .amb-note, .shell .p-chat .jump-latest, .shell .thread .msg-tools, .shell .thread .st-more, .shell .thread .cb-head, .shell .thread .cb-more, .shell .thread .tc-head, .shell .thread .tc-more, .shell .thread .pv-head, .shell .thread .pv-note, .shell .thread .pv-banner, .shell .thread .effect-trial-offer, .shell .thread .click-chip"],
|
|
15
15
|
["link", ".shell .thread .md a, .shell .thread .st-text > a.st-link, .shell .dock .prompts .txt > a.st-link"], ["attachment", ".shell .thread .atts"], ["media", ".shell .thread .att-media"],
|
|
16
16
|
["panel-navigation", ".shell .p-fleet > .seg, .shell .p-settings > .seg"],
|
|
17
17
|
["panel-content", ".shell .p-fleet > .panel-body, .shell .p-needs > .panel-body, .shell .p-settings > .panel-body, .shell .p-log > .panel-body, .shell .p-details > .panel-body, .shell .p-view > .panel-body, .shell .p-view > .v-term, .shell .p-view > .v-id, .shell .p-view > .v-card"],
|
|
@@ -20,7 +20,7 @@ const SPECS = [
|
|
|
20
20
|
];
|
|
21
21
|
export const STOCK_TRUST_INVENTORY = Object.freeze(SPECS.map(([kind, selector]) => Object.freeze({ kind, selector })));
|
|
22
22
|
const STRUCTURAL = ["html", "body", "#app", "#app > .shell", ".shell > .main", ".main > .p-chat", ".p-chat > .chat-split", ".chat-split > .chat-main"];
|
|
23
|
-
const CONTENT = [".main > .conn", ".p-chat > .panel-head", ".p-chat > .chat-card", ".p-chat > .aw-chat_header", ".chat-main > .scroller", ".chat-main > .dock", ".sidebar > .side-head", ".sidebar > .side-nav", ".sidebar > .side-section", ".sidebar > .side-widget", ".sidebar > .side-foot", ".u-rail > .u-rail-head", ".u-rail > .u-rail-body"];
|
|
23
|
+
const CONTENT = [".main > .conn", ".p-chat > .panel-head", ".p-chat > .chat-card", ".p-chat > .aw-chat_header", ".chat-main > .amb-note", ".chat-main > .scroller", ".chat-main > .dock", ".sidebar > .side-head", ".sidebar > .side-nav", ".sidebar > .side-section", ".sidebar > .side-widget", ".sidebar > .side-foot", ".u-rail > .u-rail-head", ".u-rail > .u-rail-body"];
|
|
24
24
|
// Closed producer/topology inventory. Bare Outlet loading/error panels never become an effect surface.
|
|
25
25
|
const PANELS = Object.freeze({
|
|
26
26
|
fleet: { required: [".panel-head", ".seg", ".panel-body"], optional: [] },
|
|
@@ -5,19 +5,27 @@ export const FX_FOLLOW_REDUCED_MOTION_KEY = "agend_fx_follow_reduced_motion";
|
|
|
5
5
|
/** The one-time chat note "your system asks for reduced motion; effects are on" was shown on this device. */
|
|
6
6
|
export const FX_MOTION_NOTE_KEY = "agend_fx_motion_note_seen";
|
|
7
7
|
const listeners = new Set();
|
|
8
|
+
let pageChoice = null, noteShown = false;
|
|
8
9
|
|
|
9
10
|
/** Whether this device asked effects to follow the system's reduced-motion setting (default: no). */
|
|
10
11
|
export function followsReducedMotion() {
|
|
12
|
+
if (pageChoice !== null) return pageChoice;
|
|
11
13
|
try { return globalThis.localStorage.getItem(FX_FOLLOW_REDUCED_MOTION_KEY) === "1"; } catch { return false; }
|
|
12
14
|
}
|
|
13
15
|
export function setFollowsReducedMotion(on) {
|
|
16
|
+
pageChoice = !!on;
|
|
14
17
|
try { if (on) globalThis.localStorage.setItem(FX_FOLLOW_REDUCED_MOTION_KEY, "1"); else globalThis.localStorage.removeItem(FX_FOLLOW_REDUCED_MOTION_KEY); } catch { /* this page only */ }
|
|
15
18
|
for (const fn of [...listeners]) { try { fn(); } catch { /* a listener's own */ } }
|
|
16
19
|
}
|
|
17
20
|
/** `fn()` when the switch or the system's answer moves (this tab, another tab, the OS). Returns the unsubscribe. */
|
|
18
21
|
export function onMotionChange(fn) {
|
|
19
22
|
listeners.add(fn);
|
|
20
|
-
const onStorage = (e) => {
|
|
23
|
+
const onStorage = (e) => {
|
|
24
|
+
if (!e || e.key === FX_FOLLOW_REDUCED_MOTION_KEY || e.key === null) {
|
|
25
|
+
pageChoice = e?.key === FX_FOLLOW_REDUCED_MOTION_KEY ? e.newValue === "1" : null;
|
|
26
|
+
fn();
|
|
27
|
+
}
|
|
28
|
+
};
|
|
21
29
|
let q = null;
|
|
22
30
|
try { q = typeof globalThis.matchMedia === "function" ? globalThis.matchMedia("(prefers-reduced-motion: reduce)") : null; } catch { q = null; }
|
|
23
31
|
try { globalThis.addEventListener && globalThis.addEventListener("storage", onStorage); } catch { /* none */ }
|
|
@@ -37,10 +45,11 @@ export function systemReducesMotion(win = globalThis) {
|
|
|
37
45
|
* note was not shown here before. Showing it marks it shown.
|
|
38
46
|
*/
|
|
39
47
|
export function motionNoteDue(win = globalThis) {
|
|
40
|
-
if (followsReducedMotion() || !systemReducesMotion(win)) return false;
|
|
41
|
-
try { return globalThis.localStorage.getItem(FX_MOTION_NOTE_KEY) !== "1"; } catch { return
|
|
48
|
+
if (noteShown || followsReducedMotion() || !systemReducesMotion(win)) return false;
|
|
49
|
+
try { return globalThis.localStorage.getItem(FX_MOTION_NOTE_KEY) !== "1"; } catch { return true; }
|
|
42
50
|
}
|
|
43
51
|
export function markMotionNoteShown() {
|
|
52
|
+
noteShown = true;
|
|
44
53
|
try { globalThis.localStorage.setItem(FX_MOTION_NOTE_KEY, "1"); } catch { /* shown again next time: harmless */ }
|
|
45
54
|
}
|
|
46
55
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { html, useEffect, useLayoutEffect, useMemo, useRef, useState } from "./app-html.js";
|
|
5
5
|
import { t } from "./app-i18n.js";
|
|
6
6
|
import { useLease } from "./app-ctx.js";
|
|
7
|
+
import { guestPage } from "./app-store.js";
|
|
7
8
|
import { readStream } from "./read-stream.js";
|
|
8
9
|
import { Dialog } from "./ui-dialog.js";
|
|
9
10
|
import { ErrorState, Skeleton } from "./ui-states.js";
|
|
@@ -159,11 +160,18 @@ export function UsageBody({ owner }) {
|
|
|
159
160
|
${!p.stale && p.hint ? html`<p class="note">${usageText(p.hint, p.hintI18n)}</p>` : !(p.metrics || []).length ? html`<p class="note">${tn("usage.no_data")}</p>` : null}`}
|
|
160
161
|
</section>`)}
|
|
161
162
|
<div class="u-foot"><span class="note" title=${new Date(data.fetchedAt).toLocaleString()}>${updatedAgo(data.fetchedAt)}${data.refreshing ? html` · <span class="u-refreshing">${tn("usageRefreshing")}</span>` : null}</span>
|
|
163
|
+
${byAgent() ? html`<a class="btn btn-ghost btn-sm u-by-agent" href="/ui/fleet/usage" title=${tn("usageByAgentTitle")}>${tn("usageByAgent")}</a>` : null}
|
|
162
164
|
<button type="button" class="btn btn-sm" onClick=${() => load(true)}>${tn("usageRefresh")}</button></div>`;
|
|
163
165
|
}
|
|
164
166
|
return content;
|
|
165
167
|
}
|
|
166
168
|
|
|
169
|
+
/**
|
|
170
|
+
* #2122: the plans above are per login; each agent's own tokens and API-equivalent are Fleet → Usage — linked from the
|
|
171
|
+
* app (not View's anonymous reader, not a guest, who has no Fleet).
|
|
172
|
+
*/
|
|
173
|
+
const byAgent = () => !guestPage() && typeof location !== "undefined" && location.pathname.startsWith("/ui");
|
|
174
|
+
|
|
167
175
|
/** `owner`: the navigation that opened it — its reads and the minute's timer live and end with it. */
|
|
168
176
|
export function UsageDialog({ onClose, owner = "" }) {
|
|
169
177
|
return html`<${Dialog} title=${tn("usage.title")} onClose=${onClose} wide=${true}><${UsageBody} owner=${`usage-dialog:${owner}`} /></${Dialog}>`;
|
|
@@ -40,6 +40,8 @@ register("view", {
|
|
|
40
40
|
"usageMinutes": "{n} min",
|
|
41
41
|
"usageHours": "{n} h",
|
|
42
42
|
"usageRefresh": "Refresh",
|
|
43
|
+
"usageByAgent": "By agent →",
|
|
44
|
+
"usageByAgentTitle": "Each agent's tokens and API-equivalent (Fleet → Usage)",
|
|
43
45
|
"usageHigh": "high",
|
|
44
46
|
"usageNear": "near limit",
|
|
45
47
|
"usageMoveUp": "Move up",
|
|
@@ -248,6 +250,8 @@ register("view", {
|
|
|
248
250
|
"usageMinutes": "{n} 分鐘",
|
|
249
251
|
"usageHours": "{n} 小時",
|
|
250
252
|
"usageRefresh": "重新整理",
|
|
253
|
+
"usageByAgent": "依 agent →",
|
|
254
|
+
"usageByAgentTitle": "每個 agent 的 token 與 API 等值(Fleet → 用量)",
|
|
251
255
|
"usageHigh": "偏高",
|
|
252
256
|
"usageNear": "接近上限",
|
|
253
257
|
"usageMoveUp": "上移",
|
package/dist/usage-report.d.ts
CHANGED
|
@@ -22,7 +22,10 @@ export type UsageRange = {
|
|
|
22
22
|
};
|
|
23
23
|
/** What every amount is, in words — the page, the CSV and the agent tool all carry it. */
|
|
24
24
|
export declare const usageBasis: (checked?: string) => string;
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Daily rows returned at most (the CSV and the trend): one per day, source and model. Beyond it the report keeps the
|
|
27
|
+
* latest whole days and says from which date (`dailyFrom`); the rows and totals always cover the whole range.
|
|
28
|
+
*/
|
|
26
29
|
export declare const USAGE_DAILY_MAX = 20000;
|
|
27
30
|
export interface UsageInstanceInput {
|
|
28
31
|
name: string;
|
|
@@ -54,6 +57,8 @@ export interface UsageInput {
|
|
|
54
57
|
pendingBytes: number;
|
|
55
58
|
pendingFiles: number;
|
|
56
59
|
};
|
|
60
|
+
/** Daily rows at most (default USAGE_DAILY_MAX; tests lower it to reach the same path). */
|
|
61
|
+
dailyMax?: number;
|
|
57
62
|
}
|
|
58
63
|
export interface UsageTotals {
|
|
59
64
|
/** Uncached input. */
|
|
@@ -111,7 +116,13 @@ export interface UsageReport {
|
|
|
111
116
|
rows: UsageRow[];
|
|
112
117
|
totals: UsageRow;
|
|
113
118
|
daily: UsageDaily[];
|
|
119
|
+
/**
|
|
120
|
+
* The daily rows (and the messages by day) are not the whole range: they start at `dailyFrom` (earlier days were too
|
|
121
|
+
* many rows to send), or — `dailyFrom` null — there are none at all (the latest day alone was more than the cap: a
|
|
122
|
+
* part of a day is never sent as if it were the day). The rows and totals always cover the whole range.
|
|
123
|
+
*/
|
|
114
124
|
dailyTruncated: boolean;
|
|
125
|
+
dailyFrom: string | null;
|
|
115
126
|
messagesDaily: Array<{
|
|
116
127
|
date: string;
|
|
117
128
|
instance: string;
|
package/dist/usage-report.js
CHANGED
|
@@ -4,7 +4,10 @@ export const USAGE_GROUPS = ["instance", "backend", "model", "profile", "team"];
|
|
|
4
4
|
export const USAGE_RANGES = ["today", "7d", "month", "all"];
|
|
5
5
|
/** What every amount is, in words — the page, the CSV and the agent tool all carry it. */
|
|
6
6
|
export const usageBasis = (checked = PRICES_CHECKED) => `API-equivalent at list price (checked ${checked}) — not money spent`;
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Daily rows returned at most (the CSV and the trend): one per day, source and model. Beyond it the report keeps the
|
|
9
|
+
* latest whole days and says from which date (`dailyFrom`); the rows and totals always cover the whole range.
|
|
10
|
+
*/
|
|
8
11
|
export const USAGE_DAILY_MAX = 20_000;
|
|
9
12
|
const M = 1_000_000;
|
|
10
13
|
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
@@ -102,7 +105,8 @@ export function buildUsageReport(input) {
|
|
|
102
105
|
const newRow = (key) => ({ key, ...zero(), total: 0, messages: null, amount: null, priced: "none", flags: [], unpriced: [], instances: [], backends: [], models: [], profiles: [], _amount: 0, _pricedAny: false, _unpricedAny: false });
|
|
103
106
|
const uniq = (list, v) => { if (!list.includes(v))
|
|
104
107
|
list.push(v); };
|
|
105
|
-
|
|
108
|
+
// Daily rows: one per day, zone, agent, backend, model, tier and profile (an hour's slices summed into their day).
|
|
109
|
+
const dailyBy = new Map();
|
|
106
110
|
for (const s of slices) {
|
|
107
111
|
const key = keyOf(s);
|
|
108
112
|
const row = rows.get(key) ?? (rows.set(key, newRow(key)), rows.get(key));
|
|
@@ -124,11 +128,36 @@ export function buildUsageReport(input) {
|
|
|
124
128
|
uniq(row.flags, "cache_write_not_reported");
|
|
125
129
|
if (unknownSource(s))
|
|
126
130
|
uniq(row.flags, "source_unknown");
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
+
const tier = s.long ? "long" : "standard";
|
|
132
|
+
const dk = [s.date, s.zone, s.instance, s.backend, s.model, tier, s.profile].join("\u0000");
|
|
133
|
+
let d = dailyBy.get(dk);
|
|
134
|
+
if (!d) {
|
|
135
|
+
d = { date: s.date, zone: s.zone, instance: s.instance, backend: s.backend, model: s.model, tier, profile: s.profile, ...zero(), amount: amount === null ? null : 0,
|
|
136
|
+
flags: [...(s.writesReported ? [] : ["cache_write_not_reported"]), ...(amount === null ? ["not_priced"] : []), ...(unknownSource(s) ? ["source_unknown"] : [])] };
|
|
137
|
+
dailyBy.set(dk, d);
|
|
138
|
+
}
|
|
139
|
+
addTotals(d, s.tot);
|
|
140
|
+
if (amount !== null && d.amount !== null)
|
|
141
|
+
d.amount += amount;
|
|
142
|
+
}
|
|
143
|
+
const allDaily = [...dailyBy.values()].sort((a, b) => a.date.localeCompare(b.date) || a.instance.localeCompare(b.instance) || a.model.localeCompare(b.model) || a.zone.localeCompare(b.zone));
|
|
144
|
+
// Too many: the latest whole days that fit — never part of a day (a missing row would read as nothing used). Not even
|
|
145
|
+
// the latest day fits: no daily rows at all, said so (dailyTruncated with no dailyFrom).
|
|
146
|
+
let dailyFrom = null, daily = allDaily, truncated = false;
|
|
147
|
+
const cap = input.dailyMax ?? USAGE_DAILY_MAX;
|
|
148
|
+
if (allDaily.length > cap) {
|
|
149
|
+
truncated = true;
|
|
150
|
+
const perDate = new Map();
|
|
151
|
+
for (const d of allDaily)
|
|
152
|
+
perDate.set(d.date, (perDate.get(d.date) ?? 0) + 1);
|
|
153
|
+
let kept = 0;
|
|
154
|
+
for (const date of [...perDate.keys()].sort().reverse()) {
|
|
155
|
+
if (kept + perDate.get(date) > cap)
|
|
156
|
+
break;
|
|
157
|
+
kept += perDate.get(date);
|
|
158
|
+
dailyFrom = date;
|
|
131
159
|
}
|
|
160
|
+
daily = dailyFrom === null ? [] : allDaily.filter(d => d.date >= dailyFrom);
|
|
132
161
|
}
|
|
133
162
|
// Messages: delivered, per instance and hour (§5.4); placed on days the same way as tokens.
|
|
134
163
|
const messagesDaily = new Map();
|
|
@@ -187,8 +216,10 @@ export function buildUsageReport(input) {
|
|
|
187
216
|
return {
|
|
188
217
|
range: { kind: input.range.kind, from, to }, zone: input.zone, granularity: "hour", basis: usageBasis(), pricesChecked: PRICES_CHECKED,
|
|
189
218
|
group: input.group, rows: out, totals,
|
|
190
|
-
daily
|
|
191
|
-
|
|
219
|
+
daily, dailyTruncated: truncated, dailyFrom,
|
|
220
|
+
// Messages by day: the same window as the daily rows (none when there are none); their total stays in `totals`.
|
|
221
|
+
messagesDaily: [...messagesDaily.values()].filter(m => !truncated || (dailyFrom !== null && m.date >= dailyFrom))
|
|
222
|
+
.sort((a, b) => a.date.localeCompare(b.date) || a.instance.localeCompare(b.instance)),
|
|
192
223
|
notMeasured, shared, coverage, scanning: input.scanning,
|
|
193
224
|
};
|
|
194
225
|
}
|