@rubytech/create-maxy-code 0.1.476 → 0.1.478

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 (165) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1704-data-portal-standing-audit.md +673 -0
  3. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1704-data-portal-standing-audit-design.md +177 -0
  4. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1820-cpu-triage-admin-tools-design.md +97 -0
  5. package/payload/platform/lib/storage-broker/dist/__tests__/audit.test.js +44 -0
  6. package/payload/platform/lib/storage-broker/dist/__tests__/audit.test.js.map +1 -1
  7. package/payload/platform/lib/storage-broker/dist/audit.d.ts +56 -0
  8. package/payload/platform/lib/storage-broker/dist/audit.d.ts.map +1 -1
  9. package/payload/platform/lib/storage-broker/dist/audit.js +34 -0
  10. package/payload/platform/lib/storage-broker/dist/audit.js.map +1 -1
  11. package/payload/platform/lib/storage-broker/src/__tests__/audit.test.ts +70 -1
  12. package/payload/platform/lib/storage-broker/src/audit.ts +111 -0
  13. package/payload/platform/plugins/admin/.claude-plugin/plugin.json +1 -1
  14. package/payload/platform/plugins/admin/PLUGIN.md +7 -1
  15. package/payload/platform/plugins/admin/mcp/dist/index.js +72 -0
  16. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  17. package/payload/platform/plugins/admin/mcp/dist/tools/cpu-triage.d.ts +94 -0
  18. package/payload/platform/plugins/admin/mcp/dist/tools/cpu-triage.d.ts.map +1 -0
  19. package/payload/platform/plugins/admin/mcp/dist/tools/cpu-triage.js +229 -0
  20. package/payload/platform/plugins/admin/mcp/dist/tools/cpu-triage.js.map +1 -0
  21. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +2 -2
  22. package/payload/platform/plugins/docs/references/admin-ui.md +1 -1
  23. package/payload/platform/plugins/email/PLUGIN.md +2 -1
  24. package/payload/platform/plugins/email/mcp/dist/__tests__/confirm-sent-copy.test.d.ts +2 -0
  25. package/payload/platform/plugins/email/mcp/dist/__tests__/confirm-sent-copy.test.d.ts.map +1 -0
  26. package/payload/platform/plugins/email/mcp/dist/__tests__/confirm-sent-copy.test.js +92 -0
  27. package/payload/platform/plugins/email/mcp/dist/__tests__/confirm-sent-copy.test.js.map +1 -0
  28. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-send.test.js +112 -12
  29. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-send.test.js.map +1 -1
  30. package/payload/platform/plugins/email/mcp/dist/__tests__/email-send-sent-copy.test.d.ts +2 -0
  31. package/payload/platform/plugins/email/mcp/dist/__tests__/email-send-sent-copy.test.d.ts.map +1 -0
  32. package/payload/platform/plugins/email/mcp/dist/__tests__/email-send-sent-copy.test.js +54 -0
  33. package/payload/platform/plugins/email/mcp/dist/__tests__/email-send-sent-copy.test.js.map +1 -0
  34. package/payload/platform/plugins/email/mcp/dist/__tests__/imap-drafts.test.js +170 -1
  35. package/payload/platform/plugins/email/mcp/dist/__tests__/imap-drafts.test.js.map +1 -1
  36. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-note.test.d.ts +2 -0
  37. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-note.test.d.ts.map +1 -0
  38. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-note.test.js +86 -0
  39. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-note.test.js.map +1 -0
  40. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-sweep.test.d.ts +2 -0
  41. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-sweep.test.d.ts.map +1 -0
  42. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-sweep.test.js +164 -0
  43. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-sweep.test.js.map +1 -0
  44. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-ledger.test.d.ts +2 -0
  45. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-ledger.test.d.ts.map +1 -0
  46. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-ledger.test.js +82 -0
  47. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-ledger.test.js.map +1 -0
  48. package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +95 -0
  49. package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts.map +1 -1
  50. package/payload/platform/plugins/email/mcp/dist/lib/imap.js +223 -0
  51. package/payload/platform/plugins/email/mcp/dist/lib/imap.js.map +1 -1
  52. package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-note.d.ts +18 -0
  53. package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-note.d.ts.map +1 -0
  54. package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-note.js +59 -0
  55. package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-note.js.map +1 -0
  56. package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-sweep.d.ts +48 -0
  57. package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-sweep.d.ts.map +1 -0
  58. package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-sweep.js +79 -0
  59. package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-sweep.js.map +1 -0
  60. package/payload/platform/plugins/email/mcp/dist/lib/sent-ledger.d.ts +44 -0
  61. package/payload/platform/plugins/email/mcp/dist/lib/sent-ledger.d.ts.map +1 -0
  62. package/payload/platform/plugins/email/mcp/dist/lib/sent-ledger.js +63 -0
  63. package/payload/platform/plugins/email/mcp/dist/lib/sent-ledger.js.map +1 -0
  64. package/payload/platform/plugins/email/mcp/dist/scripts/sent-copy-sweep.d.ts +2 -0
  65. package/payload/platform/plugins/email/mcp/dist/scripts/sent-copy-sweep.d.ts.map +1 -0
  66. package/payload/platform/plugins/email/mcp/dist/scripts/sent-copy-sweep.js +55 -0
  67. package/payload/platform/plugins/email/mcp/dist/scripts/sent-copy-sweep.js.map +1 -0
  68. package/payload/platform/plugins/email/mcp/dist/tools/email-draft-send.d.ts.map +1 -1
  69. package/payload/platform/plugins/email/mcp/dist/tools/email-draft-send.js +64 -13
  70. package/payload/platform/plugins/email/mcp/dist/tools/email-draft-send.js.map +1 -1
  71. package/payload/platform/plugins/email/mcp/dist/tools/email-reply.d.ts.map +1 -1
  72. package/payload/platform/plugins/email/mcp/dist/tools/email-reply.js +11 -1
  73. package/payload/platform/plugins/email/mcp/dist/tools/email-reply.js.map +1 -1
  74. package/payload/platform/plugins/email/mcp/dist/tools/email-send.d.ts.map +1 -1
  75. package/payload/platform/plugins/email/mcp/dist/tools/email-send.js +16 -3
  76. package/payload/platform/plugins/email/mcp/dist/tools/email-send.js.map +1 -1
  77. package/payload/platform/plugins/email/references/email-reference.md +36 -0
  78. package/payload/platform/plugins/scheduling/mcp/dist/__tests__/sent-sweep-throttle.test.d.ts +2 -0
  79. package/payload/platform/plugins/scheduling/mcp/dist/__tests__/sent-sweep-throttle.test.d.ts.map +1 -0
  80. package/payload/platform/plugins/scheduling/mcp/dist/__tests__/sent-sweep-throttle.test.js +18 -0
  81. package/payload/platform/plugins/scheduling/mcp/dist/__tests__/sent-sweep-throttle.test.js.map +1 -0
  82. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-block-sqlite.test.d.ts +2 -0
  83. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-block-sqlite.test.d.ts.map +1 -0
  84. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-block-sqlite.test.js +150 -0
  85. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-block-sqlite.test.js.map +1 -0
  86. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-block.test.d.ts +2 -0
  87. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-block.test.d.ts.map +1 -0
  88. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-block.test.js +248 -0
  89. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-block.test.js.map +1 -0
  90. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts +90 -0
  91. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts.map +1 -1
  92. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js +105 -0
  93. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js.map +1 -1
  94. package/payload/platform/plugins/scheduling/mcp/dist/lib/sent-sweep-throttle.d.ts +13 -0
  95. package/payload/platform/plugins/scheduling/mcp/dist/lib/sent-sweep-throttle.d.ts.map +1 -0
  96. package/payload/platform/plugins/scheduling/mcp/dist/lib/sent-sweep-throttle.js +10 -0
  97. package/payload/platform/plugins/scheduling/mcp/dist/lib/sent-sweep-throttle.js.map +1 -0
  98. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +53 -0
  99. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
  100. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js +88 -10
  101. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js.map +1 -1
  102. package/payload/platform/scripts/cpu-triage-run.sh +117 -0
  103. package/payload/platform/scripts/cpu-triage.sh +332 -0
  104. package/payload/platform/services/claude-session-manager/dist/activity-range.d.ts +40 -0
  105. package/payload/platform/services/claude-session-manager/dist/activity-range.d.ts.map +1 -0
  106. package/payload/platform/services/claude-session-manager/dist/activity-range.js +68 -0
  107. package/payload/platform/services/claude-session-manager/dist/activity-range.js.map +1 -0
  108. package/payload/platform/services/claude-session-manager/dist/activity-rows.d.ts +40 -8
  109. package/payload/platform/services/claude-session-manager/dist/activity-rows.d.ts.map +1 -1
  110. package/payload/platform/services/claude-session-manager/dist/activity-rows.js +74 -7
  111. package/payload/platform/services/claude-session-manager/dist/activity-rows.js.map +1 -1
  112. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  113. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +2 -0
  114. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  115. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  116. package/payload/platform/services/claude-session-manager/dist/http-server.js +9 -0
  117. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  118. package/payload/server/{chunk-56WJMBQQ.js → chunk-3XLLTG6R.js} +14 -0
  119. package/payload/server/{chunk-LBCMFD4O.js → chunk-XGNRSM57.js} +129 -37
  120. package/payload/server/maxy-edge.js +1 -1
  121. package/payload/server/public/activity.html +6 -6
  122. package/payload/server/public/assets/{AdminLoginScreens-DximwPlS.js → AdminLoginScreens-DR5eLF-a.js} +1 -1
  123. package/payload/server/public/assets/AdminShell-D9zaUxPd.js +2 -0
  124. package/payload/server/public/assets/{Checkbox-DM-eHqOS.js → Checkbox-DcbbAIe5.js} +1 -1
  125. package/payload/server/public/assets/activity-C9P5NjEZ.js +1 -0
  126. package/payload/server/public/assets/{admin-CAkDnGdk.js → admin-D2KLZnQW.js} +1 -1
  127. package/payload/server/public/assets/{browser-DXl8hOoi.js → browser-CDWqEiCD.js} +1 -1
  128. package/payload/server/public/assets/{calendar-DP6hn4-6.js → calendar-BG7fOpbB.js} +1 -1
  129. package/payload/server/public/assets/chat-RyH-WYQC.js +1 -0
  130. package/payload/server/public/assets/chevron-left-DmXCr6nB.js +1 -0
  131. package/payload/server/public/assets/data-BaY_LkLR.js +1 -0
  132. package/payload/server/public/assets/{graph-CHcYoEJ5.js → graph-DRloNic4.js} +1 -1
  133. package/payload/server/public/assets/{graph-labels-CZykslZM.js → graph-labels-DD-GqS4v.js} +1 -1
  134. package/payload/server/public/assets/{maximize-2-31AZEcMS.js → maximize-2-CzDaBugs.js} +1 -1
  135. package/payload/server/public/assets/{operator-BGGpdsO_.js → operator-BZeeyait.js} +1 -1
  136. package/payload/server/public/assets/{page-BloC6ygA.js → page-B0obcg3b.js} +1 -1
  137. package/payload/server/public/assets/{page-DOeiiqbR.js → page-ef-6rRPL.js} +1 -1
  138. package/payload/server/public/assets/{public-CGgO6IZv.js → public-9D3MMOkC.js} +1 -1
  139. package/payload/server/public/assets/{rotate-ccw-BlAi6iFg.js → rotate-ccw-DXEKUhXP.js} +1 -1
  140. package/payload/server/public/assets/tasks-BB0L7e5a.js +1 -0
  141. package/payload/server/public/assets/{time-entry-format-CiUUQdq7.js → time-entry-format-CsYAjplS.js} +1 -1
  142. package/payload/server/public/assets/{triangle-alert-CVqKuLoa.js → triangle-alert-0lSBIUg-.js} +1 -1
  143. package/payload/server/public/assets/{useCopyFeedback-ksyUe_g1.js → useCopyFeedback-jTBQ2h0V.js} +1 -1
  144. package/payload/server/public/assets/{useSelectionMode-BPrBkdTd.js → useSelectionMode-BooArySZ.js} +1 -1
  145. package/payload/server/public/assets/useSubAccountSwitcher-D9knHOKq.css +1 -0
  146. package/payload/server/public/assets/{useVoiceRecorder-CTXP8oTW.js → useVoiceRecorder-PUT8q_sm.js} +1 -1
  147. package/payload/server/public/browser.html +5 -5
  148. package/payload/server/public/calendar.html +6 -6
  149. package/payload/server/public/chat.html +12 -12
  150. package/payload/server/public/data.html +11 -11
  151. package/payload/server/public/graph.html +11 -11
  152. package/payload/server/public/index.html +14 -14
  153. package/payload/server/public/operator.html +14 -14
  154. package/payload/server/public/public.html +12 -12
  155. package/payload/server/public/tasks.html +5 -5
  156. package/payload/server/server.js +325 -77
  157. package/payload/server/{src-JYPKMWJR.js → src-XQ63FPRE.js} +3 -1
  158. package/payload/server/public/assets/AdminShell-BehbH-Oa.js +0 -2
  159. package/payload/server/public/assets/activity-hDwUeQd6.js +0 -1
  160. package/payload/server/public/assets/chat-DhOfVGYM.js +0 -1
  161. package/payload/server/public/assets/chevron-left-DYNBoLii.js +0 -1
  162. package/payload/server/public/assets/data-bq8fTo_s.js +0 -1
  163. package/payload/server/public/assets/tasks-C1giFRRL.js +0 -1
  164. package/payload/server/public/assets/useSubAccountSwitcher-K4kYeN6i.css +0 -1
  165. /package/payload/server/public/assets/{useSubAccountSwitcher-DzgzTRRn.js → useSubAccountSwitcher-BZnzc5C3.js} +0 -0
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env bash
2
+ # cpu-triage-run.sh — detached run harness for cpu-triage.sh.
3
+ #
4
+ # WHY THIS EXISTS
5
+ #
6
+ # cpu-triage.sh samples for at least 120 s (300 s by default) because anything
7
+ # shorter than the reconcile cycle produces contradictory readings — see that
8
+ # script's header. Every admin MCP tool completes inside 12 s. So the run has to
9
+ # be detached from the call that starts it, and the caller needs a readiness
10
+ # signal it can poll.
11
+ #
12
+ # THE ORDERING IS THE CONTRACT
13
+ #
14
+ # stdout is redirected to the result file, so that file exists from the FIRST
15
+ # BYTE of output and is half-written for the whole sampling window. It is
16
+ # therefore NOT a readiness signal. The `.status` marker is, and it is written
17
+ # strictly after the measurement process has exited and its output is closed.
18
+ # The completion log line is written after that again — a post-condition, never
19
+ # an intent. A reader that keys on the `.json` existing reports a half-written
20
+ # run as complete, which is the single failure this harness exists to prevent.
21
+ #
22
+ # EXIT CODES
23
+ # The measurement script's own exit code is recorded in the status marker and
24
+ # is what callers interpret (0 clean, 1 sustained hot core, 2 precondition
25
+ # failure). This harness exits 2 only on its own usage errors.
26
+
27
+ set -uo pipefail
28
+
29
+ RUN_ID=""
30
+ OUT_DIR=""
31
+ LOG_FILE=""
32
+ SCRIPT=""
33
+
34
+ usage() {
35
+ cat >&2 <<USAGE
36
+ usage: cpu-triage-run.sh --run-id ID --out-dir DIR --log FILE [--script PATH]
37
+ [-- <cpu-triage.sh args>...]
38
+
39
+ --run-id identifier for this run; names the output files
40
+ --out-dir directory receiving cpu-triage-<ID>.json and cpu-triage-<ID>.status
41
+ --log brand log to append the op=complete line to
42
+ --script measurement script (default: cpu-triage.sh beside this file)
43
+ USAGE
44
+ exit 2
45
+ }
46
+
47
+ while [ $# -gt 0 ]; do
48
+ case "$1" in
49
+ --run-id) RUN_ID="${2:-}"; shift 2 ;;
50
+ --out-dir) OUT_DIR="${2:-}"; shift 2 ;;
51
+ --log) LOG_FILE="${2:-}"; shift 2 ;;
52
+ --script) SCRIPT="${2:-}"; shift 2 ;;
53
+ --) shift; break ;;
54
+ -h|--help) usage ;;
55
+ *) echo "cpu-triage-run: unknown argument '$1'" >&2; usage ;;
56
+ esac
57
+ done
58
+
59
+ [ -n "$RUN_ID" ] || usage
60
+ [ -n "$OUT_DIR" ] || usage
61
+ [ -n "$LOG_FILE" ] || usage
62
+
63
+ case "$RUN_ID" in
64
+ *[!A-Za-z0-9T_-]*) echo "cpu-triage-run: run-id must be [A-Za-z0-9T_-]" >&2; exit 2 ;;
65
+ esac
66
+
67
+ HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
68
+ [ -n "$SCRIPT" ] || SCRIPT="$HERE/cpu-triage.sh"
69
+
70
+ mkdir -p "$OUT_DIR" || exit 2
71
+
72
+ RESULT="$OUT_DIR/cpu-triage-$RUN_ID.json"
73
+ STATUS="$OUT_DIR/cpu-triage-$RUN_ID.status"
74
+
75
+ # Publish a terminal state. EVERY exit path past this point goes through here.
76
+ #
77
+ # The caller has already written its meta file and logged op=start by the time
78
+ # this harness runs, so an exit that publishes nothing leaves the run readable
79
+ # as `running` forever — the no-event blind spot this whole design exists to
80
+ # close. The case that made this concrete: the measurement script shipping
81
+ # without its executable bit. The caller checks the script EXISTS, which passes,
82
+ # and the run then dies here in silence.
83
+ publish() {
84
+ local code="$1" hot
85
+ { echo "exit=$code"; [ -n "${2:-}" ] && printf '%s\n' "$2"; } > "$STATUS"
86
+
87
+ # Report what is on disk. An absent or unparseable count is `unknown`, never
88
+ # a fabricated 0 — reporting a clean count for a run that produced no report
89
+ # is exactly the false-clean this tool exists to prevent.
90
+ hot=$(grep -o '"sustainedHotCores"[[:space:]]*:[[:space:]]*[0-9]\{1,\}' "$RESULT" 2>/dev/null \
91
+ | head -1 | grep -o '[0-9]\{1,\}$')
92
+ [ -n "$hot" ] || hot="unknown"
93
+
94
+ mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null
95
+ printf '%s [cpu-triage] op=complete runId=%s exit=%s sustainedHotCores=%s\n' \
96
+ "$(date -Is 2>/dev/null || date)" "$RUN_ID" "$code" "$hot" >> "$LOG_FILE"
97
+ }
98
+
99
+ if [ ! -x "$SCRIPT" ]; then
100
+ MSG="cpu-triage-run: measurement script not executable: $SCRIPT"
101
+ echo "$MSG" >&2
102
+ publish 2 "$MSG"
103
+ exit 0
104
+ fi
105
+
106
+ ERR=$(mktemp "${TMPDIR:-/tmp}/cpu-triage-err.XXXXXX") || exit 2
107
+ trap 'rm -f "$ERR"' EXIT INT TERM
108
+
109
+ # The measurement. stdout streams into the result file for the whole window.
110
+ "$SCRIPT" --json "$@" > "$RESULT" 2> "$ERR"
111
+ CODE=$?
112
+
113
+ # The result file is closed and the exit code is known. Only now does a
114
+ # readiness signal exist to publish.
115
+ publish "$CODE" "$(cat "$ERR")"
116
+
117
+ exit 0
@@ -0,0 +1,332 @@
1
+ #!/usr/bin/env bash
2
+ # cpu-triage.sh — sustained CPU triage for a Maxy Code device.
3
+ #
4
+ # WHY THIS EXISTS, AND WHY IT SAMPLES FOR MINUTES RATHER THAN SECONDS
5
+ #
6
+ # The booking reconcile loop runs on a 120 s cycle and a tick costs roughly
7
+ # 800 ms of network wait plus a garbage-collection burst. Measured on laptop
8
+ # 192.168.88.10 (2026-07-20), the SAME server process over the SAME method read
9
+ # 0.2%, 0.2%, 0.2% in three consecutive 25 s windows and 10.8% in a 20 s window.
10
+ # Every one of those readings was correct. Any sample shorter than the period
11
+ # either lands on a tick or misses it, so spot checks on this platform produce
12
+ # contradictory answers indefinitely. This tool therefore samples across at
13
+ # least one full cycle and reports a DISTRIBUTION (mean, peak, and what share of
14
+ # the window sat above threshold), never a single number.
15
+ #
16
+ # THREE INSTRUMENTS THIS DELIBERATELY DOES NOT USE, EACH HAVING PRODUCED A FALSE
17
+ # CONCLUSION IN THE 2026-07-20 INVESTIGATION:
18
+ # * `ps %cpu` / `top` %CPU — LIFETIME averages. A 40-minute-old process read
19
+ # 8.5% against an 8-day-old peer at 0.1%; both were actually 0.2% live. This
20
+ # nearly shipped as an "85x regression".
21
+ # * `iostat -c` first line — a SINCE-BOOT average. Read as live it reported a
22
+ # machine "42% busy" that was 87% idle at that moment.
23
+ # * `ns_last_pid` deltas — counts THREAD creation as well as forks, and was
24
+ # compared against a single 10 s sample used as a "baseline". Produced a
25
+ # false 5.6x churn regression. Fork rate here comes from /proc/stat's
26
+ # `processes` counter, which counts forks only.
27
+ # Everything below is a DELTA over a stated window. No lifetime averages.
28
+ #
29
+ # Aggregate CPU is also avoided as a headline: two hot cores out of twenty
30
+ # average down to ~3% and vanish, which is exactly how a real, operator-visible
31
+ # condition was dismissed as "nothing changed". Per-core is the primary view.
32
+ #
33
+ # Unprivileged by design: the admin assistant runs without sudo, so this uses
34
+ # only /proc and the user's own cgroup tree. `perf` and `bpftrace` need root and
35
+ # are named in the escalation hint rather than invoked.
36
+ #
37
+ # EXIT CODES (gates, for deterministic callers)
38
+ # 0 no core sustained above threshold — nothing to escalate
39
+ # 1 one or more cores sustained above threshold — findings in the report
40
+ # 2 usage error or unmet precondition (nothing measured)
41
+
42
+ set -uo pipefail
43
+
44
+ WINDOW=300 # total sampling window, seconds (>= 2 reconcile cycles)
45
+ INTERVAL=10 # sample cadence, seconds
46
+ THRESHOLD=15 # per-core busy % that counts as "hot"
47
+ SUSTAIN=50 # % of samples above THRESHOLD to call a core "sustained"
48
+ TOPN=12 # processes to report
49
+ JSON=0
50
+ CONTROL="" # optional peer brand known NOT to have the change
51
+
52
+ usage() {
53
+ cat >&2 <<USAGE
54
+ usage: cpu-triage.sh [--window S] [--interval S] [--threshold PCT]
55
+ [--sustain PCT] [--top N] [--control BRAND] [--json]
56
+
57
+ --window total sampling window in seconds (default 300, min 120)
58
+ --interval sample cadence in seconds (default 10)
59
+ --threshold per-core busy %% treated as hot (default 15)
60
+ --sustain %% of samples above threshold to call it sustained (default 50)
61
+ --top processes to report (default 12)
62
+ --control a brand on the PREVIOUS build, for like-for-like comparison.
63
+ The single most valuable input: on 2026-07-20 an un-upgraded
64
+ peer collapsed a suspected regression into a measurement artifact.
65
+ --json emit machine-readable JSON instead of the text report
66
+ USAGE
67
+ exit 2
68
+ }
69
+
70
+ while [ $# -gt 0 ]; do
71
+ case "$1" in
72
+ --window) WINDOW="${2:-}"; shift 2 ;;
73
+ --interval) INTERVAL="${2:-}"; shift 2 ;;
74
+ --threshold) THRESHOLD="${2:-}"; shift 2 ;;
75
+ --sustain) SUSTAIN="${2:-}"; shift 2 ;;
76
+ --top) TOPN="${2:-}"; shift 2 ;;
77
+ --control) CONTROL="${2:-}"; shift 2 ;;
78
+ --json) JSON=1; shift ;;
79
+ -h|--help) usage ;;
80
+ *) echo "cpu-triage: unknown argument '$1'" >&2; usage ;;
81
+ esac
82
+ done
83
+
84
+ case "$WINDOW$INTERVAL$THRESHOLD$SUSTAIN$TOPN" in *[!0-9]*) usage ;; esac
85
+ [ "$INTERVAL" -ge 1 ] || usage
86
+ if [ "$WINDOW" -lt 120 ]; then
87
+ echo "cpu-triage: refusing a window under 120s — shorter than the reconcile cycle," >&2
88
+ echo " which is what makes spot readings contradict each other." >&2
89
+ exit 2
90
+ fi
91
+ [ -r /proc/stat ] || { echo "cpu-triage: /proc/stat unreadable — Linux only" >&2; exit 2; }
92
+
93
+ SAMPLES=$(( WINDOW / INTERVAL ))
94
+ [ "$SAMPLES" -ge 2 ] || { echo "cpu-triage: window/interval must yield >= 2 samples" >&2; exit 2; }
95
+
96
+ CLK=$(getconf CLK_TCK 2>/dev/null || echo 100)
97
+ TMP=$(mktemp -d "${TMPDIR:-/tmp}/cpu-triage.XXXXXX") || exit 2
98
+ trap 'rm -rf "$TMP"' EXIT INT TERM
99
+
100
+ CG_BASE="/sys/fs/cgroup/user.slice/user-$(id -u).slice/user@$(id -u).service/app.slice"
101
+
102
+ # ---------------------------------------------------------------------------
103
+ # Sampling. One loop, one cadence, every measure derived from the same instants
104
+ # so per-core, per-process and per-service figures are directly comparable.
105
+ # ---------------------------------------------------------------------------
106
+ snapshot() {
107
+ local idx="$1"
108
+ # NOTE: every redirect below MUST be `>>`. Each sample is a fresh awk process,
109
+ # and awk's `>` truncates the file once per invocation — so `>` silently keeps
110
+ # only the LAST sample, leaving nothing to diff. That bug shipped into the
111
+ # first test run and produced an empty per-core report with mean 0%.
112
+ awk -v idx="$idx" -v out="$TMP" '
113
+ FILENAME == "/proc/stat" {
114
+ if ($1 ~ /^cpu[0-9]+$/) {
115
+ total = 0
116
+ for (i = 2; i <= NF; i++) total += $i
117
+ # busy excludes idle (field 5) and iowait (field 6)
118
+ print idx, $1, total - $5 - $6, total >> (out "/cores.raw")
119
+ }
120
+ if ($1 == "processes") print idx, $2 >> (out "/forks.raw")
121
+ next
122
+ }
123
+ # /proc/<pid>/stat — comm may contain spaces and parens; the greedy .*\)
124
+ # anchors on the LAST close paren, which is the only safe split.
125
+ {
126
+ if (match($0, /^[0-9]+ \(.*\) /)) {
127
+ pid = $1
128
+ rest = substr($0, RSTART + RLENGTH)
129
+ n = split(rest, f, " ")
130
+ if (n >= 13) print idx, pid, f[12] + f[13] >> (out "/procs.raw")
131
+ }
132
+ }
133
+ ' /proc/stat /proc/[0-9]*/stat 2>/dev/null
134
+
135
+ # Per-service cgroup CPU. Counts CHILD processes, which per-process sampling
136
+ # attributes nowhere — the original wrangler churn lived entirely in children.
137
+ #
138
+ # Read every cpu.stat in ONE awk rather than a shell loop forking per service.
139
+ # This tool reports fork rate, so its own churn is measurement noise it would
140
+ # otherwise attribute to the box: the loop form cost ~16 forks per sample.
141
+ if [ -d "$CG_BASE" ]; then
142
+ awk -v idx="$idx" '
143
+ /^usage_usec/ {
144
+ n = split(FILENAME, p, "/")
145
+ svc = p[n-1]; sub(/\.service$/, "", svc)
146
+ print idx, svc, $2
147
+ }
148
+ ' "$CG_BASE"/*.service/cpu.stat >> "$TMP/cgroup.raw" 2>/dev/null
149
+ fi
150
+ }
151
+
152
+ HOST=$(hostname 2>/dev/null || echo unknown)
153
+ NCPU=$(awk '/^cpu[0-9]+/{n++} END{print n+0}' /proc/stat)
154
+ STARTED=$(date -Is 2>/dev/null || date)
155
+
156
+ if [ "$JSON" -eq 0 ]; then
157
+ echo "cpu-triage on $HOST — sampling ${WINDOW}s at ${INTERVAL}s cadence (${SAMPLES} samples, ${NCPU} cores)"
158
+ echo "this spans at least one 120s reconcile cycle; spot readings on this platform are not reliable"
159
+ echo
160
+ fi
161
+
162
+ i=0
163
+ while [ "$i" -lt "$SAMPLES" ]; do
164
+ snapshot "$i"
165
+ i=$(( i + 1 ))
166
+ [ "$i" -lt "$SAMPLES" ] && sleep "$INTERVAL"
167
+ done
168
+ ENDED=$(date -Is 2>/dev/null || date)
169
+
170
+ [ -s "$TMP/cores.raw" ] || { echo "cpu-triage: no samples captured" >&2; exit 2; }
171
+
172
+ # ---------------------------------------------------------------------------
173
+ # Analysis — distribution per core, not a single number.
174
+ # ---------------------------------------------------------------------------
175
+ awk -v thr="$THRESHOLD" '
176
+ { key = $2
177
+ if (prev_total[key] != "") {
178
+ dt = $4 - prev_total[key]
179
+ db = $3 - prev_busy[key]
180
+ if (dt > 0) {
181
+ pct = db / dt * 100
182
+ sum[key] += pct; n[key]++
183
+ if (pct > peak[key]) peak[key] = pct
184
+ if (pct > thr) hot[key]++
185
+ }
186
+ }
187
+ prev_total[key] = $4; prev_busy[key] = $3
188
+ }
189
+ END {
190
+ for (k in sum)
191
+ printf "%s %.2f %.2f %.1f %d\n", k, sum[k]/n[k], peak[k], (hot[k]+0)/n[k]*100, n[k]
192
+ }
193
+ ' "$TMP/cores.raw" | sort -k2 -rn > "$TMP/cores.out"
194
+
195
+ awk -v clk="$CLK" -v iv="$INTERVAL" '
196
+ { key = $2
197
+ if (prev[key] != "") {
198
+ d = $3 - prev[key]
199
+ if (d >= 0) {
200
+ pct = d / (clk * iv) * 100
201
+ sum[key] += pct; n[key]++
202
+ if (pct > peak[key]) peak[key] = pct
203
+ }
204
+ }
205
+ prev[key] = $3
206
+ }
207
+ END { for (k in sum) if (sum[k]/n[k] >= 0.05) printf "%s %.2f %.2f\n", k, sum[k]/n[k], peak[k] }
208
+ ' "$TMP/procs.raw" | sort -k2 -rn > "$TMP/procs.out"
209
+
210
+ if [ -s "${TMP}/cgroup.raw" ]; then
211
+ awk -v iv="$INTERVAL" '
212
+ { key = $2
213
+ if (prev[key] != "") {
214
+ d = $3 - prev[key]
215
+ if (d >= 0) { pct = d / (iv * 1000000) * 100; sum[key] += pct; n[key]++
216
+ if (pct > peak[key]) peak[key] = pct }
217
+ }
218
+ prev[key] = $3
219
+ }
220
+ END { for (k in sum) printf "%s %.2f %.2f\n", k, sum[k]/n[k], peak[k] }
221
+ ' "$TMP/cgroup.raw" | sort -k2 -rn > "$TMP/cgroup.out"
222
+ fi
223
+
224
+ FORKS_MIN=$(awk -v iv="$INTERVAL" 'NR==1{f=$2;i=$1} END{ if (NR>1) printf "%.0f", ($2-f)/((($1-i)*iv))*60 }' "$TMP/forks.raw" 2>/dev/null)
225
+ [ -n "$FORKS_MIN" ] || FORKS_MIN=0
226
+
227
+ HOT_COUNT=$(awk -v s="$SUSTAIN" '$4 >= s {n++} END{print n+0}' "$TMP/cores.out")
228
+ MEAN_CORE=$(awk '{s+=$2; n++} END{ if(n) printf "%.2f", s/n; else print "0" }' "$TMP/cores.out")
229
+
230
+ resolve() { tr '\0' ' ' < "/proc/$1/cmdline" 2>/dev/null | cut -c1-78 || true; }
231
+
232
+ # Control lookup, computed once for BOTH report shapes. It previously lived
233
+ # inside the text branch only, which made --control silently inert under
234
+ # --json — dropping the single most valuable input to this measurement from
235
+ # the machine-readable output the agent reads.
236
+ CONTROL_MEAN=""
237
+ if [ -n "$CONTROL" ] && [ -s "${TMP}/cgroup.out" ]; then
238
+ CONTROL_MEAN=$(awk -v c="$CONTROL" '$1 == c {print $2}' "$TMP/cgroup.out")
239
+ fi
240
+
241
+ # ---------------------------------------------------------------------------
242
+ # Report
243
+ # ---------------------------------------------------------------------------
244
+ if [ "$JSON" -eq 1 ]; then
245
+ printf '{\n "host": %s,\n "started": %s,\n "ended": %s,\n' "\"$HOST\"" "\"$STARTED\"" "\"$ENDED\""
246
+ printf ' "windowSec": %s, "intervalSec": %s, "samples": %s, "cores": %s,\n' "$WINDOW" "$INTERVAL" "$SAMPLES" "$NCPU"
247
+ printf ' "thresholdPct": %s, "sustainPct": %s,\n' "$THRESHOLD" "$SUSTAIN"
248
+ printf ' "meanPerCorePct": %s, "sustainedHotCores": %s, "forksPerMin": %s,\n' "$MEAN_CORE" "$HOT_COUNT" "$FORKS_MIN"
249
+ printf ' "perCore": ['
250
+ awk '{ printf "%s{\"cpu\":\"%s\",\"meanPct\":%s,\"peakPct\":%s,\"aboveThresholdPct\":%s}", (NR>1?",":""), $1,$2,$3,$4 }' "$TMP/cores.out"
251
+ printf '],\n "topProcesses": ['
252
+ head -n "$TOPN" "$TMP/procs.out" | while read -r pid mean peak; do
253
+ cl=$(resolve "$pid" | sed 's/\\/\\\\/g; s/"/\\"/g')
254
+ printf '{"pid":%s,"meanPct":%s,"peakPct":%s,"cmd":"%s"},' "$pid" "$mean" "$peak" "$cl"
255
+ done | sed 's/,$//'
256
+ printf '],\n "perService": ['
257
+ if [ -s "${TMP}/cgroup.out" ]; then
258
+ awk '{ printf "%s{\"service\":\"%s\",\"meanPct\":%s,\"peakPct\":%s}", (NR>1?",":""), $1,$2,$3 }' "$TMP/cgroup.out"
259
+ fi
260
+ printf '],\n "control": '
261
+ if [ -z "$CONTROL" ]; then
262
+ printf 'null'
263
+ else
264
+ cesc=$(printf '%s' "$CONTROL" | sed 's/\\/\\\\/g; s/"/\\"/g')
265
+ if [ -n "$CONTROL_MEAN" ]; then
266
+ printf '{"brand":"%s","meanPct":%s,"found":true}' "$cesc" "$CONTROL_MEAN"
267
+ else
268
+ printf '{"brand":"%s","found":false}' "$cesc"
269
+ fi
270
+ fi
271
+ printf '\n}\n'
272
+ else
273
+ echo "PER-CORE over the window (mean / peak / share of window above ${THRESHOLD}%)"
274
+ echo " a core is 'sustained' when it exceeds threshold in >= ${SUSTAIN}% of samples"
275
+ awk -v thr="$THRESHOLD" -v s="$SUSTAIN" '
276
+ $2 >= 1 || $4 > 0 {
277
+ flag = ($4 >= s) ? " <== SUSTAINED" : ""
278
+ printf " %-7s mean=%6.2f%% peak=%6.2f%% above=%5.1f%% of window%s\n", $1, $2, $3, $4, flag
279
+ }' "$TMP/cores.out"
280
+ echo " ---"
281
+ printf " mean per-core %s%% sustained hot cores: %s of %s forks %s/min\n\n" "$MEAN_CORE" "$HOT_COUNT" "$NCPU" "$FORKS_MIN"
282
+
283
+ echo "TOP PROCESSES by measured delta (100% = one full core; NOT a lifetime average)"
284
+ head -n "$TOPN" "$TMP/procs.out" | while read -r pid mean peak; do
285
+ printf " %6.2f%% mean %6.2f%% peak pid=%-8s %s\n" "$mean" "$peak" "$pid" "$(resolve "$pid")"
286
+ done
287
+ echo
288
+
289
+ if [ -s "${TMP}/cgroup.out" ]; then
290
+ echo "PER-SERVICE cgroup CPU (child processes included — the platform's true footprint)"
291
+ tot=$(awk '{s+=$2} END{printf "%.2f", s}' "$TMP/cgroup.out")
292
+ awk '$2 >= 0.01 { printf " %-32s mean=%6.2f%% peak=%6.2f%%\n", $1, $2, $3 }' "$TMP/cgroup.out"
293
+ printf " ---\n all services %s%% of one core (%.2f%% of the %s-core box)\n\n" \
294
+ "$tot" "$(awk -v t="$tot" -v c="$NCPU" 'BEGIN{print t/c}')" "$NCPU"
295
+ if [ -n "$CONTROL" ]; then
296
+ if [ -n "$CONTROL_MEAN" ]; then
297
+ echo "CONTROL: '$CONTROL' is on the previous build and measured ${CONTROL_MEAN}% of one core."
298
+ echo " Compare peers against this, not against their own past readings. A peer that"
299
+ echo " did not get the change is worth more than any before/after inference."
300
+ echo
301
+ else
302
+ echo "CONTROL: '$CONTROL' not found among running services — comparison skipped."; echo
303
+ fi
304
+ fi
305
+ fi
306
+
307
+ if command -v sar >/dev/null 2>&1; then
308
+ y=$(date -d yesterday +%d 2>/dev/null)
309
+ if [ -n "$y" ] && [ -r "/var/log/sysstat/sa$y" ]; then
310
+ echo "HISTORY — same instrument, yesterday, for like-for-like regression checking"
311
+ sar -P ALL -f "/var/log/sysstat/sa$y" 2>/dev/null | awk -v thr="$THRESHOLD" '
312
+ /Average/ && $2 ~ /^[0-9]+$/ { b = 100 - $8; s += b; n++; if (b > thr) h++ }
313
+ END { if (n) printf " yesterday: mean per-core %.2f%% cores above %s%%: %d/%d\n", s/n, thr, h+0, n }'
314
+ echo
315
+ fi
316
+ fi
317
+
318
+ if [ "$HOT_COUNT" -gt 0 ]; then
319
+ echo "VERDICT: ${HOT_COUNT} core(s) sustained above ${THRESHOLD}%. Findings above."
320
+ echo " Before calling it a regression, check the desktop stack in the process list."
321
+ echo " On 2026-07-20 gnome-shell + gnome-system-monitor + Xorg accounted for ~41% of"
322
+ echo " a core, roughly three times the entire four-brand platform, and the System"
323
+ echo " Monitor window doing the observing was itself a third of that."
324
+ echo " To attribute short-lived process churn, escalate (needs root):"
325
+ echo " bpftrace -e 'tracepoint:syscalls:sys_enter_execve { @[comm, str(args->filename)] = count(); }'"
326
+ else
327
+ echo "VERDICT: no core sustained above ${THRESHOLD}%."
328
+ fi
329
+ fi
330
+
331
+ [ "$HOT_COUNT" -gt 0 ] && exit 1
332
+ exit 0
@@ -0,0 +1,40 @@
1
+ import type { MeteringDay } from './session-metering.js';
2
+ import { type PricingConfig } from './pricing.js';
3
+ export interface DayRange {
4
+ from: string;
5
+ to: string;
6
+ }
7
+ /** The outcome of reading `from`/`to` off a request.
8
+ *
9
+ * `fullRangeFallback` separates the two ways `range` comes back null, which
10
+ * are NOT the same event:
11
+ *
12
+ * - false — no range was asked for (the All time preset). Legitimate, and
13
+ * the figures are honestly labelled all-time.
14
+ * - true — a range WAS asked for and could not be honoured. This is the
15
+ * most misleading state available, because the page then shows
16
+ * all-time figures under a bounded heading. */
17
+ export interface ParsedRange {
18
+ range: DayRange | null;
19
+ fullRangeFallback: boolean;
20
+ }
21
+ export declare function parseDayRange(from: unknown, to: unknown): ParsedRange;
22
+ export interface RangeSlice {
23
+ activeMs: number;
24
+ tokens: number;
25
+ costGBP: {
26
+ opus: number;
27
+ sonnet: number;
28
+ };
29
+ /** Days falling inside the range. 0 is a real answer — a quiet month — and is
30
+ * what the caller reports as an empty slice. */
31
+ matchedDays: number;
32
+ }
33
+ /** Sum a session's day rows over an inclusive range. A null range means every
34
+ * day.
35
+ *
36
+ * Cost is re-derived from the SUMMED tokens rather than summed from the
37
+ * per-day costs, matching how computeMetering derives its own totals — adding
38
+ * already-rounded figures compounds their rounding. */
39
+ export declare function sliceMeteringDays(days: MeteringDay[], range: DayRange | null, cfg?: PricingConfig): RangeSlice;
40
+ //# sourceMappingURL=activity-range.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activity-range.d.ts","sourceRoot":"","sources":["../src/activity-range.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,EAKL,KAAK,aAAa,EACnB,MAAM,cAAc,CAAA;AAErB,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,MAAM,CAAA;CACX;AAED;;;;;;;;;4DAS4D;AAC5D,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,QAAQ,GAAG,IAAI,CAAA;IACtB,iBAAiB,EAAE,OAAO,CAAA;CAC3B;AAgBD,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,GAAG,WAAW,CAQrE;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;IACzC;qDACiD;IACjD,WAAW,EAAE,MAAM,CAAA;CACpB;AAMD;;;;;wDAKwD;AACxD,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,WAAW,EAAE,EACnB,KAAK,EAAE,QAAQ,GAAG,IAAI,EACtB,GAAG,GAAE,aAAsC,GAC1C,UAAU,CAqBZ"}
@@ -0,0 +1,68 @@
1
+ // Task 1827 — the date range applied to /activity's metering figures.
2
+ //
3
+ // Pure by construction, and separate from activity-rows.ts for the reason that
4
+ // module already states about itself: the arithmetic has to be provable without
5
+ // a Pi, a transcript or a live session.
6
+ //
7
+ // The range is a pair of INCLUSIVE UTC day keys in the same `YYYY-MM-DD` shape
8
+ // as `MeteringDay.date` (see `dayKey`, session-metering.ts). Slicing is
9
+ // therefore a string comparison, and the client sends day keys rather than
10
+ // instants — so the two ends cannot disagree about where a UTC day begins.
11
+ import { defaultPricingConfig, opusCostGBP, sonnetCostGBP, } from './pricing.js';
12
+ /** True for a real calendar day in `YYYY-MM-DD`.
13
+ *
14
+ * Rejects shapes `Date.parse` would silently accept (`2026-6-1`) and days that
15
+ * do not exist (`2026-02-30`). Both would otherwise slice against a key no
16
+ * MeteringDay can ever carry, producing a silent empty range — the failure
17
+ * this whole module is shaped to make impossible. */
18
+ function isDayKey(v) {
19
+ if (typeof v !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(v))
20
+ return false;
21
+ const t = Date.parse(`${v}T00:00:00.000Z`);
22
+ if (!Number.isFinite(t))
23
+ return false;
24
+ // Round-trips only if the day exists: Date normalises 2026-02-30 to 2026-03-02.
25
+ return new Date(t).toISOString().slice(0, 10) === v;
26
+ }
27
+ export function parseDayRange(from, to) {
28
+ const absent = (v) => v === undefined || v === null || v === '';
29
+ if (absent(from) && absent(to))
30
+ return { range: null, fullRangeFallback: false };
31
+ // Half a range is a rejected range. Inferring the missing end would invent an
32
+ // operator intent and render figures under a heading nobody chose.
33
+ if (!isDayKey(from) || !isDayKey(to))
34
+ return { range: null, fullRangeFallback: true };
35
+ if (from > to)
36
+ return { range: null, fullRangeFallback: true };
37
+ return { range: { from, to }, fullRangeFallback: false };
38
+ }
39
+ function zeroTokens() {
40
+ return { output: 0, input: 0, cacheWrite: 0, cacheRead: 0 };
41
+ }
42
+ /** Sum a session's day rows over an inclusive range. A null range means every
43
+ * day.
44
+ *
45
+ * Cost is re-derived from the SUMMED tokens rather than summed from the
46
+ * per-day costs, matching how computeMetering derives its own totals — adding
47
+ * already-rounded figures compounds their rounding. */
48
+ export function sliceMeteringDays(days, range, cfg = defaultPricingConfig()) {
49
+ const inRange = range === null
50
+ ? days
51
+ : days.filter((d) => d.date >= range.from && d.date <= range.to);
52
+ let activeMs = 0;
53
+ const tokens = zeroTokens();
54
+ for (const d of inRange) {
55
+ activeMs += d.activeMs.thinking + d.activeMs.messaging + d.activeMs.toolUse;
56
+ tokens.output += d.tokens.output;
57
+ tokens.input += d.tokens.input;
58
+ tokens.cacheWrite += d.tokens.cacheWrite;
59
+ tokens.cacheRead += d.tokens.cacheRead;
60
+ }
61
+ return {
62
+ activeMs,
63
+ tokens: tokens.output + tokens.input + tokens.cacheWrite + tokens.cacheRead,
64
+ costGBP: { opus: opusCostGBP(tokens, cfg), sonnet: sonnetCostGBP(tokens, cfg) },
65
+ matchedDays: inRange.length,
66
+ };
67
+ }
68
+ //# sourceMappingURL=activity-range.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activity-range.js","sourceRoot":"","sources":["../src/activity-range.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,5 @@
1
+ import { type MeteringDay } from './session-metering.js';
2
+ import { type DayRange } from './activity-range.js';
1
3
  import type { CensusSession, PtyCensusSnapshot } from './pty-census.js';
