@coreplane/switchboard 1.226.1 → 1.228.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 (37) hide show
  1. package/dist/assets/deploy/cloudflare-memory/worker.ts +157 -3
  2. package/dist/assets/deploy/cloudflare-resident/worker.ts +354 -9
  3. package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +25 -14
  4. package/dist/assets/deploy/cloudflare-sandbox/package.json +1 -1
  5. package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.sh +68 -0
  6. package/dist/assets/deploy/cloudflare-sandbox/worker.ts +343 -261
  7. package/dist/assets/deploy/cloudflare-sandbox/wrangler.template.jsonc +2 -2
  8. package/dist/assets/package-lock.json +7 -48
  9. package/dist/assets/package.json +1 -1
  10. package/dist/assets/project.json +2 -2
  11. package/dist/assets/source.json +3 -3
  12. package/dist/assets/src/agents/registry.ts +15 -0
  13. package/dist/assets/src/core/authz/policy.ts +8 -0
  14. package/dist/assets/src/core/coordinator/contract.ts +6 -0
  15. package/dist/assets/src/core/coordinator/driver.ts +8 -6
  16. package/dist/assets/src/core/runEvents.ts +50 -3
  17. package/dist/assets/src/core/runFriction.ts +2 -1
  18. package/dist/assets/src/core/runRecord.ts +107 -3
  19. package/dist/assets/src/core/runUsage.ts +199 -0
  20. package/dist/assets/src/core/ship/coordinator.ts +4 -2
  21. package/dist/assets/src/execution/residentRebind.ts +310 -0
  22. package/dist/assets/src/execution/residentSteps.ts +1 -0
  23. package/dist/assets/src/execution/sandboxErrors.ts +85 -10
  24. package/dist/assets/src/execution/sandboxLifecycle.ts +78 -0
  25. package/dist/assets/web/dist/.vite/manifest.json +18 -18
  26. package/dist/assets/web/dist/assets/{ResidentDetailPage-BBOpejGX.js → ResidentDetailPage-B1Q9pabX.js} +1 -1
  27. package/dist/assets/web/dist/assets/{ResidentsIndexPage-B8kFWHpB.js → ResidentsIndexPage-CP7U_4aK.js} +1 -1
  28. package/dist/assets/web/dist/assets/{RunRoutePage-COWeVtIE.js → RunRoutePage-dCC25f_b.js} +4 -4
  29. package/dist/assets/web/dist/assets/{RunsIndexPage-BjH93cKx.js → RunsIndexPage-Cgp4t4C8.js} +1 -1
  30. package/dist/assets/web/dist/assets/{ScheduledPage-grlNKvCA.js → ScheduledPage-DthDA2xG.js} +1 -1
  31. package/dist/assets/web/dist/assets/{StatusDot-BQNoaXO6.js → StatusDot-DDc88Kbs.js} +1 -1
  32. package/dist/assets/web/dist/assets/{Tooltip-DVCeIzaa.js → Tooltip-CBapNhsh.js} +1 -1
  33. package/dist/assets/web/dist/assets/{dist-B5Wfk-oY.js → dist-BnwSD1cL.js} +1 -1
  34. package/dist/assets/web/dist/assets/{main-CN0U7d6s.js → main-ZhQGbZ2E.js} +2 -2
  35. package/dist/cli.js +1277 -344
  36. package/package.json +1 -1
  37. package/dist/assets/src/execution/sandboxKeepalive.ts +0 -118
@@ -56,6 +56,14 @@ import {
56
56
  textOfStoredRow,
57
57
  } from "../../src/core/runLedger/sessionLog.ts";
58
58
  import type { RunEvent } from "../../src/core/runEvents.ts";
