ccdeck 3.36.4 → 3.36.6

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.
@@ -64,7 +64,7 @@
64
64
  stored === "light" || (stored !== "dark" && light) ? "light" : "dark");
65
65
  })();
66
66
  </script>
67
- <script type="module" crossorigin src="/assets/index-Cb31GACQ.js"></script>
67
+ <script type="module" crossorigin src="/assets/index-B2C4N7Bl.js"></script>
68
68
  <link rel="stylesheet" crossorigin href="/assets/index-BkXTN4dZ.css">
69
69
  </head>
70
70
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccdeck",
3
- "version": "3.36.4",
3
+ "version": "3.36.6",
4
4
  "description": "Local dashboard for Claude Code and Codex CLI sessions: which Claude Code session needs input, every tool call and Claude Code subagent live, token cost and quota, several Claude accounts. Run npx ccdeck.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -63,7 +63,9 @@
63
63
  "3.35.1",
64
64
  "3.36.0",
65
65
  "3.36.1",
66
- "3.36.4"
66
+ "3.36.4",
67
+ "3.36.5",
68
+ "3.36.6"
67
69
  ],
68
70
  "3.36.3": [
69
71
  {
@@ -46,6 +46,7 @@ import { DENIED, foreignPath, quarantinePath, setAsideForeign } from "./prefs-se
46
46
  import { deckDataDir } from "./deck-home.mjs";
47
47
  import { PRODUCT } from "./brand.mjs";
48
48
  import { normaliseOrigins } from "./account-origins.mjs";
49
+ import { normaliseUsage } from "./usage-day.mjs";
49
50
 
50
51
  /** Set to "1" to keep the deck off the desktop whatever the stored preference
51
52
  * says. Same sheet of switches as AGENTS_DECK_NO_DOWNLOAD and
@@ -90,8 +91,10 @@ export const DEFAULTS = Object.freeze({
90
91
  // are what the install last said, so an update is told once and "active" at
91
92
  // most once a day; `forget` is an id whose deletion has been asked for and
92
93
  // not yet acknowledged, retried on the next start so switching off while
93
- // offline still ends in a deletion.
94
- report: Object.freeze({ installId: "", lastVersion: "", lastActiveDay: "", forget: "" }),
94
+ // offline still ends in a deletion; `usage` is the day's tally of sessions,
95
+ // subagents and projects, counts and days only, so a restart carries it on
96
+ // (usage-day.mjs).
97
+ report: Object.freeze({ installId: "", lastVersion: "", lastActiveDay: "", forget: "", usage: null }),
95
98
  // The accounts this deck signed in itself, and the re-sign-in prompts
96
99
  // somebody put off (#1893) — keyed by account identity, written by the deck
97
100
  // and never by a page. See account-origins.mjs.
@@ -291,7 +294,8 @@ export function normalise(raw) {
291
294
  };
292
295
  }
293
296
 
294
- /** The reporter's own state, coerced: four strings, empty when absent. */
297
+ /** The reporter's own state, coerced: four strings, empty when absent, and the
298
+ * day's tally, null until there is one. */
295
299
  function normaliseReport(raw) {
296
300
  const src = raw && typeof raw === "object" ? raw : {};
297
301
  const text = v => (typeof v === "string" ? v : "");
@@ -300,6 +304,7 @@ function normaliseReport(raw) {
300
304
  lastVersion: text(src.lastVersion),
301
305
  lastActiveDay: text(src.lastActiveDay),
302
306
  forget: text(src.forget),
307
+ usage: src.usage && typeof src.usage === "object" ? normaliseUsage(src.usage) : null,
303
308
  };
304
309
  }
305
310
 
@@ -30,6 +30,8 @@ import { appendLogLine } from "./log-writer.mjs";
30
30
  import { activity } from "./lifecycle.mjs";
31
31
  // The session LRU and the transcript watch — see session-tracking.mjs.
32
32
  import { outputWatch, touchSession } from "./session-tracking.mjs";
33
+ // How much the deck was used today, for the "active" report — see usage-day.mjs.
34
+ import { usageDay } from "./usage-day.mjs";
33
35
  // What the deck learns about a session that its hooks never say, read off the
34
36
  // transcript and sent back through pushEvent — see session-enrichment.mjs.
35
37
  import { knownModelId, maybeResolveContext, maybeResolveModel, maybeResolveSessionName, maybeResolveUsage } from "./session-enrichment.mjs";
@@ -212,6 +214,8 @@ export function pushEvent(raw, source, opts = {}) {
212
214
  // rollout JSONL under ~/.codex/sessions/. The Claude scanners short-circuit
213
215
  // when transcript_path is absent (always the case for Codex hooks).
214
216
  if (source === "hook" && !opts.replay) {
217
+ // A hook is somebody running the CLI, which is what the day's usage counts.
218
+ usageDay.noteUse(raw);
215
219
  if (raw && raw.provider === "codex") {
216
220
  maybeResolveCodex(raw);
217
221
  } else if (!raw?.transcript_path || isClaudeTranscriptPath(raw.transcript_path)) {
@@ -228,7 +232,10 @@ export function pushEvent(raw, source, opts = {}) {
228
232
  // Where the transcript IS, learned from the one place it is free. The
229
233
  // four scanners above read it on this event; the watch reads it between
230
234
  // events, which is the whole of what it adds.
231
- if (raw?.transcript_path) outputWatch.note(raw.session_id, raw.transcript_path);
235
+ if (raw?.transcript_path) {
236
+ outputWatch.note(raw.session_id, raw.transcript_path);
237
+ usageDay.noteProject(raw.transcript_path);
238
+ }
232
239
  } else {
233
240
  noteRefusedTranscript(raw.transcript_path);
234
241
  }
@@ -12,7 +12,8 @@
12
12
  // - an "active" event at most once a UTC day, which is how many people use
13
13
  // ccdeck gets counted without counting anything else about them, and which
14
14
  // alone also carries coarse counts of how much: how many sessions,
15
- // subagents and projects — numbers, never their names,
15
+ // subagents and projects the deck heard from on the last day it was used,
16
+ // and that day's date — numbers, never their names (see usage-day.mjs),
16
17
  // - errors: a request handler that threw on this server, or an error the page
17
18
  // caught, with home folders, email addresses and key-shaped strings
18
19
  // scrubbed out before they leave,
@@ -62,6 +63,7 @@ import { reportsVetoed } from "./deck-prefs.mjs";
62
63
  import { isGitCheckout } from "./install-layout.mjs";
63
64
  import { heldPrefs, prefsRead } from "./prefs-state.mjs";
64
65
  import { RUNNING_VERSION } from "./running-version.mjs";
66
+ import { usageDay, utcDay } from "./usage-day.mjs";
65
67
 
66
68
  export const REPORTS_API = "https://api.ccdeck.dev";
67
69
  const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
@@ -80,7 +82,7 @@ const SAME_ERROR_QUIET_MS = 60 * 60 * 1000;
80
82
  const MESSAGE_MAX = 500;
81
83
  const STACK_MAX = 4000;
82
84
 
83
- const EMPTY_REPORT = Object.freeze({ installId: "", lastVersion: "", lastActiveDay: "", forget: "" });
85
+ const EMPTY_REPORT = Object.freeze({ installId: "", lastVersion: "", lastActiveDay: "", forget: "", usage: null });
84
86
 
85
87
  /** Is this deck sending reports right now? The switch, which is on unless
86
88
  * somebody turned it off, and the machine's consent. */
@@ -307,9 +309,10 @@ export function scrub(text, home = homedir()) {
307
309
  * @param {() => { claudeVersion?: string, codexVersion?: string }} [deps.versions] the CLI
308
310
  * versions to fold into the facts, read fresh on every send so a background
309
311
  * probe's answer rides along the moment it arrives. Default: none.
310
- * @param {() => ({ sessions?: number, subagents?: number, projects?: number } | Promise<...>)} [deps.usage]
311
- * the coarse counts the "active" report carries, read only when an "active" is
312
- * about to be sent. Default: none.
312
+ * @param {ReturnType<typeof import("./usage-day.mjs").createUsageDay>} [deps.usage]
313
+ * the day's tally: the "active" report carries the last finished day's totals,
314
+ * read only when an "active" is about to be sent, and the tally is saved into
315
+ * the prefs so a restart does not lose the day. Default: the deck's own.
313
316
  */
314
317
  export function createReporter({
315
318
  fetchImpl = globalThis.fetch,
@@ -320,7 +323,7 @@ export function createReporter({
320
323
  home = homedir(),
321
324
  ready = prefsRead,
322
325
  versions = () => ({}),
323
- usage = () => ({}),
326
+ usage = usageDay,
324
327
  } = {}) {
325
328
  const errorsSent = [];
326
329
  const lastSentAt = new Map();
@@ -348,13 +351,16 @@ export function createReporter({
348
351
  return { ...facts, ...versionFacts() };
349
352
  }
350
353
 
351
- /** The coarse usage the "active" report carries: only whole counts of zero or
352
- * more survive, and a provider that throws or is slow costs the report
354
+ /** The coarse usage the "active" report carries: the last finished day before
355
+ * `today`, its date as `usageDay` and its counts beside it. Nothing at all
356
+ * when there is no such day yet — a first launch has no yesterday — and only
357
+ * whole counts of zero or more survive. A tally that throws costs the report
353
358
  * nothing. */
354
- async function coarseUsage() {
359
+ function coarseUsage(today) {
355
360
  try {
356
- const raw = (await usage()) ?? {};
357
- const out = {};
361
+ const raw = usage.finished(today);
362
+ if (!raw || typeof raw.day !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(raw.day) || raw.day >= today) return {};
363
+ const out = { usageDay: raw.day };
358
364
  addIf(out, "sessions", count(raw.sessions));
359
365
  addIf(out, "subagents", count(raw.subagents));
360
366
  addIf(out, "projects", count(raw.projects));
@@ -364,6 +370,35 @@ export function createReporter({
364
370
  }
365
371
  }
366
372
 
373
+ /** The tally the last save wrote, so a save with nothing new writes nothing. */
374
+ let usageSaved = -1;
375
+
376
+ /** Keep the day's tally in the prefs, beside the install id, so a restart
377
+ * carries it on. Only while reports are on and under the id it belongs to:
378
+ * switching off drops it with the id. Never throws. */
379
+ async function saveUsage() {
380
+ try {
381
+ const at = usage.version();
382
+ if (at === usageSaved) return;
383
+ const p = prefs.current();
384
+ if (!reportsOn(p, env) || !p.report.installId) return;
385
+ await remember(p.report.installId, { usage: usage.saved() });
386
+ usageSaved = at;
387
+ } catch {
388
+ // A settings file that cannot be written this minute; the next save tries again.
389
+ }
390
+ }
391
+
392
+ /** The tally an earlier run saved, merged in once the prefs are read. */
393
+ function restoreUsage() {
394
+ try {
395
+ const saved = prefs.current().report?.usage;
396
+ if (saved) usage.restore(saved);
397
+ } catch {
398
+ // A saved tally that will not read: this run's counting goes on without it.
399
+ }
400
+ }
401
+
367
402
  async function call(method, path, body) {
368
403
  try {
369
404
  const res = await fetchImpl(REPORTS_API + path, {
@@ -426,16 +461,18 @@ export function createReporter({
426
461
  }
427
462
  }
428
463
 
429
- const today = now().toISOString().slice(0, 10);
464
+ const today = utcDay(now());
430
465
  if (lastActiveDay !== today) {
431
466
  // Usage rides on the "active" event alone, and is read only here — the one
432
467
  // report it belongs to — so a checkIn that sends nothing new never asks the
433
468
  // deck for its counts.
434
- const used = await coarseUsage();
469
+ const used = coarseUsage(today);
435
470
  if (await call("POST", "/v1/app/events", { installId, kind: "active", ...factsNow(), ...used })) {
471
+ if (used.usageDay) usage.markSent(used.usageDay);
436
472
  await remember(installId, { lastActiveDay: today });
437
473
  }
438
474
  }
475
+ await saveUsage();
439
476
  }
440
477
 
441
478
  /** The heartbeat. While reports are on and an id exists, tell the API this deck
@@ -494,10 +531,12 @@ export function createReporter({
494
531
 
495
532
  /** The heartbeat timer, rescheduled from itself so each beat carries fresh
496
533
  * jitter — a fixed interval would let a fleet that started as one beat as one.
497
- * Unref'd, so it never holds the process open. */
534
+ * Unref'd, so it never holds the process open. Each beat also saves the day's
535
+ * tally if it moved, so a deck that stops loses ten minutes of it at most. */
498
536
  function scheduleNextPing() {
499
537
  pingTimer = setTimeout(() => {
500
538
  void ping();
539
+ void saveUsage();
501
540
  scheduleNextPing();
502
541
  }, PING_EVERY_MS + Math.floor(Math.random() * PING_JITTER_MS));
503
542
  pingTimer.unref?.();
@@ -508,7 +547,7 @@ export function createReporter({
508
547
  * open. The first check-in already puts a freshly launched deck online, so the
509
548
  * first ping is a full interval out — nothing pings at boot. */
510
549
  function start() {
511
- Promise.resolve(ready).catch(() => {}).then(checkIn);
550
+ Promise.resolve(ready).catch(() => {}).then(restoreUsage).then(checkIn);
512
551
  if (!timer) {
513
552
  timer = setInterval(() => void checkIn(), CHECK_IN_EVERY_MS);
514
553
  timer.unref?.();
@@ -523,34 +562,16 @@ export function createReporter({
523
562
  pingTimer = null;
524
563
  }
525
564
 
526
- return { checkIn, ping, setReports, reportError, start, stop };
565
+ return { checkIn, ping, setReports, reportError, restoreUsage, saveUsage, start, stop };
527
566
  }
528
567
 
529
568
  // ── the deck's own providers ─────────────────────────────────────────────────
530
569
  //
531
570
  // Kept here rather than in the modules they read, so reports.mjs stays the one
532
571
  // place that decides what leaves — and reached by lazy import, so the reporter's
533
- // own module graph does not pull the session tracker, the enrichment cache or
534
- // the CLI probe into a fast path like `--version` that imports it for nothing.
535
-
536
- /** The coarse counts the "active" report carries, read from the state the deck
537
- * already keeps — the LRU of tracked sessions and the per-session subagent
538
- * signatures. Never new tracking, and never anything but a number: no session
539
- * id, name or path. `projects` is left out for now — its only source is the
540
- * disk-backed, per-account rollup (account-projects.mjs), which has no cheap
541
- * synchronous count — so the field is simply not sent. */
542
- async function deckUsage() {
543
- const out = {};
544
- try {
545
- const { trackedSessionCount } = await import("./session-tracking.mjs");
546
- out.sessions = trackedSessionCount();
547
- } catch { /* count unavailable: omitted */ }
548
- try {
549
- const { trackedSubagentCount } = await import("./session-enrichment.mjs");
550
- out.subagents = trackedSubagentCount();
551
- } catch { /* count unavailable: omitted */ }
552
- return out;
553
- }
572
+ // own module graph does not pull the CLI probe into a fast path like
573
+ // `--version` that imports it for nothing. The day's usage tally is imported
574
+ // directly: usage-day.mjs imports nothing.
554
575
 
555
576
  // The CLI versions, detected once in the background, off the boot path. The
556
577
  // first send starts the probe (past boot, and not while the machine has vetoed
@@ -570,4 +591,4 @@ function deckCliVersions() {
570
591
  }
571
592
 
572
593
  /** The deck's own reporter. */
573
- export const reporter = createReporter({ versions: deckCliVersions, usage: deckUsage });
594
+ export const reporter = createReporter({ versions: deckCliVersions });
@@ -77,21 +77,6 @@ function knownModelId(sid) {
77
77
  return cachedModelId(modelBySession.get(sid));
78
78
  }
79
79
 
80
- /** How many subagents the deck has seen across the sessions it still tracks —
81
- * the coarse "subagents" count the "active" report carries
82
- * (reports.mjs). Reused, not new state: `subsSig` is already the per-session
83
- * signature of the subagent models resolved for it, so its keys are the
84
- * subagents. A signature that will not parse is skipped rather than guessed at.
85
- * Only ever a number leaves — never an id, a model, a name or a path. */
86
- export function trackedSubagentCount() {
87
- let n = 0;
88
- for (const cached of modelBySession.values()) {
89
- if (!cached?.subsSig) continue;
90
- try { n += Object.keys(JSON.parse(cached.subsSig)).length; } catch { /* unparseable signature: not counted */ }
91
- }
92
- return n;
93
- }
94
-
95
80
  /** Read the main session JSONL. Returns the root model and any
96
81
  * legacy-schema subagent models (older CC versions kept subagent blocks
97
82
  * inline with `isSidechain:true` + `parentToolUseID`). Current CC versions
@@ -147,15 +147,6 @@ function forgetSession(sid) {
147
147
  forgetCodexSession(sid);
148
148
  }
149
149
 
150
- /** How many sessions the deck is still tracking — the size of the LRU the watch
151
- * polls, which is the coarse "how many sessions" the "active" report
152
- * counts (reports.mjs). Reused, not new state: the Map is already the deck's
153
- * own answer to which sessions it still holds anything for. Never a session id,
154
- * a name or a path — only the count. */
155
- function trackedSessionCount() {
156
- return sessionTouchedAt.size;
157
- }
158
-
159
150
  function touchSession(sid) {
160
151
  if (!sid || typeof sid !== "string") return;
161
152
  // Re-insert so the Map's own insertion order *is* the LRU order and eviction
@@ -248,4 +239,4 @@ async function handleForget(req, res) {
248
239
  // What index.mjs calls besides HARD_TRACKED_SESSIONS. Listed rather than
249
240
  // marked at each declaration, so the declarations read as they did where they
250
241
  // came from.
251
- export { handleForget, outputWatch, startOutputWatch, touchSession, trackedSessionCount };
242
+ export { handleForget, outputWatch, startOutputWatch, touchSession };
@@ -0,0 +1,176 @@
1
+ // How much this deck was used on one UTC day: the distinct sessions, subagents
2
+ // and projects its hooks heard from. The "active" report carries the totals of
3
+ // the last finished day (reports.mjs).
4
+ //
5
+ // WHY A DAY AND NOT A SNAPSHOT. The counts used to be read off the live state at
6
+ // the moment the "active" went out — and that moment is the first check-in of
7
+ // the day, which runs at launch, before anybody has done anything. Production
8
+ // on 2026-10-01: of 33 "active" reports carrying counts, the median session
9
+ // count was 0, and 24 said zero sessions. A day's totals, sent the next day,
10
+ // are the number the snapshot was trying to be.
11
+ //
12
+ // WHAT IS KEPT. In memory, the session ids, the subagent ids and the folders the
13
+ // transcripts sit in, so a session heard from a hundred times counts once. They
14
+ // never leave this module: what is saved between runs (prefs.json, beside the
15
+ // install id) and what is sent are three counts and the day they belong to.
16
+ //
17
+ // A RESTART MID-DAY. The saved counts come back as a floor and the new run
18
+ // counts on top of them, so a session that was running across the restart is
19
+ // counted twice. That is the price of keeping no ids on disk, and a restart is
20
+ // rare next to the day's sessions.
21
+ //
22
+ // Only what a hook said counts: a hook fires because somebody is running the
23
+ // CLI. Replayed lines, a rollout read off disk and the deck's own synthetic
24
+ // events are not use.
25
+
26
+ /** The day as the reporter says it: the UTC date, "2026-10-01". */
27
+ export function utcDay(date) {
28
+ return date.toISOString().slice(0, 10);
29
+ }
30
+
31
+ const DAY = /^\d{4}-\d{2}-\d{2}$/;
32
+
33
+ /** A whole count of zero or more, else 0. */
34
+ function whole(n) {
35
+ return Number.isInteger(n) && n >= 0 ? n : 0;
36
+ }
37
+
38
+ /** Saved totals, coerced: a day and three counts, or null for anything else. */
39
+ function savedTotals(raw) {
40
+ if (!raw || typeof raw !== "object" || typeof raw.day !== "string" || !DAY.test(raw.day)) return null;
41
+ return { day: raw.day, sessions: whole(raw.sessions), subagents: whole(raw.subagents), projects: whole(raw.projects) };
42
+ }
43
+
44
+ /** What prefs.json keeps for this, coerced — deck-prefs.mjs normalises with it. */
45
+ export function normaliseUsage(raw) {
46
+ const src = raw && typeof raw === "object" ? raw : {};
47
+ return {
48
+ current: savedTotals(src.current),
49
+ done: savedTotals(src.done),
50
+ sent: typeof src.sent === "string" && DAY.test(src.sent) ? src.sent : "",
51
+ };
52
+ }
53
+
54
+ /** The folder a transcript sits in, which is the project it belongs to:
55
+ * `~/.claude/projects/<project>/<session>.jsonl`. */
56
+ function folderOf(path) {
57
+ const cut = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
58
+ return cut > 0 ? path.slice(0, cut) : path;
59
+ }
60
+
61
+ /**
62
+ * The tally. `now` is injectable so the suite can walk it across midnight.
63
+ */
64
+ export function createUsageDay({ now = () => new Date() } = {}) {
65
+ /** The day being counted: saved counts from an earlier run as a floor, and
66
+ * this run's distinct ids on top. */
67
+ let current = null;
68
+ /** The last finished day whose totals have not gone out yet. */
69
+ let done = null;
70
+ /** The last day whose totals did go out. */
71
+ let sent = "";
72
+ /** Moves on every change, so the reporter saves only when there is something new. */
73
+ let changes = 0;
74
+
75
+ function fresh(day, floor = null) {
76
+ return {
77
+ day,
78
+ floor: floor ?? { sessions: 0, subagents: 0, projects: 0 },
79
+ sessions: new Set(),
80
+ subagents: new Set(),
81
+ projects: new Set(),
82
+ };
83
+ }
84
+
85
+ function totals(c) {
86
+ return {
87
+ day: c.day,
88
+ sessions: c.floor.sessions + c.sessions.size,
89
+ subagents: c.floor.subagents + c.subagents.size,
90
+ projects: c.floor.projects + c.projects.size,
91
+ };
92
+ }
93
+
94
+ /** The day being counted, rolled over first if the clock has passed it. */
95
+ function today() {
96
+ const day = utcDay(now());
97
+ if (current?.day === day) return current;
98
+ if (current && current.day < day) done = totals(current);
99
+ current = fresh(day);
100
+ changes++;
101
+ return current;
102
+ }
103
+
104
+ function add(set, key) {
105
+ if (set.has(key)) return;
106
+ set.add(key);
107
+ changes++;
108
+ }
109
+
110
+ /** A live hook payload: its session, and the subagent it came from if any. */
111
+ function noteUse(raw) {
112
+ const sid = raw?.session_id;
113
+ if (!sid || typeof sid !== "string") return;
114
+ const c = today();
115
+ add(c.sessions, sid);
116
+ if (typeof raw.agent_id === "string" && raw.agent_id) add(c.subagents, `${sid}\u0000${raw.agent_id}`);
117
+ }
118
+
119
+ /** A transcript path a hook carried, already accepted as Claude's. */
120
+ function noteProject(path) {
121
+ if (!path || typeof path !== "string") return;
122
+ add(today().projects, folderOf(path));
123
+ }
124
+
125
+ /** The totals of the last finished day before `day`, if they have not gone
126
+ * out yet; null when there is none. Rolls over first, so a deck that ran past
127
+ * midnight with nothing said since still has yesterday finished. */
128
+ function finished(day) {
129
+ today();
130
+ const c = current && current.day < day ? totals(current) : done;
131
+ return c && c.day < day && c.day > sent ? c : null;
132
+ }
133
+
134
+ /** Those totals went out: never send them again. */
135
+ function markSent(day) {
136
+ if (day > sent) sent = day;
137
+ if (done && done.day <= sent) done = null;
138
+ changes++;
139
+ }
140
+
141
+ /** What survives a restart: counts and days, never an id or a path. */
142
+ function saved() {
143
+ return { current: current ? totals(current) : null, done, sent };
144
+ }
145
+
146
+ /**
147
+ * An earlier run's `saved()`, merged into this one. This run may already have
148
+ * heard from sessions — the prefs are read after the first hooks can land —
149
+ * so nothing it counted is dropped: the saved day becomes the floor of the
150
+ * same day, or the finished day of an earlier one.
151
+ */
152
+ function restore(raw) {
153
+ const s = normaliseUsage(raw);
154
+ if (s.sent > sent) sent = s.sent;
155
+ for (const t of [s.done, s.current]) {
156
+ if (!t) continue;
157
+ const c = today();
158
+ if (t.day === c.day) {
159
+ c.floor = { sessions: t.sessions, subagents: t.subagents, projects: t.projects };
160
+ } else if (t.day < c.day && (!done || t.day > done.day)) {
161
+ done = t;
162
+ }
163
+ }
164
+ if (done && done.day <= sent) done = null;
165
+ changes++;
166
+ }
167
+
168
+ function version() {
169
+ return changes;
170
+ }
171
+
172
+ return { noteUse, noteProject, finished, markSent, saved, restore, version };
173
+ }
174
+
175
+ /** The deck's own tally, which the event pipeline feeds and the reporter reads. */
176
+ export const usageDay = createUsageDay();