2
4
  import type { FsWatcher } from './fs-watcher.js';
3
5
  import type { Logger } from './pty-census.js';
@@ -24,15 +26,17 @@ export interface ActivityEnriched {
24
26
  turnCount: number;
25
27
  /** True when the tail parse was clipped, making turnCount a lower bound. */
26
28
  capped: boolean;
27
- usedTokens: number | null;
28
29
  }
29
- /** The metering half of a row. `costGBP` carries BOTH price assumptions
30
- * because that is what MeteringSummary produces; the join picks the arm. */
30
+ /** The metering half of a row.
31
+ *
32
+ * Carries the DAY ROWS, not a pre-summed cost. The range is applied per
33
+ * request while the parse is memoised per transcript, so changing the range is
34
+ * a filter over data already in hand rather than a re-parse of every live
35
+ * transcript on the box (Task 1827). Each day still prices its own tokens under
36
+ * BOTH assumptions, because that is what MeteringSummary produces; the join
37
+ * picks the arm. */
31
38
  export interface ActivityMetering {
32
- costGBP: {
33
- opus: number;
34
- sonnet: number;
35
- } | null;
39
+ days: MeteringDay[] | null;
36
40
  error: string | null;
37
41
  }
38
42
  /** Why a row has no cost. Three distinct facts that would otherwise render as
@@ -76,7 +80,23 @@ export interface ActivityRow {
76
80
  rssKb: number | null;
77
81
  /** Null when unmeasured. Never 0 to mean idle. See CensusSession.cpuPct. */
