@songsid/agend 2.3.4-beta.7 → 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.
Files changed (146) hide show
  1. package/THIRD_PARTY_NOTICES.md +37 -0
  2. package/dist/agent-cli-instructions.md +1 -1
  3. package/dist/agent-credentials.d.ts +24 -0
  4. package/dist/agent-credentials.js +66 -0
  5. package/dist/agent-credentials.js.map +1 -0
  6. package/dist/agent-endpoint.d.ts +3 -1
  7. package/dist/agent-endpoint.js +13 -0
  8. package/dist/agent-endpoint.js.map +1 -1
  9. package/dist/authority/session-authorizations.d.ts +28 -4
  10. package/dist/authority/session-authorizations.js +63 -17
  11. package/dist/authority/session-authorizations.js.map +1 -1
  12. package/dist/backend/antigravity.js +3 -1
  13. package/dist/backend/antigravity.js.map +1 -1
  14. package/dist/backend/codex-footer-hints.d.ts +4 -0
  15. package/dist/backend/codex-footer-hints.js +27 -0
  16. package/dist/backend/codex-footer-hints.js.map +1 -0
  17. package/dist/backend/codex.js +8 -3
  18. package/dist/backend/codex.js.map +1 -1
  19. package/dist/backend/kiro.d.ts +3 -0
  20. package/dist/backend/kiro.js +39 -1
  21. package/dist/backend/kiro.js.map +1 -1
  22. package/dist/backend/muse.js +5 -3
  23. package/dist/backend/muse.js.map +1 -1
  24. package/dist/backend/types.d.ts +6 -1
  25. package/dist/backend/types.js +3 -0
  26. package/dist/backend/types.js.map +1 -1
  27. package/dist/cache-ledger.d.ts +85 -0
  28. package/dist/cache-ledger.js +236 -1
  29. package/dist/cache-ledger.js.map +1 -1
  30. package/dist/cache-service.d.ts +5 -0
  31. package/dist/cache-service.js +16 -4
  32. package/dist/cache-service.js.map +1 -1
  33. package/dist/channel/adapters/discord.js +11 -3
  34. package/dist/channel/adapters/discord.js.map +1 -1
  35. package/dist/channel/adapters/telegram.js +4 -3
  36. package/dist/channel/adapters/telegram.js.map +1 -1
  37. package/dist/channel/mcp-tools.js +1 -1
  38. package/dist/channel/mcp-tools.js.map +1 -1
  39. package/dist/cross-instance-notice.d.ts +10 -0
  40. package/dist/cross-instance-notice.js +67 -5
  41. package/dist/cross-instance-notice.js.map +1 -1
  42. package/dist/daemon.d.ts +43 -0
  43. package/dist/daemon.js +361 -17
  44. package/dist/daemon.js.map +1 -1
  45. package/dist/delivery-consumed-watch.d.ts +23 -1
  46. package/dist/delivery-consumed-watch.js +30 -3
  47. package/dist/delivery-consumed-watch.js.map +1 -1
  48. package/dist/delivery-reconciliation.d.ts +7 -0
  49. package/dist/delivery-reconciliation.js +9 -1
  50. package/dist/delivery-reconciliation.js.map +1 -1
  51. package/dist/export-import.d.ts +7 -0
  52. package/dist/export-import.js +44 -10
  53. package/dist/export-import.js.map +1 -1
  54. package/dist/fleet-context.d.ts +2 -0
  55. package/dist/fleet-manager.d.ts +85 -4
  56. package/dist/fleet-manager.js +375 -30
  57. package/dist/fleet-manager.js.map +1 -1
  58. package/dist/general-knowledge/skills/concepts/SKILL.md +390 -0
  59. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +1 -0
  60. package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -1
  61. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +41 -4
  62. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
  63. package/dist/instance-lifecycle.d.ts +8 -2
  64. package/dist/instance-lifecycle.js +37 -6
  65. package/dist/instance-lifecycle.js.map +1 -1
  66. package/dist/instance-topic-delete.d.ts +77 -0
  67. package/dist/instance-topic-delete.js +123 -0
  68. package/dist/instance-topic-delete.js.map +1 -0
  69. package/dist/instructions.d.ts +1 -1
  70. package/dist/instructions.js +1 -1
  71. package/dist/instructions.js.map +1 -1
  72. package/dist/kiro-engine-status.d.ts +11 -0
  73. package/dist/kiro-engine-status.js +6 -0
  74. package/dist/kiro-engine-status.js.map +1 -1
  75. package/dist/locale.js +47 -1
  76. package/dist/locale.js.map +1 -1
  77. package/dist/log-steps.d.ts +4 -3
  78. package/dist/log-steps.js +32 -7
  79. package/dist/log-steps.js.map +1 -1
  80. package/dist/login-flows.d.ts +2 -0
  81. package/dist/login-flows.js +5 -2
  82. package/dist/login-flows.js.map +1 -1
  83. package/dist/needs-you-hub.js +1 -1
  84. package/dist/needs-you-hub.js.map +1 -1
  85. package/dist/needs-you.d.ts +6 -0
  86. package/dist/needs-you.js +8 -2
  87. package/dist/needs-you.js.map +1 -1
  88. package/dist/outbound-handlers.js +3 -3
  89. package/dist/outbound-handlers.js.map +1 -1
  90. package/dist/outbound-schemas.js +5 -2
  91. package/dist/outbound-schemas.js.map +1 -1
  92. package/dist/queued-handoffs.d.ts +33 -0
  93. package/dist/queued-handoffs.js +75 -0
  94. package/dist/queued-handoffs.js.map +1 -0
  95. package/dist/scheduler/db.d.ts +2 -1
  96. package/dist/scheduler/db.js +23 -9
  97. package/dist/scheduler/db.js.map +1 -1
  98. package/dist/settings-api.d.ts +13 -0
  99. package/dist/settings-api.js +12 -1
  100. package/dist/settings-api.js.map +1 -1
  101. package/dist/tool-permissions.d.ts +5 -2
  102. package/dist/tool-permissions.js +6 -5
  103. package/dist/tool-permissions.js.map +1 -1
  104. package/dist/tool-progress.d.ts +2 -0
  105. package/dist/tool-progress.js +19 -3
  106. package/dist/tool-progress.js.map +1 -1
  107. package/dist/topic-commands.js +4 -0
  108. package/dist/topic-commands.js.map +1 -1
  109. package/dist/ui/chat-strings.js +2 -2
  110. package/dist/ui/fleet-usage.js +90 -27
  111. package/dist/ui/panel-chat.js +2 -0
  112. package/dist/ui/panel-needs.js +4 -3
  113. package/dist/ui/settings-strings.js +2 -2
  114. package/dist/ui/shared/app-i18n.js +2 -2
  115. package/dist/ui/shared/app-needs.js +1 -1
  116. package/dist/ui/shared/app.css +15 -0
  117. package/dist/ui/shared/usage-dialog.js +18 -1
  118. package/dist/ui/shared/view-strings.js +16 -0
  119. package/dist/usage/capacity.d.ts +69 -0
  120. package/dist/usage/capacity.js +119 -0
  121. package/dist/usage/capacity.js.map +1 -0
  122. package/dist/usage/i18n-keys.d.ts +1 -1
  123. package/dist/usage/i18n-keys.js +3 -0
  124. package/dist/usage/i18n-keys.js.map +1 -1
  125. package/dist/usage/profile-timeline.d.ts +16 -0
  126. package/dist/usage/profile-timeline.js +58 -0
  127. package/dist/usage/profile-timeline.js.map +1 -0
  128. package/dist/usage/quota-history.d.ts +43 -0
  129. package/dist/usage/quota-history.js +168 -0
  130. package/dist/usage/quota-history.js.map +1 -0
  131. package/dist/usage/usage-api.d.ts +31 -0
  132. package/dist/usage/usage-api.js +80 -1
  133. package/dist/usage/usage-api.js.map +1 -1
  134. package/dist/usage-report.d.ts +25 -1
  135. package/dist/usage-report.js +55 -4
  136. package/dist/usage-report.js.map +1 -1
  137. package/dist/usage-rollup.d.ts +4 -1
  138. package/dist/usage-rollup.js +12 -3
  139. package/dist/usage-rollup.js.map +1 -1
  140. package/dist/web-api.d.ts +6 -0
  141. package/dist/web-api.js +8 -2
  142. package/dist/web-api.js.map +1 -1
  143. package/dist/web-session-authorization.d.ts +13 -2
  144. package/dist/web-session-authorization.js +10 -3
  145. package/dist/web-session-authorization.js.map +1 -1
  146. package/package.json +1 -1
