@bongos/core 1.19.819 → 1.19.820

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.
@@ -3,9 +3,9 @@
3
3
  "generator": "scripts/gds/copy-inventory.js",
4
4
  "goal": 1000074,
5
5
  "counts": {
6
- "strings": 1863,
7
- "unique_ids": 1617,
8
- "certain": 1863,
6
+ "strings": 1868,
7
+ "unique_ids": 1622,
8
+ "certain": 1868,
9
9
  "heuristic": 0,
10
10
  "duplicate_ids": 170
11
11
  },
@@ -13,7 +13,7 @@
13
13
  "builders-hall": {
14
14
  "at": "modules/hall-ui/public",
15
15
  "files": 86,
16
- "strings": 1373
16
+ "strings": 1378
17
17
  },
18
18
  "status-dashboard": {
19
19
  "at": "modules/status-ui/public",
@@ -730,8 +730,8 @@
730
730
  "text": "Could not load sessions.",
731
731
  "occurrences": 2,
732
732
  "at": [
733
- "modules/hall-ui/public/sessions.js:275",
734
- "modules/hall-ui/public/sessions.js:313"
733
+ "modules/hall-ui/public/sessions.js:399",
734
+ "modules/hall-ui/public/sessions.js:437"
735
735
  ]
736
736
  },
737
737
  {
@@ -1480,8 +1480,8 @@
1480
1480
  "text": "The session log could not be loaded. Try again.",
1481
1481
  "occurrences": 2,
1482
1482
  "at": [
1483
- "modules/hall-ui/public/sessions.js:274",
1484
- "modules/hall-ui/public/sessions.js:326"
1483
+ "modules/hall-ui/public/sessions.js:398",
1484
+ "modules/hall-ui/public/sessions.js:450"
1485
1485
  ]
1486
1486
  },
1487
1487
  {
@@ -2491,6 +2491,15 @@
2491
2491
  "origin": "html-text",
2492
2492
  "confidence": "certain"
2493
2493
  },
2494
+ {
2495
+ "id": "e08d410571e9",
2496
+ "surface": "builders-hall",
2497
+ "text": "All kinds",
2498
+ "file": "modules/hall-ui/public/sessions.js",
2499
+ "line": 286,
2500
+ "origin": "js-markup",
2501
+ "confidence": "certain"
2502
+ },
2494
2503
  {
2495
2504
  "id": "9c34bccb06ef",
2496
2505
  "surface": "builders-hall",
@@ -3954,7 +3963,7 @@
3954
3963
  "surface": "builders-hall",
3955
3964
  "text": "Could not load sessions.",
3956
3965
  "file": "modules/hall-ui/public/sessions.js",
3957
- "line": 275,
3966
+ "line": 399,
3958
3967
  "origin": "js-notice",
3959
3968
  "confidence": "certain"
3960
3969
  },
@@ -3963,7 +3972,7 @@
3963
3972
  "surface": "builders-hall",
3964
3973
  "text": "Could not load sessions.",
3965
3974
  "file": "modules/hall-ui/public/sessions.js",
3966
- "line": 313,
3975
+ "line": 437,
3967
3976
  "origin": "js-notice",
3968
3977
  "confidence": "certain"
3969
3978
  },
@@ -4395,7 +4404,7 @@
4395
4404
  "surface": "builders-hall",
4396
4405
  "text": "Could not verify your access. Try again.",
4397
4406
  "file": "modules/hall-ui/public/sessions.js",
4398
- "line": 312,
4407
+ "line": 436,
4399
4408
  "origin": "js-text-assign",
4400
4409
  "confidence": "certain"
4401
4410
  },
@@ -5119,6 +5128,15 @@
5119
5128
  "origin": "js-text-assign",
5120
5129
  "confidence": "certain"
5121
5130
  },
