@bongos/core 1.19.804 → 1.19.806

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 (241) hide show
  1. package/.bongos-core.json +267 -242
  2. package/.claude/skills/ask-for-help/SKILL.md +6 -6
  3. package/.claude/skills/backlog-review/SKILL.md +1 -1
  4. package/.claude/skills/blocker-review/SKILL.md +7 -7
  5. package/.claude/skills/blocker-solve/SKILL.md +5 -5
  6. package/.claude/skills/bug-triage/SKILL.md +7 -7
  7. package/.claude/skills/builder-claim/SKILL.md +3 -3
  8. package/.claude/skills/builder-cost/SKILL.md +2 -2
  9. package/.claude/skills/builder-end/SKILL.md +2 -2
  10. package/.claude/skills/builder-exit/SKILL.md +7 -7
  11. package/.claude/skills/builder-key/SKILL.md +2 -2
  12. package/.claude/skills/builder-reauth/SKILL.md +2 -2
  13. package/.claude/skills/builder-redteam/SKILL.md +7 -7
  14. package/.claude/skills/builder-release/SKILL.md +2 -2
  15. package/.claude/skills/builder-sequence/SKILL.md +1 -1
  16. package/.claude/skills/builder-setup/SKILL.md +3 -3
  17. package/.claude/skills/builder-ship/SKILL.md +1 -1
  18. package/.claude/skills/builder-stage/SKILL.md +1 -1
  19. package/.claude/skills/builder-start/SKILL.md +1 -1
  20. package/.claude/skills/fix-task/SKILL.md +4 -4
  21. package/.claude/skills/goal-create/SKILL.md +18 -18
  22. package/.claude/skills/goal-review/SKILL.md +7 -7
  23. package/.claude/skills/grade-audit/SKILL.md +5 -5
  24. package/.claude/skills/grade-recover/SKILL.md +4 -4
  25. package/.claude/skills/grader-health/SKILL.md +6 -6
  26. package/.claude/skills/idea-triage/SKILL.md +14 -14
  27. package/.claude/skills/merge-mode/SKILL.md +3 -3
  28. package/.claude/skills/planning-session/SKILL.md +20 -20
  29. package/.claude/skills/priority-session/SKILL.md +10 -10
  30. package/.claude/skills/recall/SKILL.md +1 -1
  31. package/.claude/skills/scan-before-install/SKILL.md +3 -3
  32. package/.claude/skills/status/SKILL.md +2 -2
  33. package/.claude/skills/strand-fix/SKILL.md +2 -2
  34. package/clients/bongos-client/README.md +1 -1
  35. package/clients/bongos-client/bongos-client.global.js +2 -0
  36. package/clients/bongos-client/index.cjs +2 -0
  37. package/clients/bongos-client/index.d.ts +2 -0
  38. package/clients/bongos-client/index.mjs +2 -0
  39. package/docs/api/openapi.json +45 -11
  40. package/docs/api-reference.md +11 -10
  41. package/docs/copy-inventory.md +47 -34
  42. package/docs/copy-registry.json +200 -72
  43. package/docs/module-api-changelog.md +4 -0
  44. package/modules/builder-settings/profile-nudge-prefs.js +1 -1
  45. package/modules/builder-settings/render-prefs.js +1 -1
  46. package/modules/dev-box/app/README.md +1 -1
  47. package/modules/dev-box/app/src/gds-client.js +11 -9
  48. package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
  49. package/modules/dev-box/app/test/unit.mjs +13 -13
  50. package/modules/dev-box/box-access.js +3 -3
  51. package/modules/dev-box/box-onboard.js +7 -7
  52. package/modules/discord/bug-attachments.js +1 -1
  53. package/modules/economy/cost-classify.js +6 -6
  54. package/modules/grading/grader-rubric.json +1 -1
  55. package/modules/grading/grader-score.js +1 -1
  56. package/modules/hall-ui/module.json +1 -1
  57. package/modules/hall-ui/public/drachmae.js +3 -3
  58. package/modules/hall-ui/public/goals-page.js +1 -1
  59. package/modules/hall-ui/public/goals-render.js +144 -22
  60. package/modules/hall-ui/public/goals.css +16 -1
  61. package/modules/hall-ui/public/idea.js +1 -1
  62. package/modules/hall-ui/public/md-reader.js +1 -1
  63. package/modules/hall-ui/public/people.html +1 -1
  64. package/modules/hall-ui/public/people.js +2 -2
  65. package/modules/hall-ui/public/primer.js +4 -4
  66. package/modules/hall-ui/public/roster.js +1 -1
  67. package/modules/hall-ui/public/shell.js +1 -1
  68. package/modules/hall-ui/public/task.js +1 -1
  69. package/modules/ideas/inbox.js +2 -2
  70. package/modules/lifecycle/db-analytics.js +1 -1
  71. package/modules/lifecycle/done-when.js +4 -4
  72. package/modules/lifecycle/version-close.js +1 -1
  73. package/modules/memory/learnings.js +2 -2
  74. package/modules/memory/memory.js +1 -1
  75. package/modules/onboarding/onboarding.js +1 -1
  76. package/modules/platform-identity/routes/my-projects.js +1 -1
  77. package/modules/platform-identity/routes/sso.js +2 -2
  78. package/modules/public-landing/public/index.html +2 -2
  79. package/modules/public-landing/public/projects-floor.states.json +1 -1
  80. package/modules/public-landing/public/projects.html +1 -1
  81. package/modules/public-landing/public/projects.states.json +1 -1
  82. package/modules/sessions/db.js +120 -0
  83. package/modules/sessions/module.json +2 -2
  84. package/modules/sessions/pollers/session-upload-health.js +111 -0
  85. package/modules/sessions/pulse.js +2 -2
  86. package/modules/sessions/routes/sessions.js +46 -0
  87. package/modules/sessions/upload-health.js +197 -0
  88. package/modules/status-ui/module.json +1 -1
  89. package/modules/status-ui/public/explore.js +1 -1
  90. package/package-lock.json +2 -2
  91. package/package.json +1 -1
  92. package/scripts/gds/agent-invoke.js +3 -3
  93. package/scripts/gds/api.js +4 -4
  94. package/scripts/gds/api.sh +1 -1
  95. package/scripts/gds/architect-audit.js +1 -1
  96. package/scripts/gds/artifact-audit.js +2 -2
  97. package/scripts/gds/audit-ships.js +2 -2
  98. package/scripts/gds/backfill-cost.js +1 -1
  99. package/scripts/gds/backfill-sessions.js +1 -1
  100. package/scripts/gds/backfill-subagent-rewards.js +1 -1
  101. package/scripts/gds/backfill-task-rewards.js +2 -2
  102. package/scripts/gds/box-auth-check.js +1 -1
  103. package/scripts/gds/box-sync.js +3 -3
  104. package/scripts/gds/box.js +10 -10
  105. package/scripts/gds/build-cli-package.js +3 -3
  106. package/scripts/gds/capture.js +2 -2
  107. package/scripts/gds/ci-grade.js +1 -1
  108. package/scripts/gds/claim-recall.js +2 -2
  109. package/scripts/gds/cli-lib.js +3 -3
  110. package/scripts/gds/context-pack.js +1 -1
  111. package/scripts/gds/copy-apply.js +2 -2
  112. package/scripts/gds/dev-lib.js +2 -2
  113. package/scripts/gds/fetch-art-key.js +2 -2
  114. package/scripts/gds/fetch-sound-prefs.js +3 -3
  115. package/scripts/gds/grade-smoke.js +1 -1
  116. package/scripts/gds/hooks/pre-push.js +1 -1
  117. package/scripts/gds/init.js +9 -9
  118. package/scripts/gds/key-card.js +1 -1
  119. package/scripts/gds/learning-capture.js +2 -2
  120. package/scripts/gds/linkify-refs.js +2 -2
  121. package/scripts/gds/local-preview-lib.js +1 -1
  122. package/scripts/gds/login.js +11 -10
  123. package/scripts/gds/main-audit.js +1 -1
  124. package/scripts/gds/newcomer-floor.config.json +1 -1
  125. package/scripts/gds/newcomer-floor.js +2 -2
  126. package/scripts/gds/newcomer-floor.templates.json +2 -2
  127. package/scripts/gds/onboarding-ship.js +1 -1
  128. package/scripts/gds/paste-token.js +3 -3
  129. package/scripts/gds/portability-smoke-memory.sh +5 -5
  130. package/scripts/gds/prove-strict-versioning.js +2 -2
  131. package/scripts/gds/provision-net.js +2 -2
  132. package/scripts/gds/provision-units.js +3 -3
  133. package/scripts/gds/pulse.js +2 -2
  134. package/scripts/gds/push-path-brief.js +2 -2
  135. package/scripts/gds/reauth.js +1 -1
  136. package/scripts/gds/recall.js +1 -1
  137. package/scripts/gds/redteam-patrol.js +3 -3
  138. package/scripts/gds/render-ideas.js +5 -5
  139. package/scripts/gds/repo-metrics.js +1 -1
  140. package/scripts/gds/sandbox-stage.js +3 -3
  141. package/scripts/gds/seed-adopt-tasks.js +3 -3
  142. package/scripts/gds/seed-board-room-tasks.js +1 -1
  143. package/scripts/gds/seed-bongos-app-tasks.js +2 -2
  144. package/scripts/gds/seed-bongos-coreB-tranche1-tasks.js +3 -3
  145. package/scripts/gds/seed-bongos-coreB-tranche2-tasks.js +4 -4
  146. package/scripts/gds/seed-bongos-tasks.js +5 -5
  147. package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +1 -1
  148. package/scripts/gds/seed-bongos-v1-seo-tasks.js +1 -1
  149. package/scripts/gds/seed-bv1-community-tasks.js +1 -1
  150. package/scripts/gds/seed-bv1-privacy-invites-tasks.js +1 -1
  151. package/scripts/gds/seed-bv1-profile-page-tasks.js +1 -1
  152. package/scripts/gds/seed-bv1-profiles-tasks.js +1 -1
  153. package/scripts/gds/seed-bv1-r01-delta-tasks.js +1 -1
  154. package/scripts/gds/seed-gds-v4-tasks.js +3 -3
  155. package/scripts/gds/seed-medusa-tasks.js +1 -1
  156. package/scripts/gds/seed-productization-tasks.js +1 -1
  157. package/scripts/gds/seed-project-admin-console-tasks.js +1 -1
  158. package/scripts/gds/seed-provisioning-tasks.js +2 -2
  159. package/scripts/gds/seed-strict-versioning-tasks.js +3 -3
  160. package/scripts/gds/seed-v2-world-tasks.js +2 -2
  161. package/scripts/gds/seed-verify-criteria.js +1 -1
  162. package/scripts/gds/session-digest.js +1 -1
  163. package/scripts/gds/session.js +2 -2
  164. package/scripts/gds/setup.js +7 -7
  165. package/scripts/gds/ship-flow.js +3 -3
  166. package/scripts/gds/ship-grade-steps.js +3 -3
  167. package/scripts/gds/ship-memory-sync.js +1 -1
  168. package/scripts/gds/ship-session-upload.js +3 -3
  169. package/scripts/gds/smoke-batch-claim.sh +10 -10
  170. package/scripts/gds/smoke-batch-release.sh +14 -14
  171. package/scripts/gds/smoke-claim-consistency.sh +19 -19
  172. package/scripts/gds/smoke-dependencies.sh +9 -9
  173. package/scripts/gds/smoke-write.sh +11 -11
  174. package/scripts/gds/start.js +1 -1
  175. package/scripts/gds/status.js +2 -2
  176. package/scripts/gds/strand-watch.js +1 -1
  177. package/scripts/gds/triage.js +4 -4
  178. package/scripts/gds/version-close.js +2 -2
  179. package/scripts/hall-preview/README.md +1 -1
  180. package/scripts/hall-preview/server.js +1 -1
  181. package/src/bongos/auth-admission.js +3 -3
  182. package/src/bongos/auth-idp.js +2 -2
  183. package/src/bongos/auth.js +3 -3
  184. package/src/bongos/db.js +2 -2
  185. package/src/bongos/llm-cache.js +2 -2
  186. package/src/bongos/metrics.js +1 -1
  187. package/src/bongos/middleware/audit.js +3 -3
  188. package/src/bongos/middleware/rate-limit.js +1 -1
  189. package/src/bongos/platform-visibility-gate.js +2 -2
  190. package/src/bongos/routes/_helpers.js +1 -1
  191. package/src/bongos/routes/audit-log.js +1 -1
  192. package/src/bongos/routes/auth.js +5 -5
  193. package/src/bongos/routes/backup.js +1 -1
  194. package/src/bongos/routes/builders.js +1 -1
  195. package/src/bongos/routes/instance.js +1 -1
  196. package/src/bongos/routes/live.js +1 -1
  197. package/src/bongos/routes/me.js +2 -2
  198. package/src/bongos/routes/modules.js +2 -2
  199. package/src/bongos/routes/public.js +1 -1
  200. package/src/bongos/routes/security.js +4 -4
  201. package/src/bongos/routes.js +1 -1
  202. package/src/bongos/serve-internal.js +6 -6
  203. package/src/branding.js +1 -1
  204. package/src/module-api.js +1 -1
  205. package/src/module-seams.js +2 -2
  206. package/src/platform-server.js +2 -2
  207. package/tests/api_alias_exceptions.mjs +67 -0
  208. package/tests/application_echo.mjs +12 -12
  209. package/tests/artifact_audit.mjs +1 -1
  210. package/tests/auth_page_gate.mjs +4 -4
  211. package/tests/auth_web_recovery.mjs +11 -11
  212. package/tests/bongos_login.mjs +7 -4
  213. package/tests/box_env.mjs +6 -6
  214. package/tests/bug_attachments.mjs +1 -1
  215. package/tests/cli_sessions.mjs +1 -1
  216. package/tests/dev_lib.mjs +1 -1
  217. package/tests/discord_bugs.mjs +2 -2
  218. package/tests/discord_inbound.mjs +2 -2
  219. package/tests/explore_visible_states.mjs +1 -1
  220. package/tests/goal_invite_consent.mjs +307 -0
  221. package/tests/government_page_gates.mjs +1 -1
  222. package/tests/hall_reference_world.mjs +1 -1
  223. package/tests/idea_ideate_full.mjs +1 -1
  224. package/tests/idp_federation.mjs +2 -2
  225. package/tests/llm-cache.mjs +1 -1
  226. package/tests/manage_manifest_shared_read.mjs +3 -3
  227. package/tests/people_directory_clip.mjs +1 -1
  228. package/tests/planning_session_skill.mjs +1 -1
  229. package/tests/platform_visibility_gate.mjs +15 -15
  230. package/tests/profile_route.mjs +25 -25
  231. package/tests/projects_hub.mjs +2 -2
  232. package/tests/provision.mjs +2 -2
  233. package/tests/provision_settings_apply.mjs +4 -4
  234. package/tests/sandbox_stage.mjs +8 -8
  235. package/tests/scouting_owner_access_and_reach.mjs +3 -3
  236. package/tests/session_upload_health.mjs +329 -0
  237. package/tests/setup_device_flow_fallback.mjs +1 -1
  238. package/tests/skill_ask_for_help.mjs +3 -3
  239. package/tests/skill_grade_audit.mjs +2 -2
  240. package/tests/skill_grader_health.mjs +3 -3
  241. package/tests/smoke_write_contract.mjs +1 -1