78
82
  cpuPct: number | null;
79
- usedTokens: number | null;
83
+ /** Active time inside the requested range: thinking + messaging + toolUse
84
+ * summed over the range's MeteringDay entries.
85
+ *
86
+ * This is WORKED time. `ageMs` beside it is wall-clock since the first turn
87
+ * and is dominated by idle human-wait, which is why the page showed `2d 17h`
88
+ * for a session that had done `1h 35m` of work.
89
+ *
90
+ * Null ONLY when metering itself failed. A range containing no days is 0 —
91
+ * a measured, genuinely quiet period — and the two must never collapse: the
92
+ * page renders a dash for the first and `0h 0m` for the second (Task 1827). */
93
+ activeMsInRange: number | null;
94
+ /** Tokens billed inside the range, summed over MeteringDay.tokens.
95
+ *
96
+ * REPLACES `usedTokens`, which was the live context-window snapshot — a
97
+ * point-in-time reading that no date range can bound. Same null rule as
98
+ * activeMsInRange. */
99
+ tokensInRange: number | null;
80
100
  /** The costGBP arm matching this row's own model, in GBP. Null when the
81
101
  * model is unknown, maps to neither priced family, or metering failed. */
82
102
  costGbp: number | null;
@@ -138,6 +158,8 @@ export declare function buildActivityRows(input: {
138
158
  metering: Map<string, ActivityMetering>;
139
159
  enrichErrors?: Map<string, string>;
140
160
  nowMs: number;
161
+ /** Inclusive UTC day range for the metering figures, or null for every day. */
162
+ range: DayRange | null;
141
163
  }): ActivityRow[];
