@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.
- package/dist/assets/config/config.example.yaml +28 -2
- package/dist/assets/deploy/cloudflare/preflight.mjs +19 -21
- package/dist/assets/deploy/cloudflare/worker.ts +6 -3
- package/dist/assets/deploy/cloudflare-memory/runMetricsSink.ts +31 -0
- package/dist/assets/deploy/cloudflare-memory/worker.ts +223 -38
- package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +12 -0
- package/dist/assets/deploy/cloudflare-resident/memoryGuard.ts +212 -0
- package/dist/assets/deploy/cloudflare-resident/refresh.ts +1 -1
- package/dist/assets/deploy/cloudflare-resident/worker.ts +317 -56
- package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +4 -2
- package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.d.mts +31 -0
- package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.mjs +119 -0
- package/dist/assets/deploy/profile.example.json +1 -1
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +3 -2
- package/dist/assets/project.json +13 -9
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +5 -5
- package/dist/assets/src/core/authz/policy.ts +8 -0
- package/dist/assets/src/core/budgets.ts +39 -0
- package/dist/assets/src/core/coordinator/contract.ts +93 -1
- package/dist/assets/src/core/coordinator/driver.ts +236 -21
- package/dist/assets/src/core/drain.ts +50 -0
- package/dist/assets/src/core/memory/engine.ts +123 -4
- package/dist/assets/src/core/memory/scorer.ts +12 -4
- package/dist/assets/src/core/memory/types.ts +73 -12
- package/dist/assets/src/core/modelCard.ts +32 -4
- package/dist/assets/src/core/modelPricing.ts +111 -1
- package/dist/assets/src/core/modelProxy/usage.ts +88 -0
- package/dist/assets/src/core/modelRegistry.ts +15 -1
- package/dist/assets/src/core/refusal.ts +4 -7
- package/dist/assets/src/core/reviewVerdict.ts +4 -0
- package/dist/assets/src/core/runEvents.ts +82 -3
- package/dist/assets/src/core/runFriction.ts +8 -2
- package/dist/assets/src/core/runLedger/types.ts +11 -0
- package/dist/assets/src/core/runMetrics.ts +237 -0
- package/dist/assets/src/core/runRecord.ts +46 -0
- package/dist/assets/src/core/runUsage.ts +67 -13
- package/dist/assets/src/core/schedules.ts +3 -0
- package/dist/assets/src/core/ship/contract.ts +34 -14
- package/dist/assets/src/core/ship/coordinator.ts +485 -55
- package/dist/assets/src/core/ship/renewal.ts +10 -5
- package/dist/assets/src/core/trace/attrs.ts +24 -0
- package/dist/assets/src/core/types.ts +5 -5
- package/dist/assets/src/core/verbosity.ts +48 -0
- package/dist/assets/src/deploy/liveGate.ts +40 -13
- package/dist/assets/src/deploy/profile.ts +16 -0
- package/dist/assets/src/deploy/restart.ts +11 -12
- package/dist/assets/src/execution/residentDepCache.ts +50 -1
- package/dist/assets/src/execution/residentDepsStore.ts +40 -2
- package/dist/assets/src/execution/residentRefresh.ts +55 -3
- package/dist/assets/src/execution/residentSteps.ts +4 -0
- package/dist/assets/src/execution/sandboxErrors.ts +8 -0
- package/dist/assets/web/dist/.vite/manifest.json +444 -430
- package/dist/assets/web/dist/assets/{AppShell-DAtHiuI6.js → AppShell-Bmz8nzz3.js} +1 -1
- package/dist/assets/web/dist/assets/{CostsPage-BoKzYa4B.js → CostsPage-CxTKdpzU.js} +1 -1
- package/dist/assets/web/dist/assets/{DeliveryPage-DUXd-Sl-.js → DeliveryPage-C7jaEJip.js} +1 -1
- package/dist/assets/web/dist/assets/HomePage-cWbupQzy.js +3 -0
- package/dist/assets/web/dist/assets/{InputMenu-C6aPpe30.js → InputMenu-C9E7YoII.js} +1 -1
- package/dist/assets/web/dist/assets/{NotFoundPage-DtE-GgTk.js → NotFoundPage-4nlrq7DP.js} +1 -1
- package/dist/assets/web/dist/assets/{PendingTurnRow-DDhMhrI7.js → PendingTurnRow-C-pUgyyX.js} +1 -1
- package/dist/assets/web/dist/assets/PlanePage-BNy4tSgJ.js +1 -0
- package/dist/assets/web/dist/assets/{ResidentDetailPage-BnEoOnGQ.js → ResidentDetailPage-Dbtiq6_J.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-Dxpgf-l-.js → ResidentsIndexPage-CKtz9H1o.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-sYFtuK2G.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-Clh68nw9.js +9 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-DPDbcOA_.js +1 -0
- package/dist/assets/web/dist/assets/{RunsTabs-YSbUu5py.js → RunsTabs-DotpuCSP.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-B_GgeJrb.js → ScheduledPage-C8FYoZy5.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingSelect-BIzAsLk1.js → SettingSelect-CSiBQCSX.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-BXX4R113.js → SettingsPage-CkYIhLUl.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-BOaw8le9.js → StatusDot-C0TUgnC1.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-DYZZ4l4V.js → Tooltip-CN5cZdl3.js} +1 -1
- package/dist/assets/web/dist/assets/{UnitRoutePage-BaSW5Odq.js → UnitRoutePage-D_EtAl8r.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-html-BjeQZdCq.js → angular-html-qW40ZL9x.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-ZGdORrL2.js → angular-ts-PUtZU8Cq.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-DhvV_X93.js → apl-D3fC7Qjd.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-DiRSE4Ug.js → astro-Bi3JFHhh.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-D8Fr5Wvq.js → blade-CSpsf9Z9.js} +1 -1
- package/dist/assets/web/dist/assets/budgets-CbIyPAER.js +1 -0
- package/dist/assets/web/dist/assets/{c-BwbC64D4.js → c-DE_b1oWP.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-DgmqRK97.js → chapel-CWXd9giL.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-DJod2RNK.js → cobol-CnMOmWNF.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-D1Y2CgBY.js → coffee-B9WvURu8.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-CsO97YOM.js → cpp-BZAJXlWI.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-CJ7m2tOc.js → crystal-Bm48wAlb.js} +1 -1
- package/dist/assets/web/dist/assets/{css-B2M-NKoj.js → css-WtMeC3ab.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-BCVXeBJ9.js → dist-B1Mf2JbM.js} +2 -2
- package/dist/assets/web/dist/assets/{edge-CFVW-m9B.js → edge-BmwNa6Ss.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-B2mtRgCr.js → elixir-BotfP-Mn.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-2teVAwCi.js → elm-DAFc6laN.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-LAbeNqen.js → erb-QkOON3bB.js} +1 -1
- package/dist/assets/web/dist/assets/{git-rebase-DnhzH_sp.js → git-rebase-BPs-dd73.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-DPKjXf1-.js → glimmer-js-D8eooepk.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-CABXnt5z.js → glimmer-ts-B0bRiDVg.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-Dz4-3-gJ.js → glsl-Bv6bMe1n.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-ByWUD1DL.js → graphql-Bh9y-n9o.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-CdmOK2K-.js → hack-DSEbn7Ir.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-DzegMpd2.js → haml-0NW1-z1f.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-B5NeiP0e.js → handlebars-BfgCHFbK.js} +1 -1
- package/dist/assets/web/dist/assets/{html-Zg_NHLv9.js → html-CQHc0gQ5.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-DZf9JNzh.js → html-derivative-B0Tm7wLP.js} +1 -1
- package/dist/assets/web/dist/assets/{http-CVFrQZeN.js → http-GAuU3aOF.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-L5brWAJe.js → hurl-CrDFbF5B.js} +1 -1
- package/dist/assets/web/dist/assets/indexRow-BxKswdlK.js +1 -0
- package/dist/assets/web/dist/assets/{java-hFphyZ4R.js → java-DYv5KhUe.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-CAJO1WKX.js → javascript-32SiLGg0.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-N4A4srXI.js → jinja--gJDUGYa.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-D3BZa_iU.js → jison-BlKLGzZH.js} +1 -1
- package/dist/assets/web/dist/assets/{json-DxD1Qh8W.js → json-BICanD1N.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-CHhag69S.js → jsx-CgW3-uFT.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-CrhkZ6Tt.js → julia-BErRpwwL.js} +1 -1
- package/dist/assets/web/dist/assets/{just-D0LFOuhq.js → just-CordHv0h.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-BHAlwCXK.js → latex-D7LVPLbi.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-CkaLfCWh.js → liquid-3ehz2i4U.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-B1h4aAzP.js → lua-C9ppSVrm.js} +1 -1
- package/dist/assets/web/dist/assets/main-Cv0q9y56.js +28 -0
- package/dist/assets/web/dist/assets/main-DKFCM-sy.css +1 -0
- package/dist/assets/web/dist/assets/{marko-D6CG4hvX.js → marko-xBZLUBEJ.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-CVQzimAU.js → mdc-BJxGoVgD.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-h-9Ir54h.js → nginx-B-sZmpsb.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-DHVI30sp.js → nim--s9wRABK.js} +1 -1
- package/dist/assets/web/dist/assets/{org-AasAXfMF.js → org-DoUvssoI.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-BVi5aTEy.js → perl-B-vGHKOy.js} +1 -1
- package/dist/assets/web/dist/assets/{php-qOkCeUKr.js → php-CerWx_Xb.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-8TnUGvEX.js → pug-CzKrBMb1.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-CpJbhoxn.js → qml-Bs8L8I6k.js} +1 -1
- package/dist/assets/web/dist/assets/{r-JkUNcAJW.js → r-w6uG6mob.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-CtU4ws5a.js → razor-dz2oj2bd.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-J2zA6ceN.js → regexp-CfQdprnY.js} +1 -1
- package/dist/assets/web/dist/assets/{rst-CMCvf-yZ.js → rst-DAOaX2tb.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-ChLBY_iR.js → ruby-QcCe4udB.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-cwn4x2VY.js → sas-BrM4lAAc.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-jSrJlzLI.js → scss-DVbJXoUb.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-D5nAd0gs.js → shellscript-09fP2hZf.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-BZY4-Tdh.js → shellsession-Qgr8xjZF.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-COppPOKr.js → soy-DIYeNTuz.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-Blc_6dOA.js → sql-CzLR9msV.js} +1 -1
- package/dist/assets/web/dist/assets/sseReplay-BYo2EZKy.js +11 -0
- package/dist/assets/web/dist/assets/{stata-DRv3OY-h.js → stata-zCY1ZzH0.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-qMv_eeJ6.js → surrealql-CJIjTRV4.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-d1Edb6Rz.js → svelte-C5OTPFZg.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-CUMSIOXG.js → templ-DYj-UICF.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-BGZQA-Ei.js → tex-xcie-3Gd.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-Z9ulxVsq.js → ts-tags-CoN86VTt.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-oI4bI3Vf.js → tsx-n3-No_Br.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-D_RXwBIX.js → twig-DPiJ7Vp1.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-BgqQDYoN.js → typescript-BP6HBaOp.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-5MW-KRDd.js → typst-Do9ZjsVP.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-CZhHHsPm.js → vue-CzAeXNRY.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-DlYy2d8v.js → vue-html-D7iYmYjd.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-CJDnx-qn.js → vue-vine-DtrMRsJx.js} +1 -1
- package/dist/assets/web/dist/assets/{xml-BRBDk65Q.js → xml-DGJHo1c0.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-BlCykPxo.js → xsl-Q00kvNgW.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-BqW48szf.js → yaml-F-UGKaiO.js} +1 -1
- package/dist/cli.js +5623 -2455
- package/package.json +1 -1
- package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.sh +0 -68
- package/dist/assets/web/dist/assets/HomePage-mSiqEEcN.js +0 -2
- package/dist/assets/web/dist/assets/RunFoldRow-CSo4-vld.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-9klVWhSF.js +0 -9
- package/dist/assets/web/dist/assets/RunsIndexPage-BplMIgaw.js +0 -1
- package/dist/assets/web/dist/assets/budgets-BvWYKPsY.js +0 -1
- package/dist/assets/web/dist/assets/indexRow-Bde9OZxG.js +0 -1
- package/dist/assets/web/dist/assets/main-Dkcbtu3u.js +0 -28
- package/dist/assets/web/dist/assets/main-zbP_dTjR.css +0 -1
- 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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
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
|
|
467
|
-
* bump. With `query`, only rows
|
|
468
|
-
*
|
|
469
|
-
* query with no tokens lists
|
|
470
|
-
|
|
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>(
|
|
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
|
-
|
|
2311
|
-
|
|
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(
|
|
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(
|
|
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: {
|
|
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
|
-
|
|
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:
|
|
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"] },
|