@@ -5,7 +5,7 @@
5
5
  // inlines the five onboarding diagrams at matching sections, builds a TOC from
6
6
  // h2s with a scrollspy, drives a reading-progress hairline, and soft-gates the
7
7
  // footer ack button until the reader has scrolled past ~90% of the article.
8
- // Acking POSTs to /api/gds/builders/me/onboarding/primer-read (same endpoint
8
+ // Acking POSTs to /api/bongos/builders/me/onboarding/primer-read (same endpoint
9
9
  // /builder-primer-read uses).
10
10
  //
11
11
  // ONE PARSER, TWO READERS (task 1003306). This file used to carry its own copy
@@ -20,7 +20,7 @@
20
20
  (() => {
21
21
  'use strict';
22
22
 
23
- const API = '/api/gds';
23
+ const API = '/api/bongos';
24
24
  const ACK_SCROLL_THRESHOLD = 0.90; // 90% of article scrolled → button enables
25
25
  // The primer markdown lives at docs/onboarding/primer.md, so its relative
26
26
  // links resolve against docs/onboarding/.
@@ -30,7 +30,7 @@
30
30
  // URLs under these bases so the rendered links go somewhere useful instead of
31
31
  // 404-ing under /builders/*. The bases are NOT hardcoded here — that would
32
32
  // violate the V3.R12 (#236) authorship-attribution audit. They are fetched
33
- // from /api/gds/public/repo-info at boot; the server derives owner/repo at
33
+ // from /api/bongos/public/repo-info at boot; the server derives owner/repo at
34
34
  // process start from `git remote get-url origin` (src/bongos/repo-info.js).
35
35
  // When they are unavailable the rewriter degrades gracefully: repo-relative
36
36
  // hrefs pass through unchanged and 404 instead of crashing the page.
@@ -372,7 +372,7 @@
372
372
  // gracefully degrades when the bases are null.
373
373
  async function loadRepoInfoBases() {
374
374
  try {
375
- const r = await fetch('/api/gds/public/repo-info', { credentials: 'same-origin' });
375
+ const r = await fetch('/api/bongos/public/repo-info', { credentials: 'same-origin' });
376
376
  if (!r.ok) return;
377
377
  const data = await r.json();
378
378
  if (data && typeof data.blob_base === 'string') repoBlobBase = data.blob_base;
@@ -217,7 +217,7 @@
217
217
  rerender();
218
218
  }
219
219
 
220
- fetch('/api/gds/leaderboard', { headers: { Accept: 'application/json' } })
220
+ fetch('/api/bongos/leaderboard', { headers: { Accept: 'application/json' } })
221
221
  .then(function (r) {
222
222
  if (!r.ok) throw new Error('http ' + r.status);
223
223
  return r.json();
@@ -263,7 +263,7 @@
263
263
  // modules/hall-ui/CLAUDE.md uses, but Builders is what a reader scans for.
264
264
  { id: 'roster', label: 'Builders', icon: 'leaderboard', href: '/roster', gate: 'signed' },
265
265
  // task 1002577: the cross-project builder directory (/scouting graduated
266
- // into the hall). Gated to MATCH its data — /api/gds/scouting checks
266
+ // into the hall). Gated to MATCH its data — /api/bongos/scouting checks
267
267
  // `project.curate`, whose catalog floor is METIC. This read 'archon' until
268
268
  // task 1002710: the nav was stricter than the wall, so a Metic could open
269
269
  // /people by URL but never saw the link. Cosmetic either way —
@@ -15,7 +15,7 @@
15
15
  (() => {
16
16
  'use strict';
17
17
 
18
- const API = '/api/gds';
18
+ const API = '/api/bongos';
19
19
  // Generated typed client (task 1996 / R09), served as window.BongosClient by
20
20
  // /lib/bongos-client.js. baseUrl:'' passes the full same-origin paths this file
21
21
  // builds through unchanged; result-mode ({ status, ok, data }) preserves control flow 1:1.
@@ -36,7 +36,7 @@ const VALID_KINDS = [
36
36
  'unclassified',
37
37
  // V3.R31 (#252) — Metics propose new version criteria via idea_inbox with
38
38
  // structured body_md { target_version, criterion_md, sort_order_hint }.
39
- // Archon ratifies via POST /api/gds/inbox/:id/ratify, which atomically
39
+ // Archon ratifies via POST /api/bongos/inbox/:id/ratify, which atomically
40
40
  // inserts a done_when_criteria row, marks the idea promoted, awards karma,
41
41
  // and writes a criterion_proposal_audit row. The kind value gates the
42
42
  // ratify endpoint (assert kind='criterion-proposal' before promotion).
@@ -1222,7 +1222,7 @@ async function getIdeaGrade(ideaId) {
1222
1222
  // the proposer (delta=+3, reason='proposal_ratified'), and writes an audit
1223
1223
  // row capturing who proposed / who ratified / when / what.
1224
1224
  //
1225
- // Called by POST /api/gds/inbox/:id/ratify (Archon-only, gated by requireRank
1225
+ // Called by POST /api/bongos/inbox/:id/ratify (Archon-only, gated by requireRank
1226
1226
  // in the router). The body_md of the source idea is parsed as JSON with the
1227
1227
  // shape { target_version, criterion_md, sort_order_hint }; malformed bodies
1228
1228
  // throw BAD_BODY so the route can return a structured 400.
@@ -500,7 +500,7 @@ async function ecosystemAnalytics(days = 90) {
500
500
  // The writing middleware (src/bongos/middleware/audit.js) calls insertAuditLog
501
501
  // AFTER the response has been sent, wrapped in try/catch — so the request
502
502
  // path never blocks on this insert and a DB failure here can never break a
503
- // real user request. The reader is the archon-only GET /api/gds/audit-log
503
+ // real user request. The reader is the archon-only GET /api/bongos/audit-log
504
504
  // endpoint backed by listAuditLog below (paginated, newest-first).
505
505
  // -------------------------------------------------------------------------
506
506
 
@@ -19,9 +19,9 @@
19
19
  //
20
20
  // Wired in via modules/lifecycle/routes/done-when.js (the loader mounts it,
21
21
  // BV1.R86). Three endpoints:
22
- // GET /api/gds/versions/:id/done-when — public, 60s cache
23
- // POST /api/gds/done-when/:criterionId/satisfy — auth
24
- // POST /api/gds/done-when/:criterionId/unsatisfy — auth
22
+ // GET /api/bongos/versions/:id/done-when — public, 60s cache
23
+ // POST /api/bongos/done-when/:criterionId/satisfy — auth
24
+ // POST /api/bongos/done-when/:criterionId/unsatisfy — auth
25
25
 
26
26
  const { pool } = require('../../src/module-api');
27
27
  const { nonTerminalSql } = require('./task-status');
@@ -165,7 +165,7 @@ async function listPendingReviewCriteria({ versionId = null } = {}, deps = {}) {
165
165
 
166
166
  // Roll up every criterion for a version together with the tasks that gate it
167
167
  // (via the task_criteria join table, migration 055) and a status breakdown.
168
- // This is the read behind GET /api/gds/versions/:id/progress and the /status
168
+ // This is the read behind GET /api/bongos/versions/:id/progress and the /status
169
169
  // skill — it answers "what's left for criterion Cn?" in one query instead of
170
170
  // the ~10-call prose archaeology that motivated #435.
171
171
  //
@@ -88,7 +88,7 @@ async function suggestClose(versionId) {
88
88
  }
89
89
 
90
90
  // Suggestions for every version still in 'building' status.
91
- // Used by GET /api/gds/versions/close-suggestions and by /builder-start.
91
+ // Used by GET /api/bongos/versions/close-suggestions and by /builder-start.
92
92
  async function getCloseSuggestionsForBuildingVersions() {
93
93
  const { rows: versions } = await pool.query(
94
94
  `SELECT id FROM versions WHERE status = 'building' ORDER BY started_at, id`
@@ -9,7 +9,7 @@
9
9
  // indexes: GIN(tags), GIN(related_files),
10
10
  // UNIQUE(source, source_ref) WHERE non-null
11
11
  //
12
- // Wired into routes by src/bongos/routes.js (POST + GET /api/gds/learnings).
12
+ // Wired into routes by src/bongos/routes.js (POST + GET /api/bongos/learnings).
13
13
 
14
14
  // Carved into modules/memory/ (BV1.R72 / ADR 0091 §2): pool via the doorway.
15
15
  const crypto = require('crypto');
@@ -178,7 +178,7 @@ async function findLearningsByTags(tags, { limit = 50 } = {}) {
178
178
  //
179
179
  // Consumers:
180
180
  // - scripts/gds/claim.js calls this indirectly via
181
- // GET /api/gds/learnings?touches=a,b,c so /builder-claim N can surface
181
+ // GET /api/bongos/learnings?touches=a,b,c so /builder-claim N can surface
182
182
  // up to 3 most-recent relevant learnings right after a successful claim.
183
183
  // Query overhead is bounded by the GIN(tags) + GIN(related_files) indexes
184
184
  // declared in migration 006_pms_v2.sql (<50ms target).
@@ -494,7 +494,7 @@ async function bfgWriteMemoryFile({ targetBuilderId, path, content, audit, karma
494
494
  RETURNING id, recorded_at`,
495
495
  [
496
496
  a.principalId ?? null,
497
- a.route ?? '/api/gds/memory/builders/:builderId/bfg-write',
497
+ a.route ?? '/api/bongos/memory/builders/:builderId/bfg-write',
498
498
  {
499
499
  action: a.action || 'bfg_dream_write',
500
500
  http_method: 'POST',
@@ -429,7 +429,7 @@ function shortSession(sessionId) {
429
429
  return String(sessionId).slice(0, 8);
430
430
  }
431
431
 
432
- // Build the { title, body_md, kind } payload for POST /api/gds/inbox. The
432
+ // Build the { title, body_md, kind } payload for POST /api/bongos/inbox. The
433
433
  // title carries ONBOARDING_FRICTION_TAG so triage (and the future kind
434
434
  // migration) can find these rows; the body holds the subagent summary, each
435
435
  // friction moment, and the deterministic signals as an audit trail.
@@ -177,7 +177,7 @@ module.exports = function myProjectsRoutes() {
177
177
  // the registry-trusted origin, never the caller's raw input.
178
178
  let outcome = null;
179
179
  try {
180
- const r = await fetch(`${client.origin}/api/gds/access-requests`, {
180
+ const r = await fetch(`${client.origin}/api/bongos/access-requests`, {
181
181
  method: 'POST',
182
182
  headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'User-Agent': userAgent() },
183
183
  body: JSON.stringify({
@@ -120,7 +120,7 @@ async function notifyInstanceLogout(membership, githubId) {
120
120
  const ac = new AbortController();
121
121
  const timer = setTimeout(() => ac.abort(), SLO_TIMEOUT_MS);
122
122
  try {
123
- const resp = await fetch(`${origin}/api/gds/auth/backchannel-logout`, {
123
+ const resp = await fetch(`${origin}/api/bongos/auth/backchannel-logout`, {
124
124
  method: 'POST',
125
125
  headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
126
126
  body: JSON.stringify({ logout_token: token }),
@@ -260,7 +260,7 @@ module.exports = function ssoRoutes() {
260
260
  const usable = pi.sessionUsableForFederation(session);
261
261
  if (!usable.ok) {
262
262
  if (usable.reason === 'no_session') {
263
- return res.redirect(302, `/api/gds/auth/web/start?return=${encodeURIComponent(req.originalUrl)}`);
263
+ return res.redirect(302, `/api/bongos/auth/web/start?return=${encodeURIComponent(req.originalUrl)}`);
264
264
  }
265
265
  const msg = usable.reason === 'inactive'
266
266
  ? 'This account is no longer active. Ask an Archon on the hub if you believe this is an error.'
@@ -632,7 +632,7 @@ baked apex number sits inside apex-only sentinels so a child instance shows an e
632
632
  its own feed answers):
633
633
  data-ledger="line" = the first progress row whose status is building (else the first row), read
634
634
  as "<shipped> of <total> shipped on <V>"; data-ledger="meter" is that ratio.
635
- data-rows = /api/gds/public/recent-shipped?limit=6. Titles are quoted data: an em or en
635
+ data-rows = /api/bongos/public/recent-shipped?limit=6. Titles are quoted data: an em or en
636
636
  dash becomes a comma and a trailing parenthetical is dropped at render time.
637
637
  A child instance with no ledger yet answers {tasks:[]}, so the meter, the
638
638
  sub-line and the list collapse rather than promise six rows that never
@@ -910,7 +910,7 @@ composition A; landing composition v3, picked by the owner 2026-08-27. No roll,
910
910
  <script>
911
911
  (function () {
912
912
  var API = '/api/bongos';
913
- var API_ROOT = '/api/gds';
913
+ var API_ROOT = '/api/bongos';
914
914
 
915
915
  /* ── the tagline. It is ONE server-stamped string, but the composition wants it
916
916
  on two lines, so the two halves are baked as separate spans. A host whose
@@ -16,7 +16,7 @@
16
16
  "/my-projects",
17
17
  "/provisioning/instances/999",
18
18
  "/provisioning/instances/101$",
19
- "/api/gds/instance$"
19
+ "/api/bongos/instance$"
20
20
  ],
21
21
  "ignoreConsole": [
22
22
  "/api/(gds|bongos)/me",
@@ -6181,7 +6181,7 @@ summary{min-height:24px;padding:3px 0;}
6181
6181
  manifestFor = manageId;
6182
6182
  var ctl = typeof AbortController === 'function' ? new AbortController() : null;
6183
6183
  var timer = ctl ? setTimeout(function () { ctl.abort(); }, 8000) : null;
6184
- manifestPromise = fetch('https://' + domain + '/api/gds/instance', { mode: 'cors', signal: ctl ? ctl.signal : undefined })
6184
+ manifestPromise = fetch('https://' + domain + '/api/bongos/instance', { mode: 'cors', signal: ctl ? ctl.signal : undefined })
6185
6185
  .then(function (res) {
6186
6186
  /* a project that ANSWERED but refused (its own pre-launch gate, a proxy
6187
6187
  error page) is not an older core — carry the status, don't guess */
@@ -4,7 +4,7 @@
4
4
  "surface": "public-landing",
5
5
  "modes": ["dark"],
6
6
  "modeQuery": false,
7
- "ignoreRequests": ["/api/(gds|bongos)/me$", "/my-projects", "/provisioning/instances/999", "/provisioning/instances/101$", "/api/gds/instance$", "/github/repos$", "/provisioning/onboard-plan"],
7
+ "ignoreRequests": ["/api/(gds|bongos)/me$", "/my-projects", "/provisioning/instances/999", "/provisioning/instances/101$", "/api/bongos/instance$", "/github/repos$", "/provisioning/onboard-plan"],
8
8
  "ignoreConsole": ["/api/(gds|bongos)/me", "\\b401\\b", "\\b404\\b", "Unauthorized"],
9
9
  "ignoreControls": [".skip", ".vh"],
10
10
  "states": {
@@ -24,6 +24,10 @@
24
24
 
25
25
  const api = require('../../src/module-api');
26
26
  const { pool } = api;
27
+ // The PURE verdict + every clamp and threshold (task 1003865). The module's own
28
+ // file, not the doorway: it touches no kernel surface, and keeping the numbers
29
+ // in one place is what stops the route and the poller disagreeing.
30
+ const uploadHealth = require('./upload-health');
27
31
  // `seams` is the doorway's kernel-mediated seam surface (resolveOptional/resolve).
28
32
  // upsertSessionRecordWithReward resolves the economy `reward` port through it —
29
33
  // the module↔module seam, never a sibling-module require (BV1.R77 / ADR 0093 §2).
@@ -656,6 +660,118 @@ async function filterTaskIdsClaimedBy(builderId, taskIds) {
656
660
  return rows.map((r) => Number(r.task_id));
657
661
  }
658
662
 
663
+
664
+
665
+ // -------------------------------------------------------------------------
666
+ // Session-upload health (task 1003865) — is the corpus still being written?
667
+ // -------------------------------------------------------------------------
668
+ //
669
+ // THE SIGNAL. The server already knows both halves independently: a task reached
670
+ // status='shipped', and whether that task's builder uploaded any session around
671
+ // then. A ship with no session record for its builder inside a window is the unit
672
+ // of evidence. Before this, the ONLY trace of a skipped upload was a jsonl file on
673
+ // the builder's own laptop, so a weeks-long outage that stopped every cost-plus
674
+ // reward from booking was invisible from the server.
675
+ //
676
+ // GROUPED BY BUILDER, which is the entire design and is measured, not assumed —
677
+ // see ../upload-health for the numbers. The corpus-wide share cannot see the
678
+ // outage (3.4% in its worst month, against 1.7-2.0% in healthy ones); per builder
679
+ // the same data fires on 13 days, all inside the outage window, all one builder.
680
+ //
681
+ // KEYED ON builder_id + TIME, NEVER ON task_ids. session_records.task_ids is
682
+ // EMPTY on 54% of rows (1,771 of 3,873 when task 1002448 measured it), so the
683
+ // obvious predicate — "no session record NAMES this task" — reports the linkage
684
+ // gap as an outage. Neither query below mentions task_ids, and a test asserts
685
+ // that about the SQL these functions actually emit.
686
+ //
687
+ // Every threshold and clamp lives in ../upload-health (PURE, unit-tested), so the
688
+ // route and the poller cannot answer this question two different ways.
689
+
690
+ // sessionUploadCoverage — per-BUILDER ship counts over the lookback, and how many
691
+ // of those ships had NO session record for that builder inside the window. One
692
+ // query, server-side, deliberately: /sessions/search is rate-limited per builder
693
+ // and clamps limit to 100, so paging the corpus from a client to compute this is
694
+ // neither cheap nor reliable (measured while scoping this task — 45 pages, with
695
+ // 429s).
696
+ async function sessionUploadCoverage({ days, windowHours } = {}) {
697
+ const d = uploadHealth.clampLookbackDays(days);
698
+ const w = uploadHealth.clampWindowHours(windowHours);
699
+ const { rows } = await pool.query(
700
+ `SELECT t.shipped_by AS builder_id,
701
+ b.github_login,
702
+ COUNT(*)::int AS ships,
703
+ COUNT(*) FILTER (
704
+ WHERE NOT EXISTS (
705
+ SELECT 1
706
+ FROM session_records sr
707
+ WHERE sr.builder_id = t.shipped_by
708
+ AND sr.uploaded_at >= t.shipped_at - ($2 * interval '1 hour')
709
+ AND sr.uploaded_at <= t.shipped_at + ($2 * interval '1 hour')
710
+ )
711
+ )::int AS uncovered
712
+ FROM tasks t
713
+ JOIN builders b ON b.id = t.shipped_by
714
+ WHERE t.status = 'shipped'
715
+ AND t.shipped_by IS NOT NULL
716
+ AND t.shipped_at IS NOT NULL
717
+ AND t.shipped_at >= now() - ($1 * interval '1 day')
718
+ GROUP BY t.shipped_by, b.github_login
719
+ ORDER BY 4 DESC`,
720
+ [d, w]
721
+ );
722
+ return { days: d, windowHours: w, rows };
723
+ }
724
+
725
+ // shipsMissingSessionRecord — the offending ships themselves, newest first. The
726
+ // verdict says how bad and whose; this says WHICH, because recovering the unpaid
727
+ // work needs the task ids and a percentage does not carry them. Bounded by
728
+ // clampSampleLimit: evidence, not a report.
729
+ async function shipsMissingSessionRecord({ days, windowHours, limit, builderId } = {}) {
730
+ const d = uploadHealth.clampLookbackDays(days);
731
+ const w = uploadHealth.clampWindowHours(windowHours);
732
+ const n = uploadHealth.clampSampleLimit(limit);
733
+ const b = Number.isFinite(Number(builderId)) ? Number(builderId) : null;
734
+ const { rows } = await pool.query(
735
+ `SELECT t.id, t.title, t.shipped_by, b.github_login, t.shipped_at
736
+ FROM tasks t
737
+ JOIN builders b ON b.id = t.shipped_by
738
+ WHERE t.status = 'shipped'
739
+ AND t.shipped_by IS NOT NULL
740
+ AND t.shipped_at IS NOT NULL
741
+ AND t.shipped_at >= now() - ($1 * interval '1 day')
742
+ AND ($3::bigint IS NULL OR t.shipped_by = $3)
743
+ AND NOT EXISTS (
744
+ SELECT 1
745
+ FROM session_records sr
746
+ WHERE sr.builder_id = t.shipped_by
747
+ AND sr.uploaded_at >= t.shipped_at - ($2 * interval '1 hour')
748
+ AND sr.uploaded_at <= t.shipped_at + ($2 * interval '1 hour')
749
+ )
750
+ ORDER BY t.shipped_at DESC
751
+ LIMIT ${n}`,
752
+ [d, w, b]
753
+ );
754
+ return rows;
755
+ }
756
+
757
+ // sessionUploadHealth — the whole answer in one call: the verdict, the per-builder
758
+ // counts behind it, and a sample of the ships it is counting. The route serves
759
+ // this and the poller logs from it, so neither owns the arithmetic. The sample is
760
+ // skipped entirely when nothing is uncovered — the healthy path costs one query.
761
+ async function sessionUploadHealth({ days, windowHours, sampleLimit } = {}) {
762
+ const coverage = await sessionUploadCoverage({ days, windowHours });
763
+ const verdict = uploadHealth.uploadHealthVerdict(coverage.rows);
764
+ const sample = verdict.uncovered > 0
765
+ ? await shipsMissingSessionRecord({ days, windowHours, limit: sampleLimit })
766
+ : [];
767
+ return {
768
+ days: coverage.days,
769
+ window_hours: coverage.windowHours,
770
+ verdict,
771
+ sample,
772
+ };
773
+ }
774
+
659
775
  module.exports = {
660
776
  // write side (6D.1)
661
777
  upsertSessionRecord,
@@ -667,6 +783,10 @@ module.exports = {
667
783
  // search + fetch (6D.2)
668
784
  searchSessions,
669
785
  summarizeSessions,
786
+ // upload health (task 1003865) — is the corpus still being written at all?
787
+ sessionUploadCoverage,
788
+ shipsMissingSessionRecord,
789
+ sessionUploadHealth,
670
790
  getSessionById,
671
791
  listSessionsByBuilder,
672
792
  filterTaskIdsClaimedBy,
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "key": "sessions",
3
3
  "title": "Sessions & BFG (session corpus · efficiency eval)",
4
- "description": "The session-records corpus + its data plane (POST /sessions upload, the Archon-gated cross-builder /sessions/search + /sessions/:id deep-dive), the BFG 'read-the-room' cross-builder scorer (ADR 0026 §6B), and the session-pulse / stale-claim heartbeat helpers. A core-domain module carved out of the GDS core in BV1.R82 (ADR 0093 §1); default-on because every instance records its builders' sessions for the BFG. Like the memory module, sessions registers no port — core never calls back into it: the ship-time digest write is an HTTP POST to the module's own /sessions route, and the only outward dependency is the economy `reward` kernel PORT it resolves (session token reward + sumCreditsForTasks) at upload time.",
4
+ "description": "The session-records corpus + its data plane (POST /sessions upload, the Archon-gated cross-builder /sessions/search + /sessions/:id deep-dive + /sessions/upload-health, the server-side signal that a builder's session uploads have silently stopped), the BFG 'read-the-room' cross-builder scorer (ADR 0026 §6B), and the session-pulse / stale-claim heartbeat helpers. A core-domain module carved out of the GDS core in BV1.R82 (ADR 0093 §1); default-on because every instance records its builders' sessions for the BFG. Like the memory module, sessions registers no port — core never calls back into it: the ship-time digest write is an HTTP POST to the module's own /sessions route, and the only outward dependency is the economy `reward` kernel PORT it resolves (session token reward + sumCreditsForTasks) at upload time.",
5
5
  "coreVersion": "^1.10.0",
6
6
  "default": true,
7
7
  "maintenance": { "status": "core-maintained" },
8
- "contributes": { "routes": ["sessions"], "pollers": ["session-reward-reconcile"] },
8
+ "contributes": { "routes": ["sessions"], "pollers": ["session-reward-reconcile", "session-upload-health"] },
9
9
  "provides": [],
10
10
  "consumes": []
11
11
  }
@@ -0,0 +1,111 @@
1
+ 'use strict';
2
+
3
+ // modules/sessions/pollers/session-upload-health.js — the half of task 1003865
4
+ // that makes the signal NOTICE rather than merely be answerable.
5
+ //
6
+ // GET /sessions/upload-health makes the outage visible to anyone who asks. Nobody
7
+ // asked for weeks, which is the whole incident: the failure mode is silence, and a
8
+ // number you have to go and look at is silent too. So this sweep asks on a
9
+ // schedule and writes the answer where the server's own operator surface already
10
+ // is — the process log, beside the reward reconciler's lines, which is what a
11
+ // journalctl over this unit is read for.
12
+ //
13
+ // SEPARATE FROM session-reward-reconcile ON PURPOSE. That poller MOVES MONEY; this
14
+ // one only reports. Folding telemetry into the payout sweep would let a bad read
15
+ // here delay or crash a booking there, and the two want different cadences —
16
+ // reward recovery wants minutes, a 14-day coverage ratio does not change in ten.
17
+ //
18
+ // It writes nothing and books nothing. A check that could itself change state
19
+ // would need its own safety argument; this one needs none.
20
+
21
+ const db = require('../db');
22
+ const uploadHealth = require('../upload-health');
23
+ // The doorway's namespaced structured logger (task 1003130), not console: this
24
+ // module reaches the kernel only through module-api, and console.* in a server
25
+ // path is the count the fitness ratchet exists to shrink, not to grow.
26
+ const api = require('../../../src/module-api');
27
+ const log = api.logger('sessions');
28
+
29
+ // Hourly. The measurement behind the thresholds used a 14-day lookback stepped
30
+ // daily, so anything under a day is already far finer than the signal; hourly is
31
+ // chosen so a fresh outage surfaces within the same working session rather than
32
+ // the next morning.
33
+ const INTERVAL_MS = Number(process.env.SESSION_UPLOAD_HEALTH_MS) || 60 * 60_000;
34
+ const FIRST_SWEEP_DELAY_MS = Number(process.env.SESSION_UPLOAD_HEALTH_FIRST_MS) || 120_000;
35
+
36
+ let intervalTimer = null;
37
+ let firstTimer = null;
38
+
39
+ // logLinesFor — PURE, so the wording is pinned by a DB-free test. A log line that
40
+ // says "unhealthy" and no more is the same silence with a timestamp on it: the
41
+ // firing line has to name the builder, the arithmetic, and where to look next,
42
+ // because the person reading it at 2am did not scope this task.
43
+ function logLinesFor(health) {
44
+ const v = (health && health.verdict) || {};
45
+ const where = `GET /api/gds/sessions/upload-health?days=${health && health.days}`;
46
+ if (!v.firing) {
47
+ return [`[session-upload-health] ${v.status || 'unknown'} — ${v.reason || 'no reason given'}`];
48
+ }
49
+ const lines = [
50
+ `[session-upload-health] ALERT — ${v.reason}`,
51
+ `[session-upload-health] a missing session record means no session.token_reward booked (ADR 0146: cost-plus is the only reward stream), so this is unpaid shipped work, not just missing telemetry.`,
52
+ `[session-upload-health] look: ${where} · likely cause: the upload skipping client-side (session-upload-skips.jsonl on that builder's machine).`,
53
+ ];
54
+ const sample = Array.isArray(health.sample) ? health.sample.slice(0, 5) : [];
55
+ if (sample.length > 0) {
56
+ lines.push(`[session-upload-health] uncovered ships, newest first: ${sample.map((s) => `#${s.id}`).join(' ')}`);
57
+ }
58
+ return lines;
59
+ }
60
+
61
+ async function runSweep() {
62
+ const health = await db.sessionUploadHealth({
63
+ days: uploadHealth.DEFAULT_LOOKBACK_DAYS,
64
+ windowHours: uploadHealth.DEFAULT_WINDOW_HOURS,
65
+ });
66
+ const lines = logLinesFor(health);
67
+ // A firing sweep goes out at ERROR so it survives a level filter set to hide
68
+ // the hourly all-clear — the whole failure mode here is a signal nobody saw.
69
+ const write = health.verdict && health.verdict.firing ? log.error : log.info;
70
+ for (const line of lines) write(line);
71
+ return health;
72
+ }
73
+
74
+ // Start the periodic sweep. Idempotent (a second call is a no-op). Kill-switched
75
+ // by SESSION_UPLOAD_HEALTH_DISABLED=1. Timers are unref'd so they never hold the
76
+ // process open on a systemd restart (the session-reward-reconcile precedent).
77
+ function start() {
78
+ if (process.env.SESSION_UPLOAD_HEALTH_DISABLED === '1') {
79
+ log.info('[session-upload-health] disabled (SESSION_UPLOAD_HEALTH_DISABLED=1)');
80
+ return;
81
+ }
82
+ if (intervalTimer) return; // already started
83
+
84
+ const tick = () => {
85
+ runSweep().catch((e) =>
86
+ log.error(e, '[session-upload-health] sweep crashed')
87
+ );
88
+ };
89
+
90
+ firstTimer = setTimeout(tick, FIRST_SWEEP_DELAY_MS);
91
+ if (firstTimer.unref) firstTimer.unref();
92
+ intervalTimer = setInterval(tick, INTERVAL_MS);
93
+ if (intervalTimer.unref) intervalTimer.unref();
94
+
95
+ log.info(
96
+ `[session-upload-health] started — checking session-upload coverage every ${Math.round(INTERVAL_MS / 60_000)}m`
97
+ );
98
+ }
99
+
100
+ function stop() {
101
+ if (firstTimer) {
102
+ clearTimeout(firstTimer);
103
+ firstTimer = null;
104
+ }
105
+ if (intervalTimer) {
106
+ clearInterval(intervalTimer);
107
+ intervalTimer = null;
108
+ }
109
+ }
110
+
111
+ module.exports = { start, stop, runSweep, logLinesFor, INTERVAL_MS };
@@ -21,7 +21,7 @@
21
21
  // rows as data; a follow-up task is expected to add the marker
22
22
  // column + UPDATE write + builder surface in /builder-start.
23
23
  //
24
- // 2. POST /api/gds/pulse route is NOT wired here — routes.js is out
24
+ // 2. POST /api/bongos/pulse route is NOT wired here — routes.js is out
25
25
  // of touches[]. recordPulse() is exported for whichever future
26
26
  // request handler is created. The CLI shell at scripts/gds/pulse.js
27
27
  // posts to the intended endpoint and will start working once the
@@ -29,7 +29,7 @@
29
29
  //
30
30
  // Follow-up work (file new tasks when picked up):
31
31
  // a) New migration adding claims.pulse_lost_at timestamptz.
32
- // b) Route: POST /api/gds/pulse → recordPulse() (auth + claim
32
+ // b) Route: POST /api/bongos/pulse → recordPulse() (auth + claim
33
33
  // ownership check).
34
34
  // c) sweepStaleClaims() side-effect: UPDATE claims
35
35
  // SET pulse_lost_at = now() WHERE id = ANY(...).
@@ -6,6 +6,11 @@
6
6
  // POST /sessions — upload your OWN session digest (6D.1). Owner is
7
7
  // forced to the caller; body builder_id is ignored.
8
8
  // GET /sessions/mine — your own sessions (self-serve summary list).
9
+ // GET /sessions/upload-health
10
+ // — is the corpus still being written? Ships whose
11
+ // builder uploaded no session around them, per
12
+ // builder (task 1003865). Same cross-builder gate +
13
+ // audit as /search, because it reads the same corpus.
9
14
  // GET /sessions/search — cross-builder search over session + builder facets
10
15
  // (rank/karma/drachmae/tenure). Archon-gated +
11
16
  // audited (the BFG principal posture until 6C.1).
@@ -36,6 +41,11 @@ const db = require('../db');
36
41
  // the interaction profile as data precisely so it does not have to require
37
42
  // builder-settings, which ADR 0083 forbids.
38
43
  const { scoreAccessibility } = require('../accessibility');
44
+ // The PURE clamps behind /sessions/upload-health. The route audits the values it
45
+ // will actually query with, not the raw query string — an audit row naming
46
+ // `days=99999` for a read that ran at 365 is a record of something that did not
47
+ // happen (task 1003865).
48
+ const uploadHealth = require('../upload-health');
39
49
  const seams = api; // resolveOptional('reward' | 'builder-settings') — kernel-mediated ports
40
50
  const { validateOrRespond, parseId } = api;
41
51
 
@@ -358,6 +368,42 @@ module.exports = function buildSessionsRouter() {
358
368
  }
359
369
  });
360
370
 
371
+ // GET /sessions/upload-health — the answer to "has anyone's session upload
372
+ // silently stopped?", which before task 1003865 existed ONLY as a jsonl file on
373
+ // the builder's own laptop. A skipped upload means no session_records row, which
374
+ // means the ADR 0146 cost-plus reward never books — a builder earning nothing for
375
+ // work that shipped, invisible from here.
376
+ //
377
+ // Read-only and side-effect-free, so it is DELIBERATELY not a new permission:
378
+ // it reads the same corpus /sessions/search reads, for the same cross-builder
379
+ // audience, so it rides the same `session.search` gate and writes the same
380
+ // fail-closed audit row. A new permission would be a new thing to grant before
381
+ // anyone could look at a number that already exists.
382
+ router.get('/sessions/upload-health', auth.requireBuilder, auth.requirePermission('session.search'), readRateLimit, async (req, res) => {
383
+ const q = req.query || {};
384
+ try {
385
+ // Audit BEFORE the query, like /search: no cross-builder read without a
386
+ // durable record of who asked.
387
+ await recordSessionRead(req, {
388
+ route: '/api/bongos/sessions/upload-health',
389
+ targetBuilderId: null,
390
+ target: `upload-health days=${uploadHealth.clampLookbackDays(q.days)} window_hours=${uploadHealth.clampWindowHours(q.window_hours)}`,
391
+ });
392
+ const health = await db.sessionUploadHealth({
393
+ days: q.days,
394
+ windowHours: q.window_hours,
395
+ sampleLimit: q.sample_limit,
396
+ });
397
+ res.json(health);
398
+ } catch (err) {
399
+ // The namespaced structured logger, not console — see the poller's note.
400
+ // Its siblings above still use console because they predate task 1003130;
401
+ // new call sites do not add to the count the ratchet exists to shrink.
402
+ api.logger('sessions').error(err, 'GET /sessions/upload-health failed');
403
+ res.fail('upload_health_failed', 500);
404
+ }
405
+ });
406
+
361
407
  // rank: any-builder — your OWN sessions (self-serve; no audit needed).
362
408
  router.get('/sessions/mine', auth.requireBuilder, readRateLimit, async (req, res) => {
363
409
  try {