59
+ import {
60
+ aggregateUsageByUser,
61
+ isRunUsage,
62
+ usageOfEvents,
63
+ type RunUsage,
64
+ type RunUsageReport,
65
+ type UsageRun,
66
+ } from "../../src/core/runUsage.ts";
59
67
  import {
60
68
  checkFence,
61
69
  decideClaim,
@@ -1160,6 +1168,10 @@ export class RunHistoryDO extends DurableObject<Env> {
1160
1168
  // The session a run was a range of (session-log item 7), so the sweep can
1161
1169
  // tell which sessions still have a kept run; null for a record without one.
1162
1170
  if (!columns.has("session_key")) this.sql.exec(`ALTER TABLE runs ADD COLUMN session_key TEXT`);
1171
+ // What the run cost in tokens (costs.md, cost by user): the record's `usage`
1172
+ // as JSON; NULL for a record written before the field existed, until the
1173
+ // by-user aggregate fills it in from the run's stored events.
1174
+ if (!columns.has("usage_json")) this.sql.exec(`ALTER TABLE runs ADD COLUMN usage_json TEXT`);
1163
1175
  this.sql.exec(`
1164
1176
  CREATE INDEX IF NOT EXISTS runs_channel_finished ON runs(channel_id, finished_at DESC, run_id DESC);
1165
1177
  CREATE INDEX IF NOT EXISTS runs_visibility_finished ON runs(channel_visibility, finished_at DESC, run_id DESC);
@@ -1820,8 +1832,8 @@ export class RunHistoryDO extends DurableObject<Env> {
1820
1832
  );
1821
1833
  this.sql.exec(
1822
1834
  `INSERT INTO runs (run_id, label, agent, model, channel_id, user_id, thread_key, channel_visibility, repo, started_at, finished_at, stored_at, status,
1823
- event_count, stored_event_count, truncated, bytes, diagnosis_json, summary_json, session_key)
1824
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1835
+ event_count, stored_event_count, truncated, bytes, diagnosis_json, summary_json, session_key, usage_json)
1836
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1825
1837
  ON CONFLICT(run_id) DO UPDATE SET
1826
1838
  label = excluded.label, agent = excluded.agent, model = excluded.model, channel_id = excluded.channel_id,
1827
1839
  user_id = excluded.user_id, thread_key = excluded.thread_key, channel_visibility = excluded.channel_visibility,
@@ -1829,7 +1841,8 @@ export class RunHistoryDO extends DurableObject<Env> {
1829
1841
  finished_at = excluded.finished_at, stored_at = excluded.stored_at, status = excluded.status,
1830
1842
  event_count = excluded.event_count, stored_event_count = excluded.stored_event_count, truncated = excluded.truncated,
1831
1843
  bytes = excluded.bytes, diagnosis_json = excluded.diagnosis_json, summary_json = excluded.summary_json,
1832
- session_key = excluded.session_key`,
1844
+ session_key = excluded.session_key,
1845
+ usage_json = COALESCE(excluded.usage_json, runs.usage_json)`,
1833
1846
  stored.id,
1834
1847
  stored.label ?? null,
1835
1848
  stored.agent ?? null,
@@ -1850,6 +1863,7 @@ export class RunHistoryDO extends DurableObject<Env> {
1850
1863
  JSON.stringify(stored.diagnosis),
1851
1864
  JSON.stringify(summary),
1852
1865
  stored.session?.key ?? null,
1866
+ stored.usage ? JSON.stringify(stored.usage) : null,
1853
1867
  );
1854
1868
  // The session's registry row learns its newest finish (session-log item
1855
1869
  // 7); a record that reaches the store without a claim (the plain put
@@ -2053,6 +2067,83 @@ export class RunHistoryDO extends DurableObject<Env> {
2053
2067
  return summary ? { ...summary, bytes: row.bytes } : null;
2054
2068
  }
2055
2069
 
2070
+ /**
2071
+ * Who spent what, per UTC day (costs.md, cost by user), over the runs that
2072
+ * finished in [sinceMs, untilMs) and are inside the retention window. A row
2073
+ * written before `usage_json` existed is filled in here from its stored
2074
+ * `model.turn` events — up to USAGE_BACKFILL_PER_CALL a call, the rest
2075
+ * reported as `pending` — so the history heals as it is read, with no
2076
+ * operator step; a run without turns is written back as the zero usage so it
2077
+ * is not re-read. A child is billed to its parent's requester; a parent
2078
+ * outside the range is looked up by id.
2079
+ */
2080
+ usageByUser(sinceMs: number, untilMs: number): RunUsageReport {
2081
+ const now = systemClock();
2082
+ const { policy } = this.policyState();
2083
+ const cutoff = now - policy.retentionDays * 86_400_000;
2084
+ const rows = this.sql
2085
+ .exec<UsageRow>(
2086
+ `SELECT run_id, user_id, started_at, finished_at, usage_json, summary_json FROM runs
2087
+ WHERE finished_at >= ? AND finished_at < ? ORDER BY finished_at ASC, run_id ASC`,
2088
+ Math.max(sinceMs, cutoff),
2089
+ untilMs,
2090
+ )
2091
+ .toArray();
2092
+ let backfilled = 0;
2093
+ const runs: UsageRun[] = rows.map((row) => {
2094
+ let usage = parseUsageJson(row.usage_json);
2095
+ if (usage === undefined && backfilled < USAGE_BACKFILL_PER_CALL) {
2096
+ usage = usageOfEvents(this.modelTurnEvents(row.run_id));
2097
+ this.sql.exec(`UPDATE runs SET usage_json = ? WHERE run_id = ?`, JSON.stringify(usage), row.run_id);
2098
+ backfilled += 1;
2099
+ }
2100
+ const who = identityOfSummary(row.summary_json);
2101
+ return {
2102
+ id: row.run_id,
2103
+ userId: row.user_id,
2104
+ ...(who.userName ? { userName: who.userName } : {}),
2105
+ ...(who.parentRunId ? { parentRunId: who.parentRunId } : {}),
2106
+ startedAt: row.started_at,
2107
+ finishedAt: row.finished_at,
2108
+ ...(usage ? { usage } : {}),
2109
+ };
2110
+ });
2111
+ const lookupParent = (id: string) => {
2112
+ const p = this.sql
2113
+ .exec<{ user_id: string; summary_json: string }>(`SELECT user_id, summary_json FROM runs WHERE run_id = ?`, id)
2114
+ .toArray()[0];
2115
+ if (!p) return undefined;
2116
+ const who = identityOfSummary(p.summary_json);
2117
+ return { userId: p.user_id, ...(who.userName ? { userName: who.userName } : {}) };
2118
+ };
2119
+ const { rows: out, pending } = aggregateUsageByUser(runs, lookupParent);
2120
+ const earliest = this.sql
2121
+ .exec<{ m: number | null }>(`SELECT MIN(finished_at) AS m FROM runs WHERE finished_at >= ?`, cutoff)
2122
+ .one().m;
2123
+ return {
2124
+ rows: out,
2125
+ pending,
2126
+ ...(earliest !== null ? { earliestFinishedAt: earliest } : {}),
2127
+ retentionDays: policy.retentionDays,
2128
+ };
2129
+ }
2130
+
2131
+ /** The stored `model.turn` span ends of one run — the rows a backfill prices. */
2132
+ private modelTurnEvents(runId: string): RunEvent[] {
2133
+ const out: RunEvent[] = [];
2134
+ for (const r of this.sql
2135
+ .exec<{ json: string }>(`SELECT json FROM run_events WHERE run_id = ? AND json LIKE '%"model.turn"%'`, runId)
2136
+ .toArray()) {
2137
+ try {
2138
+ const e = JSON.parse(r.json) as RunEvent;
2139
+ if (e && e.type === "span_end") out.push(e);
2140
+ } catch {
2141
+ // an unparsable event row prices nothing
2142
+ }
2143
+ }
2144
+ return out;
2145
+ }
2146
+
2056
2147
  /**
2057
2148
  * Newest first (finished_at desc, run_id desc) among the rows the policy
2058
2149
  * keeps, filtered, capped at RUN_LIST_MAX_LIMIT. The `before`/`beforeId`
@@ -2181,6 +2272,43 @@ function parseEventRows(rows: readonly EventRow[]): StoredRunEvent[] {
2181
2272
  }
2182
2273
 
2183
2274
  /** The stored summary (record minus events); null when the row is unreadable. */
2275
+ /** How many usage-less rows one by-user read prices from their events before
2276
+ * reporting the rest as pending: a page load stays quick while the history heals. */
2277
+ const USAGE_BACKFILL_PER_CALL = 200;
2278
+
2279
+ type UsageRow = {
2280
+ run_id: string;
2281
+ user_id: string;
2282
+ started_at: number;
2283
+ finished_at: number;
2284
+ usage_json: string | null;
2285
+ summary_json: string;
2286
+ };
2287
+
2288
+ /** A stored `usage_json`, or undefined when absent or unreadable (then it is recomputed). */
2289
+ function parseUsageJson(raw: string | null): RunUsage | undefined {
2290
+ if (raw === null) return undefined;
2291
+ try {
2292
+ const parsed: unknown = JSON.parse(raw);
2293
+ return isRunUsage(parsed) ? parsed : undefined;
2294
+ } catch {
2295
+ return undefined;
2296
+ }
2297
+ }
2298
+
2299
+ /** The two identity fields the by-user aggregate needs off a summary, read leniently. */
2300
+ function identityOfSummary(raw: string): { userName?: string; parentRunId?: string } {
2301
+ try {
2302
+ const s = JSON.parse(raw) as Record<string, unknown>;
2303
+ return {
2304
+ ...(typeof s.userName === "string" && s.userName ? { userName: s.userName } : {}),
2305
+ ...(typeof s.parentRunId === "string" && s.parentRunId ? { parentRunId: s.parentRunId } : {}),
2306
+ };
2307
+ } catch {
2308
+ return {};
2309
+ }
2310
+ }
2311
+
2184
2312
  function parseSummary(row: RunRow): Omit<RunRecord, "events"> | null {
2185
2313
  try {
2186
2314
  const parsed: unknown = JSON.parse(row.summary_json);
@@ -2237,6 +2365,23 @@ function parseRunPut(body: unknown): Validated<{ storeKey: string; record: RunRe
2237
2365
  }
2238
2366
 
2239
2367
  /** `{storeKey, id}` — the body of /runs/get, /runs/summary and /runs/delete, and the base of /runs/events. */
2368
+ /** At most a year of runs a call — the page asks for 90 days at most. */
2369
+ const USAGE_QUERY_MAX_SPAN_MS = 366 * 86_400_000;
2370
+
2371
+ function parseRunUsageQuery(body: unknown): Validated<{ storeKey: string; sinceMs: number; untilMs: number }> {
2372
+ if (typeof body !== "object" || body === null) return invalid("body must be a JSON object");
2373
+ const b = body as Record<string, unknown>;
2374
+ const key = parseStoreKey(b);
2375
+ if (!key.ok) return key;
2376
+ const { sinceMs, untilMs } = b;
2377
+ if (typeof sinceMs !== "number" || !Number.isFinite(sinceMs) || sinceMs < 0)
2378
+ return invalid("sinceMs must be a non-negative number");
2379
+ if (typeof untilMs !== "number" || !Number.isFinite(untilMs) || untilMs <= sinceMs)
2380
+ return invalid("untilMs must be a number after sinceMs");
2381
+ if (untilMs - sinceMs > USAGE_QUERY_MAX_SPAN_MS) return invalid("the range may span at most 366 days");
2382
+ return { ok: true, value: { storeKey: key.value, sinceMs, untilMs } };
2383
+ }
2384
+
2240
2385
  function parseRunTarget(body: unknown): Validated<{ storeKey: string; id: string }> {
2241
2386
  if (typeof body !== "object" || body === null) return invalid("body must be a JSON object");
2242
2387
  const b = body as Record<string, unknown>;
@@ -3504,6 +3649,14 @@ async function handleRuns(pathname: string, body: unknown, env: Env): Promise<Re
3504
3649
  );
3505
3650
  return json(result ?? { events: null });
3506
3651
  }
3652
+ if (pathname === "/runs/usage-by-user") {
3653
+ const parsed = parseRunUsageQuery(body);
3654
+ if (!parsed.ok) return json({ error: parsed.error }, 400);
3655
+ const { storeKey, sinceMs, untilMs } = parsed.value;
3656
+ const report = await stub(storeKey).usageByUser(sinceMs, untilMs);
3657
+ console.log(`[runs/usage-by-user] ${storeKey} -> ${report.rows.length} rows, ${report.pending} pending`);
3658
+ return json(report);
3659
+ }
3507
3660
  // /runs/delete
3508
3661
  const parsed = parseRunTarget(body);
3509
3662
  if (!parsed.ok) return json({ error: parsed.error }, 400);
@@ -3526,6 +3679,7 @@ const ROUTES = new Set([
3526
3679
  "/runs/summary",
3527
3680
  "/runs/list",
3528
3681
  "/runs/events",
3682
+ "/runs/usage-by-user",
3529
3683
  "/runs/delete",
3530
3684
  ...LEDGER_ROUTES,
3531
3685
  ]);