@coreplane/switchboard 1.251.0 → 1.253.0

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 (167) hide show
  1. package/dist/assets/config/config.example.yaml +28 -2
  2. package/dist/assets/deploy/cloudflare/preflight.mjs +19 -21
  3. package/dist/assets/deploy/cloudflare/worker.ts +6 -3
  4. package/dist/assets/deploy/cloudflare-memory/runMetricsSink.ts +31 -0
  5. package/dist/assets/deploy/cloudflare-memory/worker.ts +223 -38
  6. package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +12 -0
  7. package/dist/assets/deploy/cloudflare-resident/memoryGuard.ts +212 -0
  8. package/dist/assets/deploy/cloudflare-resident/refresh.ts +1 -1
  9. package/dist/assets/deploy/cloudflare-resident/worker.ts +317 -56
  10. package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +4 -2
  11. package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.d.mts +31 -0
  12. package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.mjs +119 -0
  13. package/dist/assets/deploy/profile.example.json +1 -1
  14. package/dist/assets/package-lock.json +3 -3
  15. package/dist/assets/package.json +3 -2
  16. package/dist/assets/project.json +13 -9
  17. package/dist/assets/source.json +3 -3
  18. package/dist/assets/src/agents/registry.ts +5 -5
  19. package/dist/assets/src/core/authz/policy.ts +8 -0
  20. package/dist/assets/src/core/budgets.ts +39 -0
  21. package/dist/assets/src/core/coordinator/contract.ts +93 -1
  22. package/dist/assets/src/core/coordinator/driver.ts +236 -21
  23. package/dist/assets/src/core/drain.ts +50 -0
  24. package/dist/assets/src/core/memory/engine.ts +123 -4
  25. package/dist/assets/src/core/memory/scorer.ts +12 -4
  26. package/dist/assets/src/core/memory/types.ts +73 -12
  27. package/dist/assets/src/core/modelCard.ts +32 -4
  28. package/dist/assets/src/core/modelPricing.ts +111 -1
  29. package/dist/assets/src/core/modelProxy/usage.ts +88 -0
  30. package/dist/assets/src/core/modelRegistry.ts +15 -1
  31. package/dist/assets/src/core/refusal.ts +4 -7
  32. package/dist/assets/src/core/reviewVerdict.ts +4 -0
  33. package/dist/assets/src/core/runEvents.ts +82 -3
  34. package/dist/assets/src/core/runFriction.ts +8 -2
  35. package/dist/assets/src/core/runLedger/types.ts +11 -0
  36. package/dist/assets/src/core/runMetrics.ts +237 -0
  37. package/dist/assets/src/core/runRecord.ts +46 -0
  38. package/dist/assets/src/core/runUsage.ts +67 -13
  39. package/dist/assets/src/core/schedules.ts +3 -0
  40. package/dist/assets/src/core/ship/contract.ts +34 -14
  41. package/dist/assets/src/core/ship/coordinator.ts +485 -55
  42. package/dist/assets/src/core/ship/renewal.ts +10 -5
  43. package/dist/assets/src/core/trace/attrs.ts +24 -0
  44. package/dist/assets/src/core/types.ts +5 -5
  45. package/dist/assets/src/core/verbosity.ts +48 -0
  46. package/dist/assets/src/deploy/liveGate.ts +40 -13
  47. package/dist/assets/src/deploy/profile.ts +16 -0
  48. package/dist/assets/src/deploy/restart.ts +11 -12
  49. package/dist/assets/src/execution/residentDepCache.ts +50 -1
  50. package/dist/assets/src/execution/residentDepsStore.ts +40 -2
  51. package/dist/assets/src/execution/residentRefresh.ts +55 -3
  52. package/dist/assets/src/execution/residentSteps.ts +4 -0
  53. package/dist/assets/src/execution/sandboxErrors.ts +8 -0
  54. package/dist/assets/web/dist/.vite/manifest.json +444 -430
  55. package/dist/assets/web/dist/assets/{AppShell-DAtHiuI6.js → AppShell-Bmz8nzz3.js} +1 -1
  56. package/dist/assets/web/dist/assets/{CostsPage-BoKzYa4B.js → CostsPage-CxTKdpzU.js} +1 -1
  57. package/dist/assets/web/dist/assets/{DeliveryPage-DUXd-Sl-.js → DeliveryPage-C7jaEJip.js} +1 -1
  58. package/dist/assets/web/dist/assets/HomePage-cWbupQzy.js +3 -0
  59. package/dist/assets/web/dist/assets/{InputMenu-C6aPpe30.js → InputMenu-C9E7YoII.js} +1 -1
  60. package/dist/assets/web/dist/assets/{NotFoundPage-DtE-GgTk.js → NotFoundPage-4nlrq7DP.js} +1 -1
  61. package/dist/assets/web/dist/assets/{PendingTurnRow-DDhMhrI7.js → PendingTurnRow-C-pUgyyX.js} +1 -1
  62. package/dist/assets/web/dist/assets/PlanePage-BNy4tSgJ.js +1 -0
  63. package/dist/assets/web/dist/assets/{ResidentDetailPage-BnEoOnGQ.js → ResidentDetailPage-Dbtiq6_J.js} +1 -1
  64. package/dist/assets/web/dist/assets/{ResidentsIndexPage-Dxpgf-l-.js → ResidentsIndexPage-CKtz9H1o.js} +1 -1
  65. package/dist/assets/web/dist/assets/RunFoldRow-sYFtuK2G.js +1 -0
  66. package/dist/assets/web/dist/assets/RunRoutePage-Clh68nw9.js +9 -0
  67. package/dist/assets/web/dist/assets/RunsIndexPage-DPDbcOA_.js +1 -0
  68. package/dist/assets/web/dist/assets/{RunsTabs-YSbUu5py.js → RunsTabs-DotpuCSP.js} +1 -1
  69. package/dist/assets/web/dist/assets/{ScheduledPage-B_GgeJrb.js → ScheduledPage-C8FYoZy5.js} +1 -1
  70. package/dist/assets/web/dist/assets/{SettingSelect-BIzAsLk1.js → SettingSelect-CSiBQCSX.js} +1 -1
  71. package/dist/assets/web/dist/assets/{SettingsPage-BXX4R113.js → SettingsPage-CkYIhLUl.js} +1 -1
  72. package/dist/assets/web/dist/assets/{StatusDot-BOaw8le9.js → StatusDot-C0TUgnC1.js} +1 -1
  73. package/dist/assets/web/dist/assets/{Tooltip-DYZZ4l4V.js → Tooltip-CN5cZdl3.js} +1 -1
  74. package/dist/assets/web/dist/assets/{UnitRoutePage-BaSW5Odq.js → UnitRoutePage-D_EtAl8r.js} +1 -1
  75. package/dist/assets/web/dist/assets/{angular-html-BjeQZdCq.js → angular-html-qW40ZL9x.js} +1 -1
  76. package/dist/assets/web/dist/assets/{angular-ts-ZGdORrL2.js → angular-ts-PUtZU8Cq.js} +1 -1
  77. package/dist/assets/web/dist/assets/{apl-DhvV_X93.js → apl-D3fC7Qjd.js} +1 -1
  78. package/dist/assets/web/dist/assets/{astro-DiRSE4Ug.js → astro-Bi3JFHhh.js} +1 -1
  79. package/dist/assets/web/dist/assets/{blade-D8Fr5Wvq.js → blade-CSpsf9Z9.js} +1 -1
  80. package/dist/assets/web/dist/assets/budgets-CbIyPAER.js +1 -0
  81. package/dist/assets/web/dist/assets/{c-BwbC64D4.js → c-DE_b1oWP.js} +1 -1
  82. package/dist/assets/web/dist/assets/{chapel-DgmqRK97.js → chapel-CWXd9giL.js} +1 -1
  83. package/dist/assets/web/dist/assets/{cobol-DJod2RNK.js → cobol-CnMOmWNF.js} +1 -1
  84. package/dist/assets/web/dist/assets/{coffee-D1Y2CgBY.js → coffee-B9WvURu8.js} +1 -1
  85. package/dist/assets/web/dist/assets/{cpp-CsO97YOM.js → cpp-BZAJXlWI.js} +1 -1
  86. package/dist/assets/web/dist/assets/{crystal-CJ7m2tOc.js → crystal-Bm48wAlb.js} +1 -1
  87. package/dist/assets/web/dist/assets/{css-B2M-NKoj.js → css-WtMeC3ab.js} +1 -1
  88. package/dist/assets/web/dist/assets/{dist-BCVXeBJ9.js → dist-B1Mf2JbM.js} +2 -2
  89. package/dist/assets/web/dist/assets/{edge-CFVW-m9B.js → edge-BmwNa6Ss.js} +1 -1
  90. package/dist/assets/web/dist/assets/{elixir-B2mtRgCr.js → elixir-BotfP-Mn.js} +1 -1
  91. package/dist/assets/web/dist/assets/{elm-2teVAwCi.js → elm-DAFc6laN.js} +1 -1
  92. package/dist/assets/web/dist/assets/{erb-LAbeNqen.js → erb-QkOON3bB.js} +1 -1
  93. package/dist/assets/web/dist/assets/{git-rebase-DnhzH_sp.js → git-rebase-BPs-dd73.js} +1 -1
  94. package/dist/assets/web/dist/assets/{glimmer-js-DPKjXf1-.js → glimmer-js-D8eooepk.js} +1 -1
  95. package/dist/assets/web/dist/assets/{glimmer-ts-CABXnt5z.js → glimmer-ts-B0bRiDVg.js} +1 -1
  96. package/dist/assets/web/dist/assets/{glsl-Dz4-3-gJ.js → glsl-Bv6bMe1n.js} +1 -1
  97. package/dist/assets/web/dist/assets/{graphql-ByWUD1DL.js → graphql-Bh9y-n9o.js} +1 -1
  98. package/dist/assets/web/dist/assets/{hack-CdmOK2K-.js → hack-DSEbn7Ir.js} +1 -1
  99. package/dist/assets/web/dist/assets/{haml-DzegMpd2.js → haml-0NW1-z1f.js} +1 -1
  100. package/dist/assets/web/dist/assets/{handlebars-B5NeiP0e.js → handlebars-BfgCHFbK.js} +1 -1
  101. package/dist/assets/web/dist/assets/{html-Zg_NHLv9.js → html-CQHc0gQ5.js} +1 -1
  102. package/dist/assets/web/dist/assets/{html-derivative-DZf9JNzh.js → html-derivative-B0Tm7wLP.js} +1 -1
  103. package/dist/assets/web/dist/assets/{http-CVFrQZeN.js → http-GAuU3aOF.js} +1 -1
  104. package/dist/assets/web/dist/assets/{hurl-L5brWAJe.js → hurl-CrDFbF5B.js} +1 -1
  105. package/dist/assets/web/dist/assets/indexRow-BxKswdlK.js +1 -0
  106. package/dist/assets/web/dist/assets/{java-hFphyZ4R.js → java-DYv5KhUe.js} +1 -1
  107. package/dist/assets/web/dist/assets/{javascript-CAJO1WKX.js → javascript-32SiLGg0.js} +1 -1
  108. package/dist/assets/web/dist/assets/{jinja-N4A4srXI.js → jinja--gJDUGYa.js} +1 -1
  109. package/dist/assets/web/dist/assets/{jison-D3BZa_iU.js → jison-BlKLGzZH.js} +1 -1
  110. package/dist/assets/web/dist/assets/{json-DxD1Qh8W.js → json-BICanD1N.js} +1 -1
  111. package/dist/assets/web/dist/assets/{jsx-CHhag69S.js → jsx-CgW3-uFT.js} +1 -1
  112. package/dist/assets/web/dist/assets/{julia-CrhkZ6Tt.js → julia-BErRpwwL.js} +1 -1
  113. package/dist/assets/web/dist/assets/{just-D0LFOuhq.js → just-CordHv0h.js} +1 -1
  114. package/dist/assets/web/dist/assets/{latex-BHAlwCXK.js → latex-D7LVPLbi.js} +1 -1
  115. package/dist/assets/web/dist/assets/{liquid-CkaLfCWh.js → liquid-3ehz2i4U.js} +1 -1
  116. package/dist/assets/web/dist/assets/{lua-B1h4aAzP.js → lua-C9ppSVrm.js} +1 -1
  117. package/dist/assets/web/dist/assets/main-Cv0q9y56.js +28 -0
  118. package/dist/assets/web/dist/assets/main-DKFCM-sy.css +1 -0
  119. package/dist/assets/web/dist/assets/{marko-D6CG4hvX.js → marko-xBZLUBEJ.js} +1 -1
  120. package/dist/assets/web/dist/assets/{mdc-CVQzimAU.js → mdc-BJxGoVgD.js} +1 -1
  121. package/dist/assets/web/dist/assets/{nginx-h-9Ir54h.js → nginx-B-sZmpsb.js} +1 -1
  122. package/dist/assets/web/dist/assets/{nim-DHVI30sp.js → nim--s9wRABK.js} +1 -1
  123. package/dist/assets/web/dist/assets/{org-AasAXfMF.js → org-DoUvssoI.js} +1 -1
  124. package/dist/assets/web/dist/assets/{perl-BVi5aTEy.js → perl-B-vGHKOy.js} +1 -1
  125. package/dist/assets/web/dist/assets/{php-qOkCeUKr.js → php-CerWx_Xb.js} +1 -1
  126. package/dist/assets/web/dist/assets/{pug-8TnUGvEX.js → pug-CzKrBMb1.js} +1 -1
  127. package/dist/assets/web/dist/assets/{qml-CpJbhoxn.js → qml-Bs8L8I6k.js} +1 -1
  128. package/dist/assets/web/dist/assets/{r-JkUNcAJW.js → r-w6uG6mob.js} +1 -1
  129. package/dist/assets/web/dist/assets/{razor-CtU4ws5a.js → razor-dz2oj2bd.js} +1 -1
  130. package/dist/assets/web/dist/assets/{regexp-J2zA6ceN.js → regexp-CfQdprnY.js} +1 -1
  131. package/dist/assets/web/dist/assets/{rst-CMCvf-yZ.js → rst-DAOaX2tb.js} +1 -1
  132. package/dist/assets/web/dist/assets/{ruby-ChLBY_iR.js → ruby-QcCe4udB.js} +1 -1
  133. package/dist/assets/web/dist/assets/{sas-cwn4x2VY.js → sas-BrM4lAAc.js} +1 -1
  134. package/dist/assets/web/dist/assets/{scss-jSrJlzLI.js → scss-DVbJXoUb.js} +1 -1
  135. package/dist/assets/web/dist/assets/{shellscript-D5nAd0gs.js → shellscript-09fP2hZf.js} +1 -1
  136. package/dist/assets/web/dist/assets/{shellsession-BZY4-Tdh.js → shellsession-Qgr8xjZF.js} +1 -1
  137. package/dist/assets/web/dist/assets/{soy-COppPOKr.js → soy-DIYeNTuz.js} +1 -1
  138. package/dist/assets/web/dist/assets/{sql-Blc_6dOA.js → sql-CzLR9msV.js} +1 -1
  139. package/dist/assets/web/dist/assets/sseReplay-BYo2EZKy.js +11 -0
  140. package/dist/assets/web/dist/assets/{stata-DRv3OY-h.js → stata-zCY1ZzH0.js} +1 -1
  141. package/dist/assets/web/dist/assets/{surrealql-qMv_eeJ6.js → surrealql-CJIjTRV4.js} +1 -1
  142. package/dist/assets/web/dist/assets/{svelte-d1Edb6Rz.js → svelte-C5OTPFZg.js} +1 -1
  143. package/dist/assets/web/dist/assets/{templ-CUMSIOXG.js → templ-DYj-UICF.js} +1 -1
  144. package/dist/assets/web/dist/assets/{tex-BGZQA-Ei.js → tex-xcie-3Gd.js} +1 -1
  145. package/dist/assets/web/dist/assets/{ts-tags-Z9ulxVsq.js → ts-tags-CoN86VTt.js} +1 -1
  146. package/dist/assets/web/dist/assets/{tsx-oI4bI3Vf.js → tsx-n3-No_Br.js} +1 -1
  147. package/dist/assets/web/dist/assets/{twig-D_RXwBIX.js → twig-DPiJ7Vp1.js} +1 -1
  148. package/dist/assets/web/dist/assets/{typescript-BgqQDYoN.js → typescript-BP6HBaOp.js} +1 -1
  149. package/dist/assets/web/dist/assets/{typst-5MW-KRDd.js → typst-Do9ZjsVP.js} +1 -1
  150. package/dist/assets/web/dist/assets/{vue-CZhHHsPm.js → vue-CzAeXNRY.js} +1 -1
  151. package/dist/assets/web/dist/assets/{vue-html-DlYy2d8v.js → vue-html-D7iYmYjd.js} +1 -1
  152. package/dist/assets/web/dist/assets/{vue-vine-CJDnx-qn.js → vue-vine-DtrMRsJx.js} +1 -1
  153. package/dist/assets/web/dist/assets/{xml-BRBDk65Q.js → xml-DGJHo1c0.js} +1 -1
  154. package/dist/assets/web/dist/assets/{xsl-BlCykPxo.js → xsl-Q00kvNgW.js} +1 -1
  155. package/dist/assets/web/dist/assets/{yaml-BqW48szf.js → yaml-F-UGKaiO.js} +1 -1
  156. package/dist/cli.js +5623 -2455
  157. package/package.json +1 -1
  158. package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.sh +0 -68
  159. package/dist/assets/web/dist/assets/HomePage-mSiqEEcN.js +0 -2
  160. package/dist/assets/web/dist/assets/RunFoldRow-CSo4-vld.js +0 -1
  161. package/dist/assets/web/dist/assets/RunRoutePage-9klVWhSF.js +0 -9
  162. package/dist/assets/web/dist/assets/RunsIndexPage-BplMIgaw.js +0 -1
  163. package/dist/assets/web/dist/assets/budgets-BvWYKPsY.js +0 -1
  164. package/dist/assets/web/dist/assets/indexRow-Bde9OZxG.js +0 -1
  165. package/dist/assets/web/dist/assets/main-Dkcbtu3u.js +0 -28
  166. package/dist/assets/web/dist/assets/main-zbP_dTjR.css +0 -1
  167. package/dist/assets/web/dist/assets/sseReplay-DPwdsaok.js +0 -9