142
164
  /** Fixed-size worker pool. Bounds the metering fan-out: computeMetering is a
143
165
  * FULL JSONL parse per session, so an unbounded Promise.all over a busy box
@@ -163,6 +185,12 @@ export interface CollectActivityDeps {
163
185
  nowMs?: number;
164
186
  /** Bound on concurrent full-JSONL metering parses. */
165
187
  meteringConcurrency?: number;
188
+ /** Inclusive UTC day range for the metering figures, or null for all time. */
189
+ range?: DayRange | null;
190
+ /** True when a range WAS requested and could not be honoured. Carried through
191
+ * so the response and the log can both say the page is showing all-time
192
+ * figures under a bounded heading. */
193
+ fullRangeFallback?: boolean;
166
194
  }
167
195
  export interface ActivityResult {
168
196
  rows: ActivityRow[];
@@ -171,6 +199,10 @@ export interface ActivityResult {
171
199
  live: number;
172
200
  cpuNull: number;
173
201
  } | null;
202
+ /** The range actually applied, echoed so the page can label its own figures
203
+ * with what it got rather than with what it asked for. */
204
+ range: DayRange | null;
205
+ fullRangeFallback: boolean;
174
206
  }
175
207
  /** Test seam — the memo is module state, so a suite asserting parse counts
176
208
  * needs to clear it between cases. */