5131
+ {
5132
+ "id": "e8dd9a8f3a7e",
5133
+ "surface": "builders-hall",
5134
+ "text": "Filter sessions by kind",
5135
+ "file": "modules/hall-ui/public/sessions.html",
5136
+ "line": 67,
5137
+ "origin": "html-attr:aria-label",
5138
+ "confidence": "certain"
5139
+ },
5122
5140
  {
5123
5141
  "id": "1def0e7c716e",
5124
5142
  "surface": "builders-hall",
@@ -5700,7 +5718,7 @@
5700
5718
  "surface": "builders-hall",
5701
5719
  "text": "How the session read to this builder, scored against their own interaction profile. Advisory — it gates nothing.",
5702
5720
  "file": "modules/hall-ui/public/sessions.html",
5703
- "line": 63,
5721
+ "line": 78,
5704
5722
  "origin": "html-attr:title",
5705
5723
  "confidence": "certain"
5706
5724
  },
@@ -6735,7 +6753,7 @@
6735
6753
  "surface": "builders-hall",
6736
6754
  "text": "Most recent first. Each load of this page is itself recorded in the audit log.",
6737
6755
  "file": "modules/hall-ui/public/sessions.js",
6738
- "line": 228,
6756
+ "line": 340,
6739
6757
  "origin": "js-text-assign",
6740
6758
  "confidence": "certain"
6741
6759
  },
@@ -7104,7 +7122,7 @@
7104
7122
  "surface": "builders-hall",
7105
7123
  "text": "No sessions recorded yet. They land here as builders ship and end sessions.",
7106
7124
  "file": "modules/hall-ui/public/sessions.js",
7107
- "line": 109,
7125
+ "line": 146,
7108
7126
  "origin": "js-markup",
7109
7127
  "confidence": "certain"
7110
7128
  },
@@ -7162,6 +7180,15 @@
7162
7180
  "origin": "js-markup",
7163
7181
  "confidence": "certain"
7164
7182
  },
7183
+ {
7184
+ "id": "086665af6208",
7185
+ "surface": "builders-hall",
7186
+ "text": "No {…} sessions recorded. Choose another kind to widen the ledger.",
7187
+ "file": "modules/hall-ui/public/sessions.js",
7188
+ "line": 145,
7189
+ "origin": "js-markup",
7190
+ "confidence": "certain"
7191
+ },
7165
7192
  {
7166
7193
  "id": "1967d2176f2e",
7167
7194
  "surface": "builders-hall",
@@ -7230,7 +7257,7 @@
7230
7257
  "surface": "builders-hall",
7231
7258
  "text": "Not scored — this builder’s own profile waived every signal, or the session predates this measure.",
7232
7259
  "file": "modules/hall-ui/public/sessions.js",
7233
- "line": 67,
7260
+ "line": 100,
7234
7261
  "origin": "js-markup",
7235
7262
  "confidence": "certain"
7236
7263
  },
@@ -7261,6 +7288,15 @@
7261
7288
  "origin": "js-markup",
7262
7289
  "confidence": "certain"
7263
7290
  },
7291
+ {
7292
+ "id": "20bb29015a84",
7293
+ "surface": "builders-hall",
7294
+ "text": "Not yet classified.",
7295
+ "file": "modules/hall-ui/public/sessions.js",
7296
+ "line": 72,
7297
+ "origin": "js-markup",
7298
+ "confidence": "certain"
7299
+ },
7264
7300
  {
7265
7301
  "id": "571b9b22dbda",
7266
7302
  "surface": "builders-hall",
@@ -7869,7 +7905,7 @@
7869
7905
  "surface": "builders-hall",
7870
7906
  "text": "Plain speech",
7871
7907
  "file": "modules/hall-ui/public/sessions.html",
7872
- "line": 63,
7908
+ "line": 78,
7873
7909
  "origin": "html-text",
7874
7910
  "confidence": "certain"
7875
7911
  },
@@ -10209,7 +10245,7 @@
10209
10245
  "surface": "builders-hall",
10210
10246
  "text": "The session log could not be loaded. Try again.",
10211
10247
  "file": "modules/hall-ui/public/sessions.js",