@@ -7,6 +7,7 @@ import {
7
7
  planEviction,
8
8
  planWrite,
9
9
  rankRecords,
10
+ rejectionMarkers,
10
11
  } from "../../src/core/memory/engine.ts";
11
12
  import { tokenize } from "../../src/core/memory/scorer.ts";
12
13
  import { FIRING_DETAIL_MAX, isScheduleFiring, type ScheduleFiring } from "../../src/core/schedules.ts";
@@ -118,6 +119,8 @@ import {
118
119
  type McpTicketState,
119
120
  type SealedCredential,
120
121
  } from "../../src/mcp/registry.ts";
122
+ import { isRunMetricsPoint, pointTurnsFinal, type RunMetricsPoint } from "../../src/core/runMetrics.ts";
123
+ import { AnalyticsEngineSink, NullSink, type RunMetricsSink } from "./runMetricsSink.ts";
121
124
  import { injectedBuildStamp } from "../../src/deploy/buildStamp.ts";
122
125
  import { systemClock } from "../../src/core/trace/clock.ts";
123
126
  import { createTracer } from "../../src/core/trace/tracer.ts";
@@ -147,7 +150,8 @@ const traceSinks = [workerLogSink((line) => console.log(line))];
147
150
  //
148
151
  // Route surface (JSON in/out; bearer MEMORY_TOKEN on everything but /healthz):