@@ -14,11 +14,20 @@ register("fusage", {
14
14
  en: {
15
15
  range: "Range", today: "Today", r7d: "7 days", month: "This month", all: "All", custom: "Custom", from: "From", to: "To", apply: "Apply",
16
16
  group: "Group by", g_instance: "Agent", g_backend: "Backend", g_model: "Model", g_profile: "Credential profile", g_team: "Team (as configured now)",
17
- csv: "Download CSV", basis: "API-equivalent at list price (checked {0}) — not money spent. Subscriptions are not billed per token.",
17
+ csv: "Download CSV", basis: "API-equivalent at list price ({0}) — not money spent.", basisMore: "Subscriptions are not billed per token.",
18
18
  amount: "API-equivalent", input: "Input", output: "Output", reasoning: "Reasoning", read: "Cache read", write: "Cache write", total: "Total tokens", requests: "Requests", messages: "Messages",
19
19
  notPriced: "not priced", partial: "partial", noTeam: "(no team)", unknown: "unknown",
20
- f_cache_write_not_reported: "cache writes not reported (Codex, Muse) — an estimate", f_not_priced: "not priced: {0}", f_source_unknown: "read before backend and profile were recorded",
21
- f_estimated: "estimated from text size — Kiro records no token counts", f_includes_estimates: "includes estimates (Kiro)", f_cost_reported: "amount as the CLI reported it (no list price for this model)", estimatedPart: "of which estimated: {0}",
20
+ // #3066 §3: what a row's numbers are, said once each as a small mark with its tooltip (never a pill per row).
21
+ t_not_priced: "Not priced: {0}", t_partial: "Part of this row's models have no price", t_write_unreported: "Codex/Muse don't report cache writes",
22
+ t_cost_reported: "Amount as the CLI reported it", t_estimated: "Estimated from text size (Kiro records no token counts)", t_reasoning_none: "Not reported by this CLI",
23
+ sourceNote: "Some hours were read before AgEnD recorded their backend and profile.", profileUnrecorded: "profile not recorded",
24
+ l_partial: "◐ partly priced", l_cost_reported: "† amount from the CLI", l_estimated: "≈ estimated",
25
+ // #3066 §2: output speed (Claude after the first token, Codex over the response window).
26
+ speed: "Speed", speedUnit: "{0} tok/s", speedCoverage: "{0} of {1} requests timed", speedSpread: "p10–p90: {0}–{1} tok/s",
27
+ speedNone: "no timing in its transcripts", speedByModel: "Speed by model",
28
+ speedBasis: "Output tokens ÷ the model's own response time (tool time excluded); method after tokdash.",
29
+
30
+ f_includes_estimates: "includes estimates (Kiro)", estimatedPart: "of which estimated: {0}",
22
31
  scanning: "Scanning — totals may still grow.", updated: "Updated {0} · read every 10 min", since: "Collected since {0}", notYet: "Not collected yet",
23
32
  refresh: "Refresh",
24
33
  notMeasured: "Not measured", nm_credit_billed: "Kiro is credit-billed and records no tokens", nm_unsupported: "AgEnD can't read this CLI's usage yet", nm_no_data: "nothing read yet",
@@ -42,11 +51,18 @@ register("fusage", {
42
51
  "zh-TW": {
43
52
  range: "期間", today: "今天", r7d: "7 天", month: "本月", all: "全部", custom: "自訂", from: "從", to: "到", apply: "套用",
44
53
  group: "分組", g_instance: "Agent", g_backend: "Backend", g_model: "模型", g_profile: "憑證 profile", g_team: "Team(依目前設定)",
45
- csv: "下載 CSV", basis: "依牌價換算的 API 等值(查證日期 {0})——不是實際花費。訂閱方案不按 token 計費。",
54
+ csv: "下載 CSV", basis: "依牌價換算的 API 等值({0})——不是實際花費。", basisMore: "訂閱方案不按 token 計費。",
46
55
  amount: "API 等值", input: "輸入", output: "輸出", reasoning: "推理", read: "快取讀取", write: "快取寫入", total: "token 總計", requests: "請求數", messages: "訊息數",
47
56
  notPriced: "未定價", partial: "部分", noTeam: "(不屬於任何 team)", unknown: "未知",
48
- f_cache_write_not_reported: "未回報快取寫入(Codex、Muse)——是估算", f_not_priced: "未定價:{0}", f_source_unknown: "讀取時尚未記錄 backend 與 profile",
49
- f_estimated: "依文字長度估算——Kiro 不記錄 token 數", f_includes_estimates: "含估算值(Kiro)", f_cost_reported: "金額為 CLI 自行回報(價目表沒有此模型)", estimatedPart: "其中估算:{0}",
57
+ t_not_priced: "未定價:{0}", t_partial: "這一列有部分模型沒有價格", t_write_unreported: "Codex/Muse 不回報快取寫入",
58
+ t_cost_reported: "金額為 CLI 自行回報", t_estimated: "依文字長度估算(Kiro 不記錄 token 數)", t_reasoning_none: "這個 CLI 不回報",
59
+ sourceNote: "部分時段在 AgEnD 記錄 backend 與 profile 之前就已讀取。", profileUnrecorded: "未記錄 profile",
60
+ l_partial: "◐ 部分定價", l_cost_reported: "† 金額由 CLI 回報", l_estimated: "≈ 估算值",
61
+ speed: "速度", speedUnit: "{0} token/秒", speedCoverage: "{1} 個請求中有 {0} 個可計時", speedSpread: "p10–p90:{0}–{1} token/秒",
62
+ speedNone: "transcript 沒有計時資訊", speedByModel: "各模型速度",
63
+ speedBasis: "輸出 token ÷ 模型本身的回應時間(不含工具執行時間);方法參考 tokdash。",
64
+
65
+ f_includes_estimates: "含估算值(Kiro)", estimatedPart: "其中估算:{0}",
50
66
  scanning: "掃描中——總數可能還會增加。", updated: "更新於 {0} · 每 10 分鐘讀取一次", since: "自 {0} 起收集", notYet: "尚未收集",
51
67
  refresh: "重新整理",
52
68
  notMeasured: "未量測", nm_credit_billed: "Kiro 以 credit 計費,不記錄 token", nm_unsupported: "AgEnD 目前還讀不到這個 CLI 的用量", nm_no_data: "尚未讀到任何資料",
@@ -95,10 +111,6 @@ export function keyText(group, key) {
95
111
  return key;
96
112
  }
97
113
  const writes = (r) => (r.cacheWrite5m || 0) + (r.cacheWrite1h || 0) + (r.cacheWrite || 0);
98
- function flagText(f, r) {
99
- if (f === "not_priced") return tu("f_not_priced", (r.unpriced || []).join(", "));
100
- return ["cache_write_not_reported", "source_unknown", "estimated", "includes_estimates", "cost_reported"].includes(f) ? tu(`f_${f}`) : f;
101
- }
102
114
 
103
115
  // ── CSV (design §4): the daily rows of this same answer; messages on their own rows (they are per agent, not per model).
104
116
  const CSV_COLUMNS = ["date", "zone", "instance", "backend", "model", "tier", "profile", "input", "output", "reasoning", "cache_read", "cache_write_5m", "cache_write_1h", "cache_write", "requests", "messages", "amount", "basis", "flags", "estimated"];
@@ -318,25 +330,73 @@ function Heatmap({ report, dates }) {
318
330
  </section>`;
319
331
  }
320
332
 
321
- function Row({ r, group, max, measure }) {
333
+ /**
334
+ * A mark with what it means: shown as its sign, said (title, and aria-label for readers) as words. role="img" makes it
335
+ * nameable (#3067 review, Prism P3: aria-label is not allowed on a generic span).
336
+ */
337
+ const mark = (sign, words, cls = "us-mark") => html`<span class=${cls} role="img" title=${words} aria-label=${words}>${sign}</span>`;
338
+ /** "—" that says why there is no number (nameable the same way). */
339
+ const dash = (words) => html`<span class="us-none" role="img" title=${words} aria-label=${words}>—</span>`;
340
+ /** The marks a row's numbers carry (#3066 §3): ◐ partly priced, † the CLI's own amount, ≈ estimated. */
341
+ export function rowMarks(r) {
342
+ const flags = r.flags || [];
343
+ return { partial: r.priced === "partial", cost: flags.includes("cost_reported"), est: flags.includes("estimated") || r.estimated === true,
344
+ writeUnreported: flags.includes("cache_write_not_reported") && writes(r) === 0, unpriced: r.amount === null || r.amount === undefined };
345
+ }
346
+ /** A profile as shown: Codex's older hours were never tagged with one. */
347
+ const profileText = (p) => (p === "unrecorded" ? tu("profileUnrecorded") : p);
348
+
349
+ function Row({ r, group, max, measure, cols }) {
322
350
  const share = max > 0 ? Math.max(0.01, measure(r) / max) : 0;
323
- const meta = [r.backends.join(", "), r.models.join(", "), r.profiles.join(", ")].filter(Boolean).join(" · ");
351
+ const meta = [r.backends.join(", "), r.models.join(", "), r.profiles.map(profileText).join(", ")].filter(Boolean).join(" · ");
352
+ const m = rowMarks(r);
353
+ // Estimated rows (Kiro): every number is an estimate — "≈" before each, the reason in its tooltip.
354
+ const num = (text) => (m.est ? html`${mark("≈", tu("t_estimated"), "us-mark us-approx")}${text}` : text);
355
+ const key = group === "profile" ? profileText(keyText(group, r.key)) : keyText(group, r.key);
324
356
  return html`<tr class="us-row">
325
- <th scope="row" class="us-name"><span class="mono">${keyText(group, r.key)}</span>
357
+ <th scope="row" class="us-name"><span class="mono">${key}</span>
326
358
  <svg class="us-bar" aria-hidden="true"><rect class="us-bar-bg" width="100%" height="6" rx="3" /><rect class="us-bar-fg" width=${`${Math.round(share * 100)}%`} height="6" rx="3" /></svg>
327
- ${group !== "instance" || meta ? html`<span class="us-meta">${meta}</span>` : null}
328
- ${r.flags.length ? html`<span class="us-flags">${r.flags.map((f) => html`<span key=${f} class="ch-tag">${flagText(f, r)}</span>`)}</span>` : null}</th>
329
- <td data-label=${tu("amount")} class=${`num${r.amount === null ? " muted" : ""}`}>${amountText(r)}</td>
330
- <td data-label=${tu("input")} class="num">${tokens(r.input)}</td>
331
- <td data-label=${tu("output")} class="num">${tokens(r.output)}</td>
332
- <td data-label=${tu("reasoning")} class="num">${r.reasoningReported === false ? "—" : tokens(r.reasoning || 0)}</td>
333
- <td data-label=${tu("read")} class="num">${tokens(r.cacheRead)}</td>
334
- <td data-label=${tu("write")} class="num">${r.flags.includes("cache_write_not_reported") && writes(r) === 0 ? "—" : tokens(writes(r))}</td>
335
- <td data-label=${tu("total")} class="num">${tokens(r.total)}</td>
359
+ ${group !== "instance" || meta ? html`<span class="us-meta">${meta}</span>` : null}</th>
360
+ <td data-label=${tu("amount")} class=${`num${m.unpriced ? " muted" : ""}`}>${m.unpriced ? dash(tu("t_not_priced", (r.unpriced || []).join(", "))) : html`${num(money(r.amount))}${m.partial ? mark("◐", tu("t_partial")) : null}${m.cost ? mark("†", tu("t_cost_reported")) : null}`}</td>
361
+ <td data-label=${tu("input")} class="num">${num(tokens(r.input))}</td>
362
+ <td data-label=${tu("output")} class="num">${num(tokens(r.output))}</td>
363
+ ${cols.reasoning ? html`<td data-label=${tu("reasoning")} class="num">${r.reasoningReported === false ? dash(tu("t_reasoning_none")) : num(tokens(r.reasoning || 0))}</td>` : null}
364
+ <td data-label=${tu("read")} class="num">${num(tokens(r.cacheRead))}</td>
365
+ <td data-label=${tu("write")} class="num">${m.writeUnreported ? dash(tu("t_write_unreported")) : num(tokens(writes(r)))}</td>
366
+ <td data-label=${tu("total")} class="num">${num(tokens(r.total))}</td>
336
367
  <td data-label=${tu("requests")} class="num">${n(r.requests)}</td>
337
- <td data-label=${tu("messages")} class="num">${r.messages === null ? "—" : n(r.messages)}</td>
368
+ ${cols.messages ? html`<td data-label=${tu("messages")} class="num">${r.messages === null ? "—" : n(r.messages)}</td>` : null}
369
+ ${cols.speed ? html`<td data-label=${tu("speed")} class="num">${r.speed ? speedText(r.speed) : dash(tu("speedNone"))}</td>` : null}
338
370
  </tr>`;
339
371
  }
372
+ /** #3066 §2: a rate as shown — whole tok/s, one decimal below 10. */
373
+ export const speedText = (s) => tu("speedUnit", s.tokPerSec >= 10 ? Math.round(s.tokPerSec).toLocaleString("en-US") : s.tokPerSec.toFixed(1));
374
+ /** Columns worth a place (#3066 §3): Reasoning only when some row reports it; Messages only when some row has them; Speed (§2) only when some response was timed. */
375
+ export function usageColumns(rows) {
376
+ return { reasoning: rows.some((r) => r.reasoningReported !== false), messages: rows.some((r) => r.messages !== null && r.messages !== undefined),
377
+ speed: rows.some((r) => !!r.speed) };
378
+ }
379
+ /** #3066 §2: each model's speed — rate · requests timed (coverage) · p10–p90 from 30 timed. */
380
+ function SpeedCard({ report }) {
381
+ const list = report.speedByModel || [];
382
+ if (!list.length) return null;
383
+ return html`<section class="us-speed card" aria-labelledby="us-speed-h">
384
+ <h3 id="us-speed-h">${tu("speedByModel")}</h3>
385
+ <ul class="rows">${list.map((m) => html`<li key=${m.model} class="row-item"><span class="grow strong mono">${m.model}</span>
386
+ <span class="num">${speedText(m.speed)}</span>
387
+ <span class="muted">${[m.requests > 0 ? tu("speedCoverage", n(m.speed.timedRequests), n(m.requests)) : null,
388
+ m.speed.p10 !== null && m.speed.p90 !== null ? tu("speedSpread", Math.round(m.speed.p10), Math.round(m.speed.p90)) : null].filter(Boolean).join(" · ")}</span></li>`)}</ul>
389
+ <p class="note">${tu("speedBasis")}</p>
390
+ </section>`;
391
+ }
392
+ /** The legend under the table: only the marks that appear, and the one footnote when an hour's source was not recorded. */
393
+ function Legend({ rows }) {
394
+ const marks = rows.map(rowMarks);
395
+ const parts = [marks.some((m) => m.partial) && tu("l_partial"), marks.some((m) => m.cost) && tu("l_cost_reported"), marks.some((m) => m.est) && tu("l_estimated")].filter(Boolean);
396
+ const source = rows.some((r) => (r.flags || []).includes("source_unknown"));
397
+ if (!parts.length && !source) return null;
398
+ return html`<p class="note us-key">${parts.join(" · ")}${parts.length && source ? html`<br />` : null}${source ? tu("sourceNote") : null}</p>`;
399
+ }
340
400
 
341
401
  /** The tab's body for one answer of GET /ui/usage-report. */
342
402
  export function UsageReport({ report, choice, setChoice, refresh, fetchedAt }) {
@@ -355,6 +415,7 @@ export function UsageReport({ report, choice, setChoice, refresh, fetchedAt }) {
355
415
  const zones = zonesChanged(report);
356
416
  const csvFrom = report.dailyTruncated ? report.dailyFrom : report.range.kind === "all" ? "all" : report.range.from;
357
417
  const rows = report.rows.filter((r) => r.total > 0 || (r.messages || 0) > 0);
418
+ const cols = usageColumns(rows);
358
419
  const anyAmount = rows.some((r) => r.amount !== null && r.amount > 0);
359
420
  const measure = (r) => (anyAmount ? r.amount || 0 : r.total);
360
421
  const max = Math.max(0, ...rows.map(measure));
@@ -375,7 +436,7 @@ export function UsageReport({ report, choice, setChoice, refresh, fetchedAt }) {
375
436
  <label>${tu("from")} <input type="date" value=${draft.from} max=${draft.to} onInput=${(e) => setDraft({ ...draft, from: e.target.value })} /></label>
376
437
  <label>${tu("to")} <input type="date" value=${draft.to} min=${draft.from} onInput=${(e) => setDraft({ ...draft, to: e.target.value })} /></label>
377
438
  <button type="submit" class="btn btn-sm">${tu("apply")}</button></form>` : null}
378
- <p class="us-basis" role="note">${tu("basis", report.pricesChecked)}</p>
439
+ <p class="us-basis" role="note" title=${tu("basisMore")}>${tu("basis", report.pricesChecked)}</p>
379
440
  ${noDaily ? html`<p class="us-partial" role="status">${tu("noDaily")}</p>` : report.dailyTruncated ? html`<p class="us-partial" role="status">${tu("partialDays", report.dailyFrom)}</p>` : null}
380
441
  ${zones ? html`<p class="us-partial" role="status">${tu("zoneChanged", zones.join(", "), report.zone)}</p>` : null}
381
442
  <p class="note us-fresh">${tu("updated", new Date(fetchedAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }))} · ${Number.isFinite(since) ? tu("since", new Date(since).toLocaleDateString()) : tu("notYet")} · ${tu("zoneNote", report.zone)}${group === "team" ? ` ${tu("teamsNote")}` : ""}
@@ -389,10 +450,12 @@ export function UsageReport({ report, choice, setChoice, refresh, fetchedAt }) {
389
450
  <div class="ch-metric"><dt>${tu("messages")}</dt><dd>${report.totals.messages === null ? "—" : n(report.totals.messages)}</dd></div>
390
451
  </dl>
391
452
  ${rows.length ? html`<table class="us-table"><thead><tr><th scope="col">${tu(`g_${group}`)}</th><th scope="col" class="num">${tu("amount")}</th>
392
- <th scope="col" class="num">${tu("input")}</th><th scope="col" class="num">${tu("output")}</th><th scope="col" class="num">${tu("reasoning")}</th><th scope="col" class="num">${tu("read")}</th><th scope="col" class="num">${tu("write")}</th>
393
- <th scope="col" class="num">${tu("total")}</th><th scope="col" class="num">${tu("requests")}</th><th scope="col" class="num">${tu("messages")}</th></tr></thead>
394
- <tbody>${rows.map((r) => html`<${Row} key=${r.key} r=${r} group=${group} max=${max} measure=${measure} />`)}</tbody></table>`
453
+ <th scope="col" class="num">${tu("input")}</th><th scope="col" class="num">${tu("output")}</th>${cols.reasoning ? html`<th scope="col" class="num">${tu("reasoning")}</th>` : null}<th scope="col" class="num">${tu("read")}</th><th scope="col" class="num">${tu("write")}</th>
454
+ <th scope="col" class="num">${tu("total")}</th><th scope="col" class="num">${tu("requests")}</th>${cols.messages ? html`<th scope="col" class="num">${tu("messages")}</th>` : null}${cols.speed ? html`<th scope="col" class="num">${tu("speed")}</th>` : null}</tr></thead>
455
+ <tbody>${rows.map((r) => html`<${Row} key=${r.key} r=${r} group=${group} max=${max} measure=${measure} cols=${cols} />`)}</tbody></table>
456
+ <${Legend} rows=${rows} />`
395
457
  : html`<p class="note">${tu("noRows")}</p>`}
458
+ <${SpeedCard} report=${report} />
396
459
  <${Trend} report=${report} dates=${dates} />
397
460
  <${Heatmap} report=${report} dates=${dates} />`}
398
461
  ${report.notMeasured.length || report.shared.length ? html`<section class="us-rest" aria-labelledby="us-rest-h"><h3 id="us-rest-h">${tu("notMeasured")}</h3>
@@ -797,6 +797,8 @@ function DeleteDialog({ name, onClose }) {
797
797
  } catch (err) { r = { error: err && err.message ? err.message : t("chat.disconnected") }; }
798
798
  if (r && r.error) { toast(r.error, false); if (!handedOver && lease.current()) setBusy(false); return; }
799
799
  toast(t("chat.instanceDeleted", name));
800
+ // The agent's own topic went too, or stayed with the reason (the server's own words: no new string here).
801
+ if (r.topic_note) toast(r.topic_note, r.topic_deleted !== false);
800
802
  if (handedOver || !lease.current()) return;
801
803
  setBusy(false);
802
804
  onClose();
@@ -25,7 +25,7 @@ import { openSignIn, signInOffered } from "./backend-signin.js";
25
25
  register("needs", {
26
26
  en: {
27
27
  title: "Needs you", empty: "Nothing needs you right now", emptyHint: "When an agent waits for you, it shows here — and in your chat app's General.", emptyHintWeb: "When an agent waits for you, it shows here.",
28
- open: "Open chat", openCard: "Open the card", noAssignee: "Task board (nobody assigned)", fleetSignIns: "Fleet sign-ins", signIn: "Sign in",
28
+ open: "Open chat", kiroOpen: "Open agent", kiroMigration: "Migration instructions", openCard: "Open the card", noAssignee: "Task board (nobody assigned)", fleetSignIns: "Fleet sign-ins", signIn: "Sign in",
29
29
  gap: "not wrapped up: {0}", gapUnchecked: "{0} checklist item(s) unchecked", gapNoResult: "no result", gapJoin: ", ", acknowledge: "Acknowledge", acknowledging: "Acknowledging…", answering: "Answering…",
30
30
  ageNow: "just now", ageMin: "{0} min", ageHour: "{0} h", ageDay: "{0} d",
31
31
  notifyTitle: "Notify me on this device", notifyOn: "Notifications are on for this browser while a tab is open.",
@@ -38,7 +38,7 @@ register("needs", {
38
38
  },
39
39
  "zh-TW": {
40
40
  title: "等你處理", empty: "目前沒有需要你處理的事", emptyHint: "agent 在等你時,會出現在這裡——也會出現在聊天 app 的 General。", emptyHintWeb: "agent 在等你時,會出現在這裡。",
41
- open: "開啟聊天", openCard: "打開卡片", noAssignee: "任務看板(沒有負責人)", fleetSignIns: "Fleet 登入", signIn: "登入",
41
+ open: "開啟聊天", kiroOpen: "開啟 Agent", kiroMigration: "遷移說明", openCard: "打開卡片", noAssignee: "任務看板(沒有負責人)", fleetSignIns: "Fleet 登入", signIn: "登入",
42
42
  gap: "還沒補齊:{0}", gapUnchecked: "還有 {0} 項沒勾", gapNoResult: "沒有產出總結", gapJoin: "、", acknowledge: "確認", acknowledging: "確認中…", answering: "回覆中…",
43
43
  ageNow: "剛剛", ageMin: "{0} 分鐘", ageHour: "{0} 小時", ageDay: "{0} 天",
44
44
  notifyTitle: "在這台裝置通知我", notifyOn: "這個瀏覽器的通知已開啟(需要開著分頁)。",
@@ -204,7 +204,8 @@ function NeedsItem({ item, nowMs, owner }) {
204
204
  <${Icon} name="check" size=${14} />${ack === "busy" ? tn("acknowledging") : tn("acknowledge")}</button>` : null}
205
205
  ${(item.reason === "auth_paused" || item.reason === "backend_signin") && item.signIn && signInOffered() ? html`<button type="button" class="btn btn-sm btn-primary" onClick=${() => openSignIn(item.signIn)}>${tn("signIn")}</button>` : null}
206
206
  ${item.type === "task" && item.card ? html`<a class="btn btn-sm" href=${`/ui/fleet?card=${encodeURIComponent(item.card.code)}`}>${tn("openCard")}</a>` : null}
207
- ${item.instance ? html`<a class="btn btn-sm btn-ghost" href=${chatPath(item.instance)}>${tn("open")}</a>` : null}
207
+ ${item.kiroChoice === true ? html`<a class="btn btn-sm" href="https://github.com/songsid/AgEnD/blob/main/src/general-knowledge/skills/kiro-engine-migration/SKILL.md" target="_blank" rel="noopener noreferrer">${tn("kiroMigration")}</a>` : null}
208
+ ${item.instance ? html`<a class="btn btn-sm btn-ghost" href=${chatPath(item.instance)}>${tn(item.kiroChoice === true ? "kiroOpen" : "open")}</a>` : null}
208
209
  </div>
209
210
  </div>
210
211
  </article>`;
@@ -166,7 +166,7 @@ register("settings", {
166
166
  failed: "Failed",
167
167
  actionFailed: "{0} failed",
168
168
  noAgentMatch: "No agents match \"{0}\".",
169
- deleteAgent: "Delete agent \"{0}\"? This will be applied only after you confirm the pending changes.",
169
+ deleteAgent: "Delete agent \"{0}\" and its chat topic (on Discord, its channel and history)? This will be applied only after you confirm the pending changes.",
170
170
  nameRequired: "name required",
171
171
  tokenEnvRequired: "token env required",
172
172
  removeBot: "Remove bot \"{0}\"? (the token in .env is left untouched)",
@@ -823,7 +823,7 @@ register("settings", {
823
823
  failed: "失敗",
824
824
  actionFailed: "{0}失敗",
825
825
  noAgentMatch: "沒有符合「{0}」的 Agent。",
826
- deleteAgent: "確定刪除 Agent「{0}」嗎?只有在確認套用待處理變更後才會生效。",
826
+ deleteAgent: "確定刪除 Agent「{0}」及它的聊天 topic(在 Discord 上是它的頻道與訊息記錄)嗎?只有在確認套用待處理變更後才會生效。",
827
827
  nameRequired: "必須填寫名稱",
828
828
  tokenEnvRequired: "必須填寫 token 環境變數",
829
829
  removeBot: "確定移除機器人「{0}」嗎?(.env 中的 token 會保留)",
@@ -111,7 +111,7 @@ register("app", {
111
111
  confirmTitle: "Please confirm", confirmOk: "Continue", cancel: "Cancel",
112
112
  sidePanelLeave: "Leave {0} without saving? Your changes there will be lost.", sidePanelDiscard: "Discard changes",
113
113
  // The server's own wording (src/locale.ts needs.reason.*), so the web says what Discord says.
114
- needs_reason_hang: "Not responding", needs_reason_exited: "Exited", needs_reason_assist: "Waiting at its terminal", needs_reason_question: "Asking you a question", needs_reason_permission: "Permission needed", needs_reason_dangerous_command: "Dangerous command waiting", needs_reason_login: "Sign-in needed", needs_reason_dialog: "Waiting at a dialog", needs_reason_terminal_input: "May be waiting for input", needs_reason_auth_paused: "Paused: sign-in needed", needs_reason_backend_signin: "Backend sign-in needed", needs_reason_config_invalid: "Configuration file needs attention", needs_reason_config_fallback: "Running on stored configuration", config_history_edit: "{0}, line {1}, column {2}: {3}. AgEnD is still running on the previous settings. The next restart would use the stored valid copy from {4}. Correct the YAML error; the file has not been changed by AgEnD.", config_history_no_history: "{0}, line {1}, column {2}: {3}. AgEnD is still running on the previous settings. No stored valid copy is available, so the next restart cannot recover automatically. Correct the YAML error before restarting; the file has not been changed by AgEnD.", config_history_fallback: "{0}, line {1}, column {2}: {3}. AgEnD started using the stored valid copy from {4}. Correct the YAML error and reload the settings. The file on disk has not been changed by AgEnD.", needs_reason_crashed: "Crashed", needs_reason_usage_limit: "Usage limit reached · reset time unknown", needs_reason_delivery_uncertain: "Could not confirm delivery", needs_reason_delivery_failed: "Delivery failed", needs_reason_task_review: "Waiting for your confirmation", needs_reason_task_blocked: "Blocked: asks you",
114
+ needs_reason_hang: "Not responding", needs_reason_exited: "Exited", needs_reason_assist: "Waiting at its terminal", needs_reason_question: "Asking you a question", needs_reason_permission: "Permission needed", needs_reason_dangerous_command: "Dangerous command waiting", needs_reason_login: "Sign-in needed", needs_reason_dialog: "Waiting at a dialog", needs_reason_kiro_choice: "Kiro needs your decision", needs_reason_terminal_input: "May be waiting for input", needs_reason_auth_paused: "Paused: sign-in needed", needs_reason_backend_signin: "Backend sign-in needed", needs_reason_config_invalid: "Configuration file needs attention", needs_reason_config_fallback: "Running on stored configuration", config_history_edit: "{0}, line {1}, column {2}: {3}. AgEnD is still running on the previous settings. The next restart would use the stored valid copy from {4}. Correct the YAML error; the file has not been changed by AgEnD.", config_history_no_history: "{0}, line {1}, column {2}: {3}. AgEnD is still running on the previous settings. No stored valid copy is available, so the next restart cannot recover automatically. Correct the YAML error before restarting; the file has not been changed by AgEnD.", config_history_fallback: "{0}, line {1}, column {2}: {3}. AgEnD started using the stored valid copy from {4}. Correct the YAML error and reload the settings. The file on disk has not been changed by AgEnD.", needs_reason_crashed: "Crashed", needs_reason_usage_limit: "Usage limit reached · reset time unknown", needs_reason_delivery_uncertain: "Could not confirm delivery", needs_reason_delivery_failed: "Delivery failed", needs_reason_task_review: "Waiting for your confirmation", needs_reason_task_blocked: "Blocked: asks you",
115
115
  },
116
116
  "zh-TW": {
117
117
  brand: "AgEnD", fleet: "Fleet", fleetOverview: "Fleet 概覽", view: "檢視", settings: "設定", instances: "Instances", newInstance: "新增 instance", newAgent: "新增 Agent",
@@ -156,6 +156,6 @@ register("app", {
156
156
  search: "搜尋", searchTitle: "搜尋聊天、agent 之間的訊息與任務({0})",
157
157
  confirmTitle: "請確認", confirmOk: "繼續", cancel: "取消",
158
158
  sidePanelLeave: "不儲存就離開「{0}」?你在那裡的修改會遺失。", sidePanelDiscard: "捨棄修改",
159
- needs_reason_hang: "沒有回應", needs_reason_exited: "已結束", needs_reason_assist: "在終端機上等待", needs_reason_question: "正在問你問題", needs_reason_permission: "需要權限", needs_reason_dangerous_command: "危險指令待確認", needs_reason_login: "需要登入", needs_reason_dialog: "停在對話框", needs_reason_terminal_input: "可能在等輸入", needs_reason_auth_paused: "已暫停:需要登入", needs_reason_backend_signin: "後端需要登入", needs_reason_config_invalid: "設定檔需要處理", needs_reason_config_fallback: "正在使用儲存的設定副本", config_history_edit: "{0} 第 {1} 行、第 {2} 欄:{3}。AgEnD 仍使用先前的設定執行。下次重新啟動會改用 {4} 儲存的有效副本。請修正 YAML 錯誤;AgEnD 未更動此檔案。", config_history_no_history: "{0} 第 {1} 行、第 {2} 欄:{3}。AgEnD 仍使用先前的設定執行。沒有可用的有效副本,下次重新啟動將無法自動恢復。請在重新啟動前修正 YAML 錯誤;AgEnD 未更動此檔案。", config_history_fallback: "{0} 第 {1} 行、第 {2} 欄:{3}。AgEnD 已使用 {4} 儲存的有效副本啟動。請修正 YAML 錯誤並重新載入設定。AgEnD 未更動磁碟上的檔案。", needs_reason_crashed: "已崩潰", needs_reason_usage_limit: "用量已達上限 · 恢復時間未知", needs_reason_delivery_uncertain: "無法確認是否送達", needs_reason_delivery_failed: "傳送失敗", needs_reason_task_review: "等你確認", needs_reason_task_blocked: "卡住,在問你",
159
+ needs_reason_hang: "沒有回應", needs_reason_exited: "已結束", needs_reason_assist: "在終端機上等待", needs_reason_question: "正在問你問題", needs_reason_permission: "需要權限", needs_reason_dangerous_command: "危險指令待確認", needs_reason_login: "需要登入", needs_reason_dialog: "停在對話框", needs_reason_kiro_choice: "Kiro 需要你決定", needs_reason_terminal_input: "可能在等輸入", needs_reason_auth_paused: "已暫停:需要登入", needs_reason_backend_signin: "後端需要登入", needs_reason_config_invalid: "設定檔需要處理", needs_reason_config_fallback: "正在使用儲存的設定副本", config_history_edit: "{0} 第 {1} 行、第 {2} 欄:{3}。AgEnD 仍使用先前的設定執行。下次重新啟動會改用 {4} 儲存的有效副本。請修正 YAML 錯誤;AgEnD 未更動此檔案。", config_history_no_history: "{0} 第 {1} 行、第 {2} 欄:{3}。AgEnD 仍使用先前的設定執行。沒有可用的有效副本,下次重新啟動將無法自動恢復。請在重新啟動前修正 YAML 錯誤;AgEnD 未更動此檔案。", config_history_fallback: "{0} 第 {1} 行、第 {2} 欄:{3}。AgEnD 已使用 {4} 儲存的有效副本啟動。請修正 YAML 錯誤並重新載入設定。AgEnD 未更動磁碟上的檔案。", needs_reason_crashed: "已崩潰", needs_reason_usage_limit: "用量已達上限 · 恢復時間未知", needs_reason_delivery_uncertain: "無法確認是否送達", needs_reason_delivery_failed: "傳送失敗", needs_reason_task_review: "等你確認", needs_reason_task_blocked: "卡住,在問你",
160
160
  },
161
161
  });
@@ -63,7 +63,7 @@ export const configDetail = (item) => {
63
63
  const issue = item.configIssue;
64
64
  return issue ? t(`app.config_history_${issue.mode === "fallback" ? "fallback" : issue.time ? "edit" : "no_history"}`, issue.file, issue.line, issue.column, issue.reason, issue.time || "") : item.detail;
65
65
  };
66
- export const reasonText = (item) => t(`app.needs_reason_${item.reason}`);
66
+ export const reasonText = (item) => t(`app.needs_reason_${item.kiroChoice ? "kiro_choice" : item.reason}`);
67
67
 
68
68
  let stop = null;
69
69
 
@@ -1392,6 +1392,11 @@ body.side-resizing, body.side-resizing * { cursor: col-resize !important; user-s
1392
1392
  .u-stale { font-weight: var(--fw-semibold); }
1393
1393
  .u-stale-detail { color: var(--text-3); font-size: var(--fs-xs); }
1394
1394
  .u-sub { color: var(--text-3); font-size: var(--fs-xs); font-variant-numeric: tabular-nums; }
1395
+ /* #3066 §1: a weekly quota's measured 100% under its bar, and its notes (the fleet-only limit always). */
1396
+ .u-cap { margin-top: 4px; font-size: var(--fs-xs); }
1397
+ .u-cap-line { color: var(--text-2); font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
1398
+ .u-cap-wait .u-cap-line { color: var(--text-3); }
1399
+ .u-cap-note { color: var(--text-3); overflow-wrap: anywhere; }
1395
1400
  .u-err { color: var(--warn); font-size: var(--fs-sm); }
1396
1401
  .u-foot { display: flex; align-items: center; justify-content: space-between; gap: var(--s-2); }
1397
1402
  /* #1673: the usage rail beside the page on a wide screen (header-tools.js UsageRail; shown only at min-width 1200px). */
@@ -2111,6 +2116,16 @@ html.oo-immersive .p-fleet > .panel-body { z-index: 50; }
2111
2116
  .us-meta { display: block; font-size: var(--fs-xs); color: var(--text-3); overflow-wrap: anywhere; }
2112
2117
  .us-est { display: block; font-size: var(--fs-xs); font-weight: var(--fw-regular); color: var(--text-3); }
2113
2118
  .us-flags { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }
2119
+ /* #3066 §3: a row's numbers carry small marks (◐ partly priced, † the CLI's amount, ≈ estimated) and "—" with a reason,
2120
+ each said in its tooltip; the key under the table names only the marks that appear. */
2121
+ .us-mark { margin-left: 2px; color: var(--text-3); font-size: var(--fs-xs); cursor: help; }
2122
+ .us-mark.us-approx { margin-left: 0; margin-right: 1px; }
2123
+ .us-none { cursor: help; }
2124
+ .us-key { margin-top: 6px; }
2125
+ /* #3066 §2: each model's speed after the first token. */
2126
+ .us-speed { margin-top: 12px; }
2127
+ .us-speed .row-item { gap: 8px; flex-wrap: wrap; }
2128
+ .us-speed .num { font-variant-numeric: tabular-nums; }
2114
2129
  .us-trend, .us-heat { display: flex; flex-direction: column; gap: var(--s-2); min-width: 0; }
2115
2130
  .us-trend h3, .us-heat h3, .us-rest h3 { font-size: var(--fs-sm); font-weight: var(--fw-semibold); }
2116
2131
  .us-scroll { overflow-x: auto; max-width: 100%; }
@@ -64,6 +64,22 @@ function expiryText(iso) {
64
64
  if (!(ms > 0)) return "";
65
65
  return usageText("", { key: "usage.ticket_expiry", args: [`${at.getMonth() + 1}/${at.getDate()}`, usageDuration(ms)] });
66
66
  }
67
+ /**
68
+ * #3066 §1: a server line ({ text, i18n }) in this page's language — or, when this tab's strings predate the key (a
69
+ * stale tab), the server's own English text, never the bare key.
70
+ */
71
+ export function capacityLine(x) {
72
+ if (!x || typeof x.text !== "string") return "";
73
+ const key = x.i18n && typeof x.i18n.key === "string" ? `view.${x.i18n.key}` : null;
74
+ return key && t(key) !== key ? usageText(x.text, x.i18n) : x.text;
75
+ }
76
+ /** #3066 §1: a weekly quota's measured 100% (or why not yet), and the limit it always carries: only this fleet counts. */
77
+ function UsageCapacity({ c }) {
78
+ const notes = Array.isArray(c.notes) ? c.notes : [];
79
+ return html`<div class=${`u-cap${c.status === "ok" ? "" : " u-cap-wait"}`}>
80
+ <div class="u-cap-line">${capacityLine(c.line)}</div>
81
+ ${notes.map((n, i) => html`<div key=${i} class="u-cap-note">${capacityLine(n)}</div>`)}</div>`;
82
+ }
67
83
  export function UsageMetric({ m }) {
68
84
  const bar = useRef(null);
69
85
  const label = usageText(m.label, m.labelI18n);
@@ -78,7 +94,8 @@ export function UsageMetric({ m }) {
78
94
  const sub = [note, resetText(m.resetsAt)].filter(Boolean).join(" · ");
79
95
  return html`<div class="u-metric"><div class="u-row"><span class="u-label">${label}</span>${word ? html`<span class=${`u-status ${cls}`}>${word}</span>` : null}
80
96
  <span class=${`u-pct ${cls}`}>${pct.toFixed(0)}<span class="u-unit">%</span></span></div>
81
- <div class="u-meter"><div ref=${bar} class=${`u-fill ${cls}`}></div></div>${sub ? html`<div class="u-sub">${sub}</div>` : null}</div>`;
97
+ <div class="u-meter"><div ref=${bar} class=${`u-fill ${cls}`}></div></div>${sub ? html`<div class="u-sub">${sub}</div>` : null}
98
+ ${m.capacity ? html`<${UsageCapacity} c=${m.capacity} />` : null}</div>`;
82
99
  }
83
100
  if (m.type === "dollars") {
84
101
  const val = m.limit ? `$${(m.used ?? 0).toFixed(2)} / $${m.limit.toFixed(2)}` : `$${(m.used ?? 0).toFixed(2)}`;
@@ -154,6 +154,14 @@ register("view", {
154
154
  "usage.stale.transient": "not updated · cached at {0}",
155
155
  "usage.stale.error": "couldn't read · cached at {0}",
156
156
  "usage.metric.reset_since": "reset since {0}",
157
+ "usage.capacity.line": "100% ≈ {0} ({1}–{2}) · ≈ {3} tokens · from {4} weeks",
158
+ "usage.capacity.line_tokens": "100% ≈ {0} tokens ({1}–{2}) · from {3} weeks",
159
+ "usage.capacity.not_enough": "Not enough usage yet to measure 100% (Δ {0}% of 10%)",
160
+ "usage.capacity.only_agend": "Counts only this fleet's agents; use of the same account elsewhere makes this low.",
161
+ "usage.capacity.mixed": "Bases disagree — possibly used outside this fleet",
162
+ "usage.capacity.best_fit_api": "API-equivalent fits best: ±{0}% across {1} weeks",
163
+ "usage.capacity.best_fit_output": "Output tokens fit best: ±{0}% across {1} weeks",
164
+ "usage.capacity.best_fit_all": "All tokens fit best: ±{0}% across {1} weeks",
157
165
  "usage.error.http": "Usage request failed (HTTP {0}).",
158
166
  "usage.error.billing_http": "Billing request failed (HTTP {0}).",
159
167
  "usage.error.invalid_response": "Invalid response from usage endpoint.",
@@ -414,6 +422,14 @@ register("view", {
414
422
  "usage.stale.transient": "暫時無法更新 · 快取於 {0}",
415
423
  "usage.stale.error": "讀取失敗 · 快取於 {0}",
416
424
  "usage.metric.reset_since": "已於 {0} 重置",
425
+ "usage.capacity.line": "100% ≈ {0}({1}–{2})· 約 {3} token · 依 {4} 週",
426
+ "usage.capacity.line_tokens": "100% ≈ {0} token({1}–{2})· 依 {3} 週",
427
+ "usage.capacity.not_enough": "用量還不夠估算 100%(已用 Δ {0}%,需 10%)",
428
+ "usage.capacity.only_agend": "只計算這個 fleet 的 agent;同一帳號在別處的使用會讓這個值偏低。",
429
+ "usage.capacity.mixed": "各換算方式不一致——可能有 fleet 以外的使用",
430
+ "usage.capacity.best_fit_api": "「API 等值」最吻合:{1} 週內 ±{0}%",
431
+ "usage.capacity.best_fit_output": "「輸出 token」最吻合:{1} 週內 ±{0}%",
432
+ "usage.capacity.best_fit_all": "「全部 token」最吻合:{1} 週內 ±{0}%",
417
433
  "usage.error.http": "用量請求失敗(HTTP {0})。",
418
434
  "usage.error.billing_http": "計費請求失敗(HTTP {0})。",
419
435
  "usage.error.invalid_response": "用量端點回傳了無效資料。",
@@ -0,0 +1,69 @@
1
+ /**
2
+ * #3066 §1: how much is 100% of an account's weekly quota, measured — what this account's agents used while the vendor's
3
+ * weekly percent moved (the readings of quota-history.ts, the hours of the usage ledger). Pure: the fleet gathers both.
4
+ *
5
+ * Within one reset window (the same `resetsAt`, the percent never going down) the first and last readings bound an
6
+ * interval: capacity = consumed in it ÷ Δ%. Two uncertainties, both carried into the range, never hidden:
7
+ * - the vendors report whole percents, so Δ is ±1: the range divides by Δ+1 … Δ−1;
8
+ * - the ledger is hourly, so the two boundary hours are uncertain: without them is the low end, with them the high.
9
+ * A window counts only when Δ ≥ 10 points (rounding then costs ≤ ~±10%). Several windows: the median of their midpoints,
10
+ * the range their lowest low … highest high.
11
+ *
12
+ * Three bases, because the vendor's weighting is not published: the API-equivalent at list price (likely what the quota
13
+ * tracks — it weighs Opus over Sonnet, cache reads far below input), output tokens, all tokens. With three windows or
14
+ * more, the basis whose capacity varies least across weeks (coefficient of variation) is named as the best fit.
15
+ *
16
+ * What it cannot see: the same account used outside this fleet (claude.ai, another machine) spends the quota without a
17
+ * token here, so the estimate comes out LOW; a window whose bases disagree with each other by more than 2× is marked
18
+ * `mixed`. The page says so (usage.capacity.only_agend, usage.capacity.mixed).
19
+ */
20
+ import { type QuotaReading } from "./quota-history.js";
21
+ export declare const CAPACITY_MIN_DELTA = 10;
22
+ export declare const BASES: readonly ["api", "output", "all"];
23
+ export type CapacityBasis = typeof BASES[number];
24
+ /** One hour of one account's usage, on each basis (api null: a model in it has no list price). */
25
+ export interface CapacityHour {
26
+ hour: number;
27
+ api: number | null;
28
+ output: number;
29
+ all: number;
30
+ }
31
+ /** `windows`: how many reset windows this basis was measured over (a basis can miss one: an unpriced hour). */
32
+ export interface BasisEstimate {
33
+ median: number;
34
+ low: number;
35
+ high: number;
36
+ cv: number | null;
37
+ windows: number;
38
+ }
39
+ export type CapacityEstimate = {
40
+ status: "ok";
41
+ windows: number;
42
+ deltaPct: number;
43
+ bases: Partial<Record<CapacityBasis, BasisEstimate>>;
44
+ bestFit: CapacityBasis | null;
45
+ mixed: boolean;
46
+ } | {
47
+ status: "not_enough";
48
+ deltaPct: number;
49
+ };
50
+ interface Window {
51
+ t1: number;
52
+ p1: number;
53
+ t2: number;
54
+ p2: number;
55
+ }
56
+ /**
57
+ * The reset windows the readings span: a new `resetsAt`, or a percent that went down, starts the next. "New" is
58
+ * quota-history's sameReset (Prism #3073 P2): a vendor that gives only "resets in N seconds" drifts by seconds between
59
+ * reads and is still one window; compared with the window's latest reading, a real reset moves it by days.
60
+ */
61
+ export declare function quotaWindows(readings: readonly QuotaReading[]): Window[];
62
+ /**
63
+ * The estimate for one account's one weekly metric. `available(from, to)`: whether every hour from `from` to `to` is in
64
+ * the ledger — kept and scanned (Prism #3073 P2): the quota history keeps 12 weeks, the hours 31 days, so a window
65
+ * outside them would count as zero use and the estimate would come out absurdly low. Such a window is left out.
66
+ * Null: there are windows to measure, but no usage of this account in any of them on any basis — nothing to show.
67
+ */
68
+ export declare function estimateCapacity(readings: readonly QuotaReading[], hours: readonly CapacityHour[], available?: (from: number, to: number) => boolean): CapacityEstimate | null;
69
+ export {};
@@ -0,0 +1,119 @@
1
+ /**
2
+ * #3066 §1: how much is 100% of an account's weekly quota, measured — what this account's agents used while the vendor's
3
+ * weekly percent moved (the readings of quota-history.ts, the hours of the usage ledger). Pure: the fleet gathers both.
4
+ *
5
+ * Within one reset window (the same `resetsAt`, the percent never going down) the first and last readings bound an
6
+ * interval: capacity = consumed in it ÷ Δ%. Two uncertainties, both carried into the range, never hidden:
7
+ * - the vendors report whole percents, so Δ is ±1: the range divides by Δ+1 … Δ−1;
8
+ * - the ledger is hourly, so the two boundary hours are uncertain: without them is the low end, with them the high.
9
+ * A window counts only when Δ ≥ 10 points (rounding then costs ≤ ~±10%). Several windows: the median of their midpoints,
10
+ * the range their lowest low … highest high.
11
+ *
12
+ * Three bases, because the vendor's weighting is not published: the API-equivalent at list price (likely what the quota
13
+ * tracks — it weighs Opus over Sonnet, cache reads far below input), output tokens, all tokens. With three windows or
14
+ * more, the basis whose capacity varies least across weeks (coefficient of variation) is named as the best fit.
15
+ *
16
+ * What it cannot see: the same account used outside this fleet (claude.ai, another machine) spends the quota without a
17
+ * token here, so the estimate comes out LOW; a window whose bases disagree with each other by more than 2× is marked
18
+ * `mixed`. The page says so (usage.capacity.only_agend, usage.capacity.mixed).
19
+ */
20
+ import { sameReset } from "./quota-history.js";
21
+ export const CAPACITY_MIN_DELTA = 10;
22
+ export const BASES = ["api", "output", "all"];
23
+ const H = 3_600_000;
24
+ /**
25
+ * The reset windows the readings span: a new `resetsAt`, or a percent that went down, starts the next. "New" is
26
+ * quota-history's sameReset (Prism #3073 P2): a vendor that gives only "resets in N seconds" drifts by seconds between
27
+ * reads and is still one window; compared with the window's latest reading, a real reset moves it by days.
28
+ */
29
+ export function quotaWindows(readings) {
30
+ const out = [];
31
+ let cur = null;
32
+ for (const r of [...readings].sort((a, b) => a.t - b.t)) {
33
+ if (!cur || !sameReset(r.resetsAt, cur.resetsAt) || r.used < cur.p2) {
34
+ if (cur)
35
+ out.push(cur);
36
+ cur = { t1: r.t, p1: r.used, t2: r.t, p2: r.used, resetsAt: r.resetsAt };
37
+ }
38
+ else {
39
+ cur.t2 = r.t;
40
+ cur.p2 = r.used;
41
+ cur.resetsAt = r.resetsAt;
42
+ }
43
+ }
44
+ if (cur)
45
+ out.push(cur);
46
+ return out.map(({ t1, p1, t2, p2 }) => ({ t1, p1, t2, p2 }));
47
+ }
48
+ const median = (xs) => { const s = [...xs].sort((a, b) => a - b), m = s.length >> 1; return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2; };
49
+ const cv = (xs) => {
50
+ if (xs.length < 2)
51
+ return null;
52
+ const mean = xs.reduce((a, b) => a + b, 0) / xs.length;
53
+ return mean > 0 ? Math.sqrt(xs.reduce((a, x) => a + (x - mean) ** 2, 0) / xs.length) / mean : null;
54
+ };
55
+ /**
56
+ * The estimate for one account's one weekly metric. `available(from, to)`: whether every hour from `from` to `to` is in
57
+ * the ledger — kept and scanned (Prism #3073 P2): the quota history keeps 12 weeks, the hours 31 days, so a window
58
+ * outside them would count as zero use and the estimate would come out absurdly low. Such a window is left out.
59
+ * Null: there are windows to measure, but no usage of this account in any of them on any basis — nothing to show.
60
+ */
61
+ export function estimateCapacity(readings, hours, available = () => true) {
62
+ const windows = quotaWindows(readings).filter((w) => available(Math.floor(w.t1 / H) * H, w.t2));
63
+ const usable = windows.filter((w) => w.p2 - w.p1 >= CAPACITY_MIN_DELTA);
64
+ if (!usable.length) {
65
+ // No window inside the ledger at all: "not enough usage" would be a claim about weeks it cannot see. Say nothing.
66
+ if (!windows.length)
67
+ return null;
68
+ return { status: "not_enough", deltaPct: Math.max(0, ...windows.map((w) => w.p2 - w.p1)) };
69
+ }
70
+ const per = usable.map((w) => {
71
+ const delta = w.p2 - w.p1;
72
+ const inside = hours.filter((h) => h.hour >= w.t1 && h.hour + H <= w.t2); // whole hours within: low
73
+ const touching = hours.filter((h) => h.hour + H > w.t1 && h.hour < w.t2); // any hour touching: high
74
+ const sum = (list, b) => {
75
+ let n = 0;
76
+ for (const h of list) {
77
+ const v = h[b];
78
+ if (v === null)
79
+ return null;
80
+ n += v;
81
+ }
82
+ return n;
83
+ };
84
+ const out = {};
85
+ for (const b of BASES) {
86
+ const lo = sum(inside, b), hi = sum(touching, b);
87
+ if (lo === null || hi === null || hi <= 0)
88
+ continue; // a basis with an unpriced hour, or nothing used
89
+ out[b] = { low: lo / ((delta + 1) / 100), mid: ((lo + hi) / 2) / (delta / 100), high: hi / ((delta - 1) / 100) };
90
+ }
91
+ return out;
92
+ });
93
+ const bases = {};
94
+ for (const b of BASES) {
95
+ const ws = per.map((p) => p[b]).filter((x) => !!x);
96
+ if (!ws.length)
97
+ continue;
98
+ const mids = ws.map((x) => x.mid);
99
+ bases[b] = { median: median(mids), low: Math.min(...ws.map((x) => x.low)), high: Math.max(...ws.map((x) => x.high)), cv: cv(mids), windows: ws.length };
100
+ }
101
+ if (!BASES.some((b) => bases[b]))
102
+ return null; // Prism #3073 P1: no basis, no estimate
103
+ // Best fit: with three windows or more, the basis that varies least across them.
104
+ const scored = BASES.filter((b) => bases[b]?.cv !== null && bases[b]?.cv !== undefined && bases[b].windows >= 3);
105
+ const bestFit = scored.length ? scored.reduce((a, b) => (bases[b].cv < bases[a].cv ? b : a)) : null;
106
+ // Mixed: in some window the API-equivalent and the token count disagree (each against its own median) by over 2×.
107
+ let mixed = false;
108
+ if (bases.api && bases.all && usable.length >= 2) {
109
+ for (const p of per) {
110
+ if (!p.api || !p.all)
111
+ continue;
112
+ const ra = p.api.mid / bases.api.median, rt = p.all.mid / bases.all.median;
113
+ if (Math.max(ra / rt, rt / ra) > 2)
114
+ mixed = true;
115
+ }
116
+ }
117
+ return { status: "ok", windows: usable.length, deltaPct: Math.max(...usable.map((w) => w.p2 - w.p1)), bases, bestFit, mixed };
118
+ }
119
+ //# sourceMappingURL=capacity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capacity.js","sourceRoot":"","sources":["../../src/usage/capacity.ts"],"names":[],"mappings":"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"}