10212
- "line": 274,
10248
+ "line": 398,
10213
10249
  "origin": "js-text-assign",
10214
10250
  "confidence": "certain"
10215
10251
  },
@@ -10218,7 +10254,7 @@
10218
10254
  "surface": "builders-hall",
10219
10255
  "text": "The session log could not be loaded. Try again.",
10220
10256
  "file": "modules/hall-ui/public/sessions.js",
10221
- "line": 326,
10257
+ "line": 450,
10222
10258
  "origin": "js-text-assign",
10223
10259
  "confidence": "certain"
10224
10260
  },
@@ -10731,7 +10767,7 @@
10731
10767
  "surface": "builders-hall",
10732
10768
  "text": "Tool errors",
10733
10769
  "file": "modules/hall-ui/public/sessions.html",
10734
- "line": 61,
10770
+ "line": 76,
10735
10771
  "origin": "html-text",
10736
10772
  "confidence": "certain"
10737
10773
  },
@@ -11068,6 +11104,15 @@
11068
11104
  "origin": "html-attr:placeholder",
11069
11105
  "confidence": "certain"
11070
11106
  },
11107
+ {
11108
+ "id": "e179870922a3",
11109
+ "surface": "builders-hall",
11110
+ "text": "What the session did, derived from its own transcript: whether it wrote files, used tools, and how many prompts a person actually sent.",
11111
+ "file": "modules/hall-ui/public/sessions.html",
11112
+ "line": 74,
11113
+ "origin": "html-attr:title",
11114
+ "confidence": "certain"
11115
+ },
11071
11116
  {
11072
11117
  "id": "5f2b989bbf6a",
11073
11118
  "surface": "builders-hall",
@@ -13395,7 +13440,7 @@
13395
13440
  "surface": "builders-hall",
13396
13441
  "text": "{…} and up",
13397
13442
  "file": "modules/hall-ui/public/sessions.js",
13398
- "line": 149,
13443
+ "line": 187,
13399
13444
  "origin": "js-text-assign",
13400
13445
  "confidence": "certain"
13401
13446
  },
@@ -13404,7 +13449,7 @@
13404
13449
  "surface": "builders-hall",
13405
13450
  "text": "{…} and up.",
13406
13451
  "file": "modules/hall-ui/public/sessions.js",
13407
- "line": 147,
13452
+ "line": 185,
13408
13453
  "origin": "js-text-assign",
13409
13454
  "confidence": "certain"
13410
13455
  },
@@ -13579,15 +13624,6 @@
13579
13624
  "origin": "js-markup",
13580
13625
  "confidence": "certain"
13581
13626
  },
13582
- {
13583
- "id": "3d76dce19c4a",
13584
- "surface": "builders-hall",
13585
- "text": "{…} session{…} across {…} builder{…}.",
13586
- "file": "modules/hall-ui/public/sessions.js",
13587
- "line": 226,
13588
- "origin": "js-text-assign",
13589
- "confidence": "certain"
13590
- },
13591
13627
  {
13592
13628
  "id": "e08fa22622f0",
13593
13629
  "surface": "builders-hall",
@@ -13629,7 +13665,7 @@
13629
13665
  "surface": "builders-hall",
13630
13666
  "text": "{…} tokens",
13631
13667
  "file": "modules/hall-ui/public/sessions.js",
13632
- "line": 132,
13668
+ "line": 170,
13633
13669
  "origin": "js-markup",
13634
13670
  "confidence": "certain"
13635
13671
  },
@@ -13840,6 +13876,15 @@
13840
13876
  "origin": "js-markup",
13841
13877
  "confidence": "certain"
13842
13878
  },