149
152
  // POST /retrieve {scopeKey, query, limit} → {records: MemoryRecord[]}
150
- // POST /write {scopeKey, records: MemoryCandidate[]} → {ok, inserted, deduped, superseded}
153
+ // POST /write {scopeKey, records: MemoryCandidate[]} → {ok, inserted, deduped, restated, superseded, evicted}
154
+ // POST /sweep {scopeKey, dryRun?} → {ok, swept} (+ ids under dryRun — the marked rows, flipped to `swept`)
151
155
  // GET /healthz → {ok:true} (deploy wake ping; touches no DO)
152
156
  // Scheduled-firing routes (the record behind the /runs Scheduled panel;
153
157
  // written by the bot's Worker shim after every cron firing, read by the bot's
@@ -159,7 +163,7 @@ const traceSinks = [workerLogSink((line) => console.log(line))];
159
163
  // store key, owning the retention policy. Same bearer; /runs/put has its own 2 MiB
160
164
  // body fence (a record is budgeted to 1.5 MiB upstream), every other route
161
165
  // keeps the 512 KB one.
162
- // POST /runs/put {storeKey, record, policy?, policyUpdatedAt?} → {ok, retained, stored, rewritten}
166
+ // POST /runs/put {storeKey, record, policy?, policyUpdatedAt?, point?} → {ok, retained, stored, rewritten}
163
167
  // POST /runs/get {storeKey, id} → {record: RunRecord | null} (unknown/expired: null, 200)