13879
+ {
13880
+ "id": "97e27ab169ae",
13881
+ "surface": "builders-hall",
13882
+ "text": "{…}{…} session{…} across {…} builder{…}.",
13883
+ "file": "modules/hall-ui/public/sessions.js",
13884
+ "line": 338,
13885
+ "origin": "js-text-assign",
13886
+ "confidence": "certain"
13887
+ },
13843
13888
  {
13844
13889
  "id": "3d74668f69e5",
13845
13890
  "surface": "builders-hall",
@@ -2111,6 +2111,8 @@ is load-bearing: the script throws rather than guess if it is missing, and
2111
2111
  1.19.818 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2112
2112
  landed since 1.19.817 with no explicit bump. run 35201213516. (task 1002620)
2113
2113
  1.19.819 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2114
- landed since 1.19.818 with no explicit bump. run 35205615659. (task 1002620)
2114
+ landed since 1.19.818 with no explicit bump. run 35206448426. (task 1002620)
2115
+ 1.19.820 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2116
+ landed since 1.19.819 with no explicit bump. run 35209838815. (task 1002620)
2115
2117
  ---------------------------------------------------------------------------
2116
2118
  ```
@@ -0,0 +1,49 @@
1
+ -- core_246_session_type.sql — WHAT EACH SESSION WAS (task 1003975, idea 1001033).
2
+ --
3
+ -- The session ledger could say whether a session shipped a task and nothing else,
4
+ -- so the 54% of the corpus with an empty task_ids read as an undifferentiated
5
+ -- column of dashes. This column holds the verdict of modules/sessions/session-type.js
6
+ -- — a pure, deterministic read of the digest the session already uploaded (its
7
+ -- prompt count and which tools it called), computed once at upload rather than on
8
+ -- every query, because the write-tool signal lives in the `detail` jsonb and
9
+ -- scanning it per request would put a jsonb walk on the ledger's hot path.
10
+ --
11
+ -- WHY IT IS HERE AND NOT IN modules/sessions/migrations/. The sessions module's
12
+ -- own CLAUDE.md says a new session migration belongs in the module — but that
13
+ -- holds only for a table the module OWNS. `session_records` predates the module
14
+ -- system and lives in core, and ADR 0083 §Decision #5 (enforced by fitness.js,
15
+ -- "module migrations additive + namespaced") lets a module migration touch only
16
+ -- its own `sessions_`-prefixed tables. A module migration adding this column was
17
+ -- written first and the gate refused it, correctly. The rule wins; the module doc
18
+ -- has been narrowed to say so.
19
+ --
20
+ -- NULLABLE ON PURPOSE, and there is no DEFAULT. Three reasons:
21
+ -- * Every row that predates this migration is legitimately unclassified until
22
+ -- the backfill (scripts/gds/backfill-session-types.js) runs. A DEFAULT would
23
+ -- assert a type for 4,595 rows nobody measured.
24
+ -- * The upload path fills it, but an older CLI posting the current body shape
25
+ -- must keep working — the column is derived server-side from what the body
26
+ -- already carries, never sent by the client, so no pinned core is broken.
27
+ -- * NULL is distinguishable from 'unknown'. NULL means "not yet classified";
28
+ -- 'unknown' means "classified, and the digest did not decide". Collapsing the
29
+ -- two would make the backfill's own progress unreadable.
30
+ --
31
+ -- NO CHECK CONSTRAINT against the type list. The closed set lives in
32
+ -- session-type.js, which the route filter and the ledger both validate against;
33
+ -- duplicating it here would give the vocabulary a second home that a deploy can
34
+ -- leave behind. The values are written by one function, never by a client.
35
+
36
+ ALTER TABLE session_records
37
+ ADD COLUMN IF NOT EXISTS session_type text;
38
+
39
+ COMMENT ON COLUMN session_records.session_type IS
40
+ 'Derived shape of the session (build / automated / exploration / conversation / unknown) '
41
+ 'from modules/sessions/session-type.js. NULL = not yet classified, which is not the same '
42
+ 'as ''unknown''. Never client-supplied.';
43
+
44
+ -- Partial index: the ledger filters BY a type, never FOR the unclassified, so the
45
+ -- NULL rows are dead weight in the index. Drops with the backlog as the backfill
46
+ -- runs, which is the opposite of what a full index would do.
47
+ CREATE INDEX IF NOT EXISTS idx_session_records_type
48
+ ON session_records (session_type, uploaded_at DESC)
49
+ WHERE session_type IS NOT NULL;
@@ -17,3 +17,22 @@
17
17
  /* The task ids: chips that are doors (the family's a.fact-chip hover). */
18
18
  .ledger-tasks { display: inline-flex; flex-wrap: wrap; gap: 6px; }
19
19
  .ledger-tasks .fact-chip { text-decoration: none; }
20
+ /* task 1003975 — the kind filter row. Right-aligned above the ledger so it sits
21
+ over the column it acts on without stealing the lede's line; it reuses the
22
+ pager's .pager__perpage label+select so the page has ONE control grammar. */
23
+ .ledger-filters { display: flex; justify-content: flex-end; margin: 0 0 var(--sp-sm); }
24
+ /* The control itself: the same label+select proportions as the pager's per-page
25
+ picker, so the page reads as one grammar — but its OWN class, because the
26
+ hall_audit guard treats any .pager* markup outside the kit as a second pager,
27
+ and borrowing that family is exactly how one starts. */
28
+ .ledger-filter {
29
+ display: inline-flex; align-items: center; gap: 6px;
30
+ font-size: 12px; color: var(--ink-faint);
31
+ }
32
+ .ledger-filter select { height: 30px; padding: 2px 8px; font-size: 13px; }
33
+ /* The kind cell: a quiet fact, not a chip. The ledger already spends its colour
34
+ on the two warnings (tool errors, plain speech), and a third coloured token
35
+ per row would make the warnings stop reading as warnings. Deliberately NO
36
+ white-space: nowrap — task 1003470 removed the blanket nowrap that broke this
37
+ table on a phone, and "Automated run" is short enough to wrap harmlessly. */
38
+ .ledger-kind { color: var(--ink-soft); }
@@ -54,10 +54,25 @@
54
54
  <section class="scroll" id="sessions-scroll" aria-labelledby="sessions-h" hidden>
55
55
  <h2 id="sessions-h" class="scroll__h">Recorded sessions</h2>
56
56
  <p class="scroll__lede">Most recent first. Each row is one recorded work session; every id on it opens its record.</p>
57
+ <!-- task 1003975: the ledger's first filter. It LOOKS like the pager's
58
+ per-page control on purpose (one control grammar per page) but wears
59
+ its own class — the hall_audit guard reads `class="pager…"` as a
60
+ hand-rolled pager, and it is right to: borrowing that family is how a
61
+ second pager implementation starts. Its options are filled from the
62
+ server's vocabulary in sessions.js, never hardcoded here, where they
63
+ would drift from what the route accepts. -->
64
+ <div class="ledger-filters">
65
+ <label class="ledger-filter">
66
+ <span>Kind</span>
67
+ <select id="session-type-filter" aria-label="Filter sessions by kind"></select>
68
+ </label>
69
+ </div>
57
70
  <table class="ledger" id="sessions-table">
58
71
  <thead>
59
72
  <tr>
60
- <th>Session</th><th>Builder</th><th>Tasks</th>
73
+ <th>Session</th><th>Builder</th>
74
+ <th title="What the session did, derived from its own transcript: whether it wrote files, used tools, and how many prompts a person actually sent.">Kind</th>
75
+ <th>Tasks</th>
61
76
  <th class="ledger__num">Earned</th><th class="ledger__num">Turns</th><th class="ledger__num">Tokens</th><th class="ledger__num">Tool errors</th>
62
77
  <th class="ledger__num">Duration</th>
63
78
  <th class="ledger__num" title="How the session read to this builder, scored against their own interaction profile. Advisory — it gates nothing.">Plain speech</th>
@@ -42,6 +42,39 @@
42
42
 
43
43
  const shortSid = (sid) => String(sid || '').slice(0, 8) || '—';
44
44
 
45
+ // task 1003975 — what each session type is CALLED, and what it means on hover.
46
+ // The keys come from the server (`session_types` on the search response); this
47
+ // map only supplies the words. A type the server adds before this map learns it
48
+ // still renders — as its raw key — because a missing label must never make a
49
+ // real row look blank.
50
+ const KIND_LABEL = {
51
+ build: 'Build',
52
+ automated: 'Automated run',
53
+ exploration: 'Exploring',
54
+ conversation: 'Conversation',
55
+ unknown: 'Unclear',
56
+ };
57
+ const KIND_TITLE = {
58
+ build: 'Files were written or edited in this session.',
59
+ automated: 'One prompt and no edits — a grader, a subagent or a scripted run, not someone working.',
60
+ exploration: 'Someone steered it through several prompts, reading and running things without editing.',
61
+ conversation: 'Several prompts and no tools at all — a question or a discussion.',
62
+ unknown: 'The recorded transcript does not say — usually a session that predates this measure.',
63
+ };
64
+ const kindLabel = (t) => KIND_LABEL[t] || String(t);
65
+
66
+ // Emits the whole <td>. A row with no type yet (the backfill has not reached it)
67
+ // is a dash, NOT 'Unclear' — "not classified" and "classified as undecidable"
68
+ // are different facts and the ledger should not merge them.
69
+ function kindCell(s) {
70
+ const t = s.session_type;
71
+ if (!t) {
72
+ return '<td class="ledger-kind" title="Not yet classified.">—</td>';
73
+ }
74
+ const title = KIND_TITLE[t] || '';
75
+ return `<td class="ledger-kind" title="${escapeHtml(title)}">${escapeHtml(kindLabel(t))}</td>`;
76
+ }
77
+
45
78
  // task 1002516 — the transcript accessibility dimension (criterion
46
79
  // `accessibility-fluency-profile`, clause 4). Headed "Plain speech" rather than
47
80
  // "Accessibility" because this page already lives under a shell whose only
@@ -105,8 +138,12 @@
105
138
  function renderTable(sessions) {
106
139
  const body = $('#sessions-body');
107
140
  if (!sessions.length) {
108
- body.innerHTML =
109
- '<tr><td colspan="10" class="ledger__empty">No sessions recorded yet. They land here as builders ship and end sessions.</td></tr>';
141
+ // The empty line has to answer the question the reader is actually asking,
142
+ // and with a filter on that question is "did I filter everything away?",
143
+ // not "why has nobody ever worked here".
144
+ body.innerHTML = state.sessionType
145
+ ? `<tr><td colspan="11" class="ledger__empty">No ${escapeHtml(kindLabel(state.sessionType).toLowerCase())} sessions recorded. Choose another kind to widen the ledger.</td></tr>`
146
+ : '<tr><td colspan="11" class="ledger__empty">No sessions recorded yet. They land here as builders ship and end sessions.</td></tr>';
110
147
  return;
111
148
  }
112
149
  body.innerHTML = sessions
@@ -126,6 +163,7 @@
126
163
  `<td>${whoHtml}` +
127
164
  (s.rank ? ` <span class="ledger-rank">${escapeHtml(rankLabel(s.rank))}</span>` : '') +
128
165
  '</td>' +
166
+ kindCell(s) +
129
167
  `<td>${tasks}</td>` +
130
168
  `<td class="ledger__num">${fmtInt(s.drachmae_earned)}</td>` +
131
169
  `<td class="ledger__num">${fmtInt(s.assistant_turns)}</td>` +
@@ -186,7 +224,24 @@
186
224
  // pager first and calls back second, so a failed fetch has to put it back to these —
187
225
  // otherwise the bar goes on claiming a page whose rows never arrived. Keeping both
188
226
  // (not just the page) means a failed per-page change rolls back cleanly too.
189
- const state = { total: 0, shown: 0, page: 1, perPage: pager?.perPage ?? 0 };
227
+ // `sessionType` is the live filter (null = every kind). It sits in the same
228
+ // roll-back state as page/perPage because a failed fetch has to put ALL THREE
229
+ // back — a bar claiming page 1 of "Exploring" whose rows never arrived is the
230
+ // same lie whichever control moved last.
231
+ const state = { total: 0, shown: 0, page: 1, perPage: pager?.perPage ?? 0, sessionType: null };
232
+ // Remembered per viewer, like the pager's per-page choice: a monitoring page is
233
+ // returned to, and re-picking the same lens every visit is a tax. Wrapped
234
+ // because storage throws in a private window, where the page must still work.
235
+ const TYPE_KEY = 'otb.sessions.kind';
236
+ function readStoredType() {
237
+ try { return window.localStorage.getItem(TYPE_KEY) || null; } catch { return null; }
238
+ }
239
+ function storeType(t) {
240
+ try {
241
+ if (t) window.localStorage.setItem(TYPE_KEY, t);
242
+ else window.localStorage.removeItem(TYPE_KEY);
243
+ } catch { /* no-op: the filter still works for this visit */ }
244
+ }
190
245
  let loading = false;
191
246
  // Two clicks can be in flight at once; only the newest may paint (the profile
192
247
  // activity-log idiom, task 1003281).
@@ -201,6 +256,11 @@
201
256
  const clamped = kit.renderServerPage(pager, state.total, state.shown);
202
257
  const perPage = $(`#${PAGER_PREFIX}-per-page`);
203
258
  if (perPage) perPage.disabled = loading;
259
+ // The kind filter is disabled for the same reason the pager is: a second
260
+ // change mid-fetch would put a third request in flight behind two that are
261
+ // already racing to paint.
262
+ const kind = $('#session-type-filter');
263
+ if (kind) kind.disabled = loading;
204
264
  if (loading) {
205
265
  for (const id of ['prev', 'next']) {
206
266
  const el = $(`#${PAGER_PREFIX}-${id}`);
@@ -210,6 +270,52 @@
210
270
  return clamped;
211
271
  }
212
272
 
273
+ // ---- the kind filter -----------------------------------------------------
274
+
275
+ // Fill the control from the vocabulary the SERVER sent, once. Re-stamping it on
276
+ // every paint would reset the open dropdown under a reader's cursor, so it
277
+ // returns early when the options already match what arrived.
278
+ let typeOptionsFrom = null;
279
+ function syncTypeOptions(types) {
280
+ const el = $('#session-type-filter');
281
+ if (!el || !Array.isArray(types) || types.length === 0) return;
282
+ const key = types.join(',');
283
+ if (typeOptionsFrom === key) return;
284
+ typeOptionsFrom = key;
285
+ el.innerHTML =
286
+ '<option value="">All kinds</option>' +
287
+ types.map((t) => `<option value="${escapeHtml(t)}">${escapeHtml(kindLabel(t))}</option>`).join('');
288
+ // A remembered kind is only honoured once the server has confirmed it is
289
+ // still a kind — otherwise the control would show a value no query accepts.
290
+ if (state.sessionType && !types.includes(state.sessionType)) {
291
+ setTypeFilter(null, { reload: false });
292
+ }
293
+ el.value = state.sessionType || '';
294
+ }
295
+
296
+ // Move the filter. `reload:false` is the internal correction (a stale stored
297
+ // value being dropped) — it must not fire a fetch from inside one.
298
+ function setTypeFilter(type, { reload = true } = {}) {
299
+ state.sessionType = type || null;
300
+ storeType(state.sessionType);
301
+ const el = $('#session-type-filter');
302
+ if (el) el.value = state.sessionType || '';
303
+ if (!reload) return;
304
+ // A narrower corpus makes the open page meaningless — page 7 of Build is not
305
+ // page 7 of everything — so every change starts at the first page.
306
+ pager.page = 1;
307
+ state.page = 1;
308
+ fetchPage().then((ok) => {
309
+ if (ok) window.scrollTo({ top: 0, behavior: 'smooth' });
310
+ });
311
+ }
312
+
313
+ function wireTypeFilter() {
314
+ const el = $('#session-type-filter');
315
+ if (!el) return;
316
+ el.addEventListener('change', () => setTypeFilter(el.value || null));
317
+ }
318
+
213
319
  // Paint one page. Returns true when the pager clamped — the corpus shrank under an
214
320
  // open page, so the caller must re-ask rather than leave a range that lies.
215
321
  function showData(data) {
@@ -222,9 +328,15 @@
222
328
  $('#sessions-scroll').hidden = false;
223
329
  renderStats(totals);
224
330
  renderTable(sessions);
331
+ syncTypeOptions(data.session_types);
225
332
  const clamped = renderPager();
333
+ // With a kind chosen, the count IS the filtered count (the tally shares the
334
+ // filter clause with the page query), so the line has to say which — a
335
+ // headline reading "57 sessions" under a control set to Build would otherwise
336
+ // look like the whole corpus had shrunk.
337
+ const scope = state.sessionType ? ` ${kindLabel(state.sessionType).toLowerCase()}` : '';
226
338
  $('#ledger-sub').textContent =
227
- `${fmtInt(totals.total)} session${num(totals.total) === 1 ? '' : 's'} across ${fmtInt(totals.builders)} builder${num(totals.builders) === 1 ? '' : 's'}.`;
339
+ `${fmtInt(totals.total)}${scope} session${num(totals.total) === 1 ? '' : 's'} across ${fmtInt(totals.builders)} builder${num(totals.builders) === 1 ? '' : 's'}.`;
228
340
  $('#ledger-foot').textContent =
229
341
  'Most recent first. Each load of this page is itself recorded in the audit log.';
230
342
  return clamped;
@@ -238,14 +350,17 @@
238
350
  // then calls back — so a failure restores `state.page`/`state.perPage`, the last
239
351
  // values that painted. That roll-back is what the old hand-roll did with
240
352
  // `state.offset`, and it is the one piece of it worth carrying over.
241
- async function fetchPage({ reclamps = 0 } = {}) {
353
+ async function fetchPage({ reclamps = 0, droppedFilter = false } = {}) {
242
354
  const seq = ++inFlight;
243
355
  loading = true;
244
356
  renderPager();
245
357
  try {
358
+ const typeParam = state.sessionType
359
+ ? `&session_type=${encodeURIComponent(state.sessionType)}`
360
+ : '';
246
361
  const res = await api.request(
247
362
  'GET',
248
- `${API}/sessions/search?order=recent&limit=${pager.perPage}&offset=${kit.pageOffset(pager)}`
363
+ `${API}/sessions/search?order=recent&limit=${pager.perPage}&offset=${kit.pageOffset(pager)}${typeParam}`
249
364
  );
250
365
  // A newer click already went out; painting this answer would show a page nobody
251
366
  // is on any more.
@@ -255,6 +370,15 @@
255
370
  showSealed('You do not have access to the session log.');
256
371
  return false;
257
372
  }
373
+ // A remembered kind the server no longer accepts (a type retired between
374
+ // visits) comes back as a named 400. Drop the stale lens and re-ask once,
375
+ // rather than showing a load failure the reader cannot act on — their only
376
+ // clue would be a control still displaying the dead value.
377
+ if (res.status === 400 && state.sessionType && !droppedFilter) {
378
+ setTypeFilter(null, { reload: false });
379
+ loading = false;
380
+ return fetchPage({ reclamps, droppedFilter: true });
381
+ }
258
382
  if (!res.ok) throw new Error(`search → ${res.status}`);
259
383
  const clamped = showData(res.data);
260
384
  // The corpus shrank under this page (sessions pruned between loads):
@@ -329,6 +453,11 @@
329
453
 
330
454
  // 2. Fetch the first page (Archon-gated + audited server-side).
331
455
  mountPager();
456
+ // The remembered kind is applied BEFORE the first fetch so a returning reader
457
+ // lands on their own lens rather than seeing the whole corpus paint and then
458
+ // snap to a narrower one.
459
+ state.sessionType = readStoredType();
460
+ wireTypeFilter();
332
461
  await fetchPage();
333
462
  }
334
463