164
168
  // POST /runs/list {storeKey, limit?, before?, beforeId?, sinceMs?, agent?, channel?, threadKey?, parentRunId?}
165
169
  // → {items: RunListItem[], nextBefore?: {finishedAt, id}} (cursor = the last row's list key)
@@ -196,6 +200,14 @@ export interface Env {
196
200
  * binding is a cross-script one, and the class must exist on the bot before
197
201
  * the state Worker may name it), and a finish then commits with no event. */
198
202
  SHIP_COORDINATOR?: Workflow;
203
+ /** The run-metrics dataset (docs/reference/specs/run-metrics.md): where the
204
+ * RunHistoryDO writes one point per run whose row turned final. Optional
205
+ * like SHIP_COORDINATOR: this Worker deploys without it and every answer is
206
+ * byte-identical — the NullSink swallows the points. */
207
+ RUN_METRICS?: AnalyticsEngineDataset;
208
+ /** The dataset's name, rendered beside the binding, so `/healthz` can answer
209
+ * `runMetrics:<dataset>` and the bot's boot probe can compare names. */
210
+ RUN_METRICS_DATASET?: string;
199
211
  MEMORY_TOKEN?: string;
200
212
  }
201
213
 
@@ -379,8 +391,8 @@ export class MemoryDO extends DurableObject<Env> {
379
391
  scopeKey: string,
380
392
  candidates: MemoryCandidate[],
381
393
  cap: number = DEFAULT_SCOPE_CAP,
382
- ): Promise<{ inserted: number; deduped: number; superseded: number; evicted: number }> {
383
- const counts = { inserted: 0, deduped: 0, superseded: 0, evicted: 0 };
394
+ ): Promise<{ inserted: number; deduped: number; restated: number; superseded: number; evicted: number }> {
395
+ const counts = { inserted: 0, deduped: 0, restated: 0, superseded: 0, evicted: 0 };
384
396
  if (candidates.length === 0) return counts;
385
397
  this.ctx.storage.transactionSync(() => {
386
398
  let seq = this.sql.exec<{ next: number }>(`SELECT COALESCE(MAX(seq), -1) + 1 AS next FROM records`).one().next;
@@ -398,18 +410,41 @@ export class MemoryDO extends DurableObject<Env> {
398
410
  // own TRUTHINESS test: `supersedes: ""` passes validation but means NO
399
411
  // supersede to the engine, so it must dedup against the norm pool —
400
412
  // an `!== undefined` branch here would hand it an empty pool and
401
- // insert a duplicate active row.
402
- const relevant = (
403
- cand.supersedes
404
- ? this.sql.exec<Row>(`SELECT * FROM records WHERE id = ? AND status = 'active'`, cand.supersedes)
405
- : this.sql.exec<Row>(
406
- `SELECT * FROM records WHERE status = 'active' AND norm = ? ORDER BY seq`,
407
- normalizeText(cand.text),
413
+ // insert a duplicate active row. A `restates` id is looked up first
414
+ // (the restate target); when it misses — not active, or another
415
+ // scope's id, which this DO simply doesn't hold — the candidate takes
416
+ // today's pool, so planWrite falls through to dedup-or-insert.
417
+ const restatePool = cand.restates
418
+ ? this.sql
419
+ .exec<Row>(`SELECT * FROM records WHERE id = ? AND status = 'active'`, cand.restates)
420
+ .toArray()
421
+ .map(toRecord)
422
+ : [];
423
+ const relevant =
424
+ restatePool.length > 0
425
+ ? restatePool
426
+ : (cand.supersedes
427
+ ? this.sql.exec<Row>(`SELECT * FROM records WHERE id = ? AND status = 'active'`, cand.supersedes)
428
+ : this.sql.exec<Row>(
429
+ `SELECT * FROM records WHERE status = 'active' AND norm = ? ORDER BY seq`,
430
+ normalizeText(cand.text),
431
+ )
408
432
  )
409
- )
410
- .toArray()
411
- .map(toRecord);
433
+ .toArray()
434
+ .map(toRecord);
412
435
  const plan = planWrite(relevant, cand, (c) => mintRecord(scopeKey, seq++, now, c));
436
+ if (plan.action === "restate") {
437
+ // The shown record is refreshed in place; COALESCE keeps the stored
438
+ // confidence when the plan carries none (neither side had a value).
439
+ this.sql.exec(
440
+ `UPDATE records SET use_count = use_count + 1, last_used_at = ?, confidence = COALESCE(?, confidence) WHERE id = ?`,
441
+ now,
442
+ plan.confidence ?? null,
443
+ plan.target.id,
444
+ );
445
+ counts.restated++;
446
+ continue;
447
+ }
413
448
  if (plan.action === "dedup") {
414
449
  this.sql.exec(`UPDATE records SET use_count = use_count + 1 WHERE id = ?`, plan.target.id);
415
450
  counts.deduped++;
@@ -463,14 +498,19 @@ export class MemoryDO extends DurableObject<Env> {
463
498
  return counts;
464
499
  }
465
500
 
466
- /** Human view (docs/reference/specs/memory.md item 24): the scope's ACTIVE rows, newest first, no usage
467
- * bump. With `query`, only rows an FTS token hits (the same quoted-OR MATCH
468
- * as retrieve, so user text never reaches the FTS parser as syntax); a
469
- * query with no tokens lists nothing. */
470
- async list(_scopeKey: string, limit: number, query?: string): Promise<MemoryRecord[]> {
501
+ /** Human view and the repository window's read (docs/reference/specs/memory.md items 22, 26): the
502
+ * scope's ACTIVE rows, newest first, no usage bump. With `query`, only rows
503
+ * an FTS token hits (the same quoted-OR MATCH as retrieve, so user text
504
+ * never reaches the FTS parser as syntax); a query with no tokens lists
505
+ * nothing. With `kind`, only rows of that kind (the window lists facts). */
506
+ async list(_scopeKey: string, limit: number, query?: string, kind?: MemoryRecord["kind"]): Promise<MemoryRecord[]> {
471
507
  if (query === undefined) {
472
508
  return this.sql
473
- .exec<Row>(`SELECT * FROM records WHERE status = 'active' ORDER BY seq DESC LIMIT ?`, limit)
509
+ .exec<Row>(
510
+ `SELECT * FROM records WHERE status = 'active'${kind === undefined ? "" : " AND kind = ?"} ORDER BY seq DESC LIMIT ?`,
511
+ ...(kind === undefined ? [] : [kind]),
512
+ limit,
513
+ )
474
514
  .toArray()
475
515
  .map(toRecord);
476
516
  }
@@ -480,9 +520,10 @@ export class MemoryDO extends DurableObject<Env> {
480
520
  .exec<Row>(
481
521
  `SELECT r.* FROM records r
482
522
  JOIN records_fts f ON f.id = r.id
483
- WHERE r.status = 'active' AND records_fts MATCH ?
523
+ WHERE r.status = 'active'${kind === undefined ? "" : " AND r.kind = ?"} AND records_fts MATCH ?
484
524
  ORDER BY r.seq DESC
485
525
  LIMIT ?`,
526
+ ...(kind === undefined ? [] : [kind]),
486
527
  match,
487
528
  limit,
488
529
  )
@@ -504,6 +545,33 @@ export class MemoryDO extends DurableObject<Env> {
504
545
  return flipped;
505
546
  });
506
547
  }
548
+
549
+ /** Human control (docs/reference/specs/memory.md item 27): retire every
550
+ * ACTIVE fact whose text the write gate would reject today — the same
551
+ * `rejectionMarkers` the bot's write path runs, imported from the shared
552
+ * engine, so the sweep and the gate can never disagree. Summaries are never
553
+ * gated, so never swept. The scan, the flips and the FTS deletes run in ONE
554
+ * sync transaction (the per-scope cap's atomicity rule): the sweep commits
555
+ * whole or not at all. Idempotent — swept rows are no longer active, so a
556
+ * second call answers 0. Under `dryRun` nothing flips. */
557
+ async sweep(_scopeKey: string, dryRun: boolean): Promise<{ swept: number; ids: string[] }> {
558
+ return this.ctx.storage.transactionSync(() => {
559
+ const ids = this.sql
560
+ .exec<Row>(`SELECT * FROM records WHERE status = 'active' AND kind = 'fact'`)
561
+ .toArray()
562
+ .filter((row) => rejectionMarkers(row.text).length > 0)
563
+ .map((row) => row.id);
564
+ if (!dryRun) {
565
+ for (const id of ids) {
566
+ // Soft delete for the record row, hard delete for its FTS entry —
567
+ // the forget/supersede/evict hygiene rule (§15).
568
+ this.sql.exec(`UPDATE records SET status = 'swept' WHERE id = ? AND status = 'active'`, id);
569
+ this.sql.exec(`DELETE FROM records_fts WHERE id = ?`, id);
570
+ }
571
+ }
572
+ return { swept: ids.length, ids };
573
+ });
574
+ }
507
575
  }
508
576
 
509
577
  /** Build the FTS5 MATCH expression for a query: each engine token (`[a-z0-9]+`
@@ -1450,10 +1518,15 @@ type HeartbeatAnswer = FenceResult & { stop?: StopMode | null; phase?: LivePhase
1450
1518
 
1451
1519
  export class RunHistoryDO extends DurableObject<Env> {
1452
1520
  private readonly sql: SqlStorage;
1521
+ /** Where a turned-final run's point goes (run-metrics.md): the Analytics
1522
+ * Engine dataset when the deploy bound one, the NullSink otherwise —
1523
+ * selected once at construction, the `SHIP_COORDINATOR?` shape. */
1524
+ private readonly metrics: RunMetricsSink;
1453
1525
 
1454
1526
  constructor(ctx: DurableObjectState, env: Env) {
1455
1527
  super(ctx, env);
1456
1528
  this.sql = ctx.storage.sql;
1529
+ this.metrics = env.RUN_METRICS !== undefined ? new AnalyticsEngineSink(env.RUN_METRICS) : new NullSink();
1457
1530
  // Idempotent schema. `runs` carries the listing columns plus the record
1458
1531
  // minus its events as JSON (`summary_json`, what `list` returns); events
1459
1532
  // live one per row keyed (run_id, seq) so a 5000-event run is paged, never
@@ -2028,8 +2101,10 @@ export class RunHistoryDO extends DurableObject<Env> {
2028
2101
  gen: string,
2029
2102
  record: RunRecord,
2030
2103
  proposal?: RunPolicyProposal,
2104
+ point?: RunMetricsPoint,
2031
2105
  ): Promise<FenceResult & { stored?: boolean; event?: RunFinishedSend["kind"] }> {
2032
2106
  let out: FenceResult & { stored?: boolean } = { ok: true };
2107
+ let turnedFinal = false;
2033
2108
  this.ctx.storage.transactionSync(() => {
2034
2109
  const fence = checkFence(this.liveRow(runId), gen);
2035
2110
  if (!fence.ok) {
@@ -2038,9 +2113,14 @@ export class RunHistoryDO extends DurableObject<Env> {
2038
2113
  }
2039
2114
  const put = this.upsertInTransaction(record, proposal);
2040
2115
  this.deleteLiveRows([runId]);
2116
+ turnedFinal = put.turnedFinal;
2041
2117
  out = { ok: true, stored: put.stored };
2042
2118
  });
2043
2119
  if (!out.ok) return out;
2120
+ // The point after the commit, never inside it (`sendRunFinished`'s placement):
2121
+ // the finish usually replaces the start tombstone, so this is where most
2122
+ // runs are counted (run-metrics.md item 2).
2123
+ this.writeMetricsPoint(runId, point, turnedFinal && out.stored === true);
2044
2124
  if ((await this.ctx.storage.getAlarm()) === null)
2045
2125
  await this.ctx.storage.setAlarm(systemClock() + RUN_SWEEP_INTERVAL_MS);
2046
2126
  await this.refreshSessionBytes(record.session?.key);
@@ -2303,15 +2383,21 @@ export class RunHistoryDO extends DurableObject<Env> {
2303
2383
  * when the stored version changed (`event_count`, `finished_at`, `bytes`) —
2304
2384
  * an identical retry is a no-op on `run_events`. `stored: false` when the
2305
2385
  * record itself fell outside the (possibly just-updated) policy: it was
2306
- * written and deleted in the same transaction, so nothing of it remains. */
2386
+ * written and deleted in the same transaction, so nothing of it remains.
2387
+ * `point` is the record's metrics point, computed by the client
2388
+ * (run-metrics.md): written to the sink AFTER the commit, only when the row
2389
+ * turned final and the record was stored — `turnedFinal` stays internal (the
2390
+ * route strips it), so the wire answer is exactly the shape it always was. */
2307
2391
  async put(
2308
2392
  record: RunRecord,
2309
2393
  proposal?: RunPolicyProposal,
2310
- ): Promise<{ ok: true; retained: number; stored: boolean; rewritten: boolean }> {
2311
- let result = { ok: true as const, retained: 0, stored: false, rewritten: false };
2394
+ point?: RunMetricsPoint,
2395
+ ): Promise<{ ok: true; retained: number; stored: boolean; rewritten: boolean; turnedFinal: boolean }> {
2396
+ let result = { ok: true as const, retained: 0, stored: false, rewritten: false, turnedFinal: false };
2312
2397
  this.ctx.storage.transactionSync(() => {
2313
2398
  result = this.upsertInTransaction(record, proposal);
2314
2399
  });
2400
+ this.writeMetricsPoint(record.id, point, result.turnedFinal && result.stored);
2315
2401
  // A coordinator child closed OUTSIDE the ledger's finish — the run loop or
2316
2402
  // a reclaim writing an `interrupted` record, the pi harness's typed restart,
2317
2403
  // a resume abandoning a lost workspace — still wakes its parent's wait at
@@ -2335,7 +2421,7 @@ export class RunHistoryDO extends DurableObject<Env> {
2335
2421
  private upsertInTransaction(
2336
2422
  record: RunRecord,
2337
2423
  proposal?: RunPolicyProposal,
2338
- ): { ok: true; retained: number; stored: boolean; rewritten: boolean } {
2424
+ ): { ok: true; retained: number; stored: boolean; rewritten: boolean; turnedFinal: boolean } {
2339
2425
  {
2340
2426
  const now = systemClock();
2341
2427
  const policy = proposal ? this.applyProposal(proposal, now).policy : this.policyState().policy;
@@ -2352,11 +2438,29 @@ export class RunHistoryDO extends DurableObject<Env> {
2352
2438
  const { events, ...summary } = stored;
2353
2439
  const bytes = utf8ByteLength(JSON.stringify(stored));
2354
2440
  const existing = this.sql
2355
- .exec<{ event_count: number; finished_at: number; bytes: number }>(
2356
- `SELECT event_count, finished_at, bytes FROM runs WHERE run_id = ?`,
2441
+ .exec<{ event_count: number; finished_at: number; bytes: number; summary_json: string }>(
2442
+ `SELECT event_count, finished_at, bytes, summary_json FROM runs WHERE run_id = ?`,
2357
2443
  record.id,
2358
2444
  )
2359
2445
  .toArray()[0];
2446
+ // The one field of the stored summary the emission rule reads (run-metrics.md
2447
+ // item 2): the row's `provisional`, parsed alone — never deserialized whole.
2448
+ const existingProvisional = existing !== undefined && summaryIsProvisional(existing.summary_json);
2449
+ const turnedFinal = pointTurnsFinal(
2450
+ existing !== undefined ? { provisional: existingProvisional } : undefined,
2451
+ stored,
2452
+ );
2453
+ // A provisional record never overwrites a final row (run-history.md item 27;
2454
+ // run-metrics.md item 3): a start tombstone sitting in retry backoff or a
2455
+ // drain upgrade racing a fast finish would otherwise land after the finish
2456
+ // record, overwrite it with `interrupted` — and let the run's point be
2457
+ // written twice when a later final write turned the row "final" again.
2458
+ // Answered as stored, with nothing written: the row, its events and the
2459
+ // sessions table stay exactly as the final write left them.
2460
+ if (stored.provisional === true && existing !== undefined && !existingProvisional) {
2461
+ const retained = this.sql.exec<{ n: number }>(`SELECT COUNT(*) AS n FROM runs`).one().n;
2462
+ return { ok: true as const, retained, stored: true, rewritten: false, turnedFinal: false };
2463
+ }
2360
2464
  const unchanged =
2361
2465
  existing !== undefined &&
2362
2466
  sameStoredVersion(
@@ -2436,10 +2540,25 @@ export class RunHistoryDO extends DurableObject<Env> {
2436
2540
  retained: kept.size,
2437
2541
  stored: kept.has(record.id),
2438
2542
  rewritten: existing !== undefined && !unchanged,
2543
+ turnedFinal,
2439
2544
  };
2440
2545
  }
2441
2546
  }
2442
2547
 
2548
+ /** One point per run whose row turned final, AFTER the commit (run-metrics.md
2549
+ * item 2) — advisory: a throwing sink leaves the answer exactly as a
2550
+ * recording one would, and says so in one warn line with the run id and the
2551
+ * error's constructor name, never the point's contents. */
2552
+ private writeMetricsPoint(runId: string, point: RunMetricsPoint | undefined, turnedFinal: boolean): void {
2553
+ if (point === undefined || !turnedFinal) return;
2554
+ try {
2555
+ this.metrics.write(point);
2556
+ } catch (err) {
2557
+ const kind = err instanceof Error ? err.constructor.name : "Error";
2558
+ console.warn(`[runs/metrics] ${runId} point not written: ${kind}`);
2559
+ }
2560
+ }
2561
+
2443
2562
  /** Remove a run and its events. Returns whether a run row existed. */
2444
2563
  async delete(id: string): Promise<boolean> {
2445
2564
  let deleted = false;
@@ -2889,6 +3008,17 @@ function parseSummary(row: RunRow): Omit<RunRecord, "events"> | null {
2889
3008
  }
2890
3009
  }
2891
3010
 
3011
+ /** The one field of a stored row's summary the emission rule reads: whether the
3012
+ * row is a provisional tombstone. Deliberately not a full `RunRecord` parse
3013
+ * (run-metrics.md item 2) — this runs inside every upsert's transaction. */
3014
+ function summaryIsProvisional(summaryJson: string): boolean {
3015
+ try {
3016
+ return (JSON.parse(summaryJson) as { provisional?: unknown }).provisional === true;
3017
+ } catch {
3018
+ return false;
3019
+ }
3020
+ }
3021
+
2892
3022
  function parseRunId(v: unknown): Validated<string> {
2893
3023
  if (typeof v !== "string" || !RUN_ID_PATTERN.test(v)) return invalid("id must match ^[A-Za-z0-9_-]{1,64}$");
2894
3024
  return { ok: true, value: v };
@@ -2904,16 +3034,24 @@ function parsePositiveInt(v: unknown, name: string, max: number): Validated<numb
2904
3034
  return { ok: true, value: v };
2905
3035
  }
2906
3036
 
2907
- function parseRunPut(body: unknown): Validated<{ storeKey: string; record: RunRecord; proposal?: RunPolicyProposal }> {
3037
+ function parseRunPut(
3038
+ body: unknown,
3039
+ ): Validated<{ storeKey: string; record: RunRecord; proposal?: RunPolicyProposal; point?: RunMetricsPoint }> {
2908
3040
  if (typeof body !== "object" || body === null) return invalid("body must be a JSON object");
2909
3041
  const b = body as Record<string, unknown>;
2910
3042
  const key = parseStoreKey(b);
2911
3043
  if (!key.ok) return key;
2912
3044
  if (!isRunRecord(b.record)) return invalid("record must be a RunRecord");
2913
- const out: { storeKey: string; record: RunRecord; proposal?: RunPolicyProposal } = {
3045
+ const out: { storeKey: string; record: RunRecord; proposal?: RunPolicyProposal; point?: RunMetricsPoint } = {
2914
3046
  storeKey: key.value,
2915
3047
  record: b.record,
2916
3048
  };
3049
+ // The record's metrics point (run-metrics.md item 1), validated at the door
3050
+ // like everything else that reaches storage.
3051
+ if (b.point !== undefined) {
3052
+ if (!isRunMetricsPoint(b.point)) return invalid("point must be a RunMetricsPoint");
3053
+ out.point = b.point;
3054
+ }
2917
3055
  if (b.policy !== undefined) {
2918
3056
  if (typeof b.policy !== "object" || b.policy === null) return invalid("policy must be an object");
2919
3057
  const p = b.policy as Record<string, unknown>;
@@ -3121,8 +3259,10 @@ function parseLimit(v: unknown): Validated<number> {
3121
3259
  return { ok: true, value: v };
3122
3260
  }
3123
3261
 
3124
- /** `POST /list {scopeKey, limit, query?}`. */
3125
- function parseList(body: unknown): Validated<{ scopeKey: string; limit: number; query?: string }> {
3262
+ /** `POST /list {scopeKey, limit, query?, kind?}`. */
3263
+ function parseList(
3264
+ body: unknown,
3265
+ ): Validated<{ scopeKey: string; limit: number; query?: string; kind?: MemoryRecord["kind"] }> {
3126
3266
  if (typeof body !== "object" || body === null) return invalid("body must be a JSON object");
3127
3267
  const b = body as Record<string, unknown>;
3128
3268
  const scope = parseScopeKey(b.scopeKey);
@@ -3133,12 +3273,29 @@ function parseList(body: unknown): Validated<{ scopeKey: string; limit: number;
3133
3273
  if (typeof b.query !== "string") return invalid("query must be a string");
3134
3274
  if (b.query.length > MAX_QUERY_CHARS) return invalid(`query must be at most ${MAX_QUERY_CHARS} characters`);
3135
3275
  }
3276
+ if (b.kind !== undefined && b.kind !== "fact" && b.kind !== "summary")
3277
+ return invalid('kind must be "fact" or "summary"');
3136
3278
  return {
3137
3279
  ok: true,
3138
- value: { scopeKey: scope.value, limit: limit.value, ...(typeof b.query === "string" ? { query: b.query } : {}) },
3280
+ value: {
3281
+ scopeKey: scope.value,
3282
+ limit: limit.value,
3283
+ ...(typeof b.query === "string" ? { query: b.query } : {}),
3284
+ ...(b.kind === "fact" || b.kind === "summary" ? { kind: b.kind } : {}),
3285
+ },
3139
3286
  };
3140
3287
  }
3141
3288
 
3289
+ /** `POST /sweep {scopeKey, dryRun?}`. */
3290
+ function parseSweep(body: unknown): Validated<{ scopeKey: string; dryRun: boolean }> {
3291
+ if (!isJsonObject(body)) return invalid("body must be a JSON object");
3292
+ const b = body;
3293
+ const scope = parseScopeKey(b.scopeKey);
3294
+ if (!scope.ok) return scope;
3295
+ if (b.dryRun !== undefined && typeof b.dryRun !== "boolean") return invalid("dryRun must be a boolean");
3296
+ return { ok: true, value: { scopeKey: scope.value, dryRun: b.dryRun === true } };
3297
+ }
3298
+
3142
3299
  /** `POST /forget {scopeKey, id}`: the id is an opaque key, same caps as scopeKey. */
3143
3300
  function parseForget(body: unknown): Validated<{ scopeKey: string; id: string }> {
3144
3301
  if (typeof body !== "object" || body === null) return invalid("body must be a JSON object");
@@ -3205,6 +3362,11 @@ function parseCandidate(v: unknown, i: number): Validated<MemoryCandidate> {
3205
3362
  return invalid(`${at}.supersedes must be a string`);
3206
3363
  out.supersedes = c.supersedes;
3207
3364
  }
3365
+ if (c.restates !== undefined) {
3366
+ if (typeof c.restates !== "string" || c.restates.length > MAX_KEY_CHARS)
3367
+ return invalid(`${at}.restates must be a string`);
3368
+ out.restates = c.restates;
3369
+ }
3208
3370
  return { ok: true, value: out };
3209
3371
  }
3210
3372
 
@@ -4248,7 +4410,7 @@ async function handleLedger(pathname: string, body: unknown, env: Env): Promise<
4248
4410
  const parsed = parseRunPut({ ...b, storeKey: key.value });
4249
4411
  if (!parsed.ok) return json({ error: parsed.error }, 400);
4250
4412
  if (parsed.value.record.id !== runId.value) return json({ error: "record.id must equal runId" }, 400);
4251
- const r = await stub.finish(runId.value, g.value, parsed.value.record, parsed.value.proposal);
4413
+ const r = await stub.finish(runId.value, g.value, parsed.value.record, parsed.value.proposal, parsed.value.point);
4252
4414
  console.log(
4253
4415
  `[runs/finish] ${key.value} ${runId.value} ok=${r.ok}${r.ok ? ` stored=${r.stored} event=${r.event}` : ` ${r.reason}`}`,
4254
4416
  );
@@ -4265,8 +4427,10 @@ async function handleRuns(pathname: string, body: unknown, env: Env): Promise<Re
4265
4427
  if (pathname === "/runs/put") {
4266
4428
  const parsed = parseRunPut(body);
4267
4429
  if (!parsed.ok) return json({ error: parsed.error }, 400);
4268
- const { storeKey, record, proposal } = parsed.value;
4269
- const result = await stub(storeKey).put(record, proposal);
4430
+ const { storeKey, record, proposal, point } = parsed.value;
4431
+ // `turnedFinal` stays internal: the wire answer is exactly the shape it
4432
+ // always was, binding or no binding (run-metrics.md item 4).
4433
+ const { turnedFinal: _turnedFinal, ...result } = await stub(storeKey).put(record, proposal, point);
4270
4434
  console.log(
4271
4435
  `[runs/put] ${storeKey} <- ${record.id} (${record.storedEventCount} events, stored=${result.stored}, ${result.retained} retained)`,
4272
4436
  );
@@ -4329,6 +4493,7 @@ const ROUTES = new Set([
4329
4493
  "/write",
4330
4494
  "/list",
4331
4495
  "/forget",
4496
+ "/sweep",
4332
4497
  "/schedules/record",
4333
4498
  "/schedules/latest",
4334
4499
  "/runs/put",
@@ -4349,11 +4514,21 @@ interface Admission {
4349
4514
  authorized: boolean;
4350
4515
  }
4351
4516
 
4517
+ /** What this deploy carries, for the bot's boot probe: the fixed route set,
4518
+ * plus `runMetrics:<dataset>` when the deploy bound the Analytics Engine
4519
+ * dataset (run-metrics.md item 5) — the name from the `RUN_METRICS_DATASET`
4520
+ * var rendered beside the binding, so the probe can compare it to the bot's. */
4521
+ export function featuresOf(env: Pick<Env, "RUN_METRICS" | "RUN_METRICS_DATASET">): string[] {
4522
+ const features = ["memory", "schedules", "runs", "config", "delivery", "costs"];
4523
+ if (env.RUN_METRICS !== undefined) features.push(`runMetrics:${env.RUN_METRICS_DATASET ?? "unknown"}`);
4524
+ return features;
4525
+ }
4526
+
4352
4527
  /** Every request, once `fetch` has decided whether it gets a root. */
4353
4528
  async function handleRequest(request: Request, env: Env, admission: Admission): Promise<Response> {
4354
4529
  const url = new URL(request.url);
4355
4530
  if (url.pathname === "/healthz" && request.method === "GET")
4356
- return json({ ok: true, build: BUILD, features: ["memory", "schedules", "runs", "config", "delivery", "costs"] });
4531
+ return json({ ok: true, build: BUILD, features: featuresOf(env) });
4357
4532
  if (!admission.known) return json({ error: "not found" }, 404);
4358
4533
  if (request.method !== "POST") return json({ error: "method not allowed" }, 405);
4359
4534
  if (!admission.authorized) return json({ error: "unauthorized" }, 401);
@@ -4422,8 +4597,8 @@ async function handleRequest(request: Request, env: Env, admission: Admission):
4422
4597
  if (url.pathname === "/list") {
4423
4598
  const parsed = parseList(body);
4424
4599
  if (!parsed.ok) return json({ error: parsed.error }, 400);
4425
- const { scopeKey, limit, query } = parsed.value;
4426
- const records = await env.MEMORY.get(env.MEMORY.idFromName(scopeKey)).list(scopeKey, limit, query);
4600
+ const { scopeKey, limit, query, kind } = parsed.value;
4601
+ const records = await env.MEMORY.get(env.MEMORY.idFromName(scopeKey)).list(scopeKey, limit, query, kind);
4427
4602
  console.log(`[list] ${scopeKey} -> ${records.length} records`);
4428
4603
  return json({ records });
4429
4604
  }
@@ -4436,6 +4611,16 @@ async function handleRequest(request: Request, env: Env, admission: Admission):
4436
4611
  console.log(`[forget] ${scopeKey} ${id} -> ${forgotten}`);
4437
4612
  return json({ ok: true, forgotten });
4438
4613
  }
4614
+ if (url.pathname === "/sweep") {
4615
+ const parsed = parseSweep(body);
4616
+ if (!parsed.ok) return json({ error: parsed.error }, 400);
4617
+ const { scopeKey, dryRun } = parsed.value;
4618
+ const out = await env.MEMORY.get(env.MEMORY.idFromName(scopeKey)).sweep(scopeKey, dryRun);
4619
+ // Observability: scope + count only (ids carry no record text; they ride
4620
+ // the dryRun answer, not the log).
4621
+ console.log(`[sweep] ${scopeKey} -> ${out.swept}${dryRun ? " (dry run)" : ""}`);
4622
+ return json({ ok: true, swept: out.swept, ...(dryRun ? { ids: out.ids } : {}) });
4623
+ }
4439
4624
 
4440
4625
  const parsed = parseWrite(body);
4441
4626
  if (!parsed.ok) return json({ error: parsed.error }, 400);
@@ -66,6 +66,18 @@
66
66
  "script_name": "{{bot.script}}"
67
67
  }
68
68
  ],
69
+ // {{#if metrics}}
70
+ // The run-metrics dataset (docs/reference/specs/run-metrics.md): where RunHistoryDO
71
+ // writes a turned-final run's point, bound only when the deployment profile names a
72
+ // dataset (`metrics.dataset`) — nothing is created ahead of the deploy; the platform
73
+ // creates the dataset on first write. Without it the Worker deploys without the
74
+ // binding and writes no points.
75
+ "analytics_engine_datasets": [{ "binding": "RUN_METRICS", "dataset": "{{metrics.dataset}}" }],
76
+ // The dataset's name beside its binding (rendered together): /healthz `features`
77
+ // says `runMetrics:<dataset>` from it, so the bot's boot probe can compare the
78
+ // name its own config carries with the one this deploy bound.
79
+ "vars": { "RUN_METRICS_DATASET": "{{metrics.dataset}}" },
80
+ // {{/if}}
69
81
  // SQLite-backed DOs (durable across restarts — AGENTS.md invariant 6).
70
82
  "migrations": [
71
83
  { "tag": "v1", "new_sqlite_classes": ["MemoryDO"] },