@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.
- package/dist/assets/deploy/cloudflare-memory/worker.ts +157 -3
- package/dist/assets/deploy/cloudflare-resident/worker.ts +354 -9
- package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +25 -14
- package/dist/assets/deploy/cloudflare-sandbox/package.json +1 -1
- package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.sh +68 -0
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +343 -261
- package/dist/assets/deploy/cloudflare-sandbox/wrangler.template.jsonc +2 -2
- package/dist/assets/package-lock.json +7 -48
- package/dist/assets/package.json +1 -1
- package/dist/assets/project.json +2 -2
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +15 -0
- package/dist/assets/src/core/authz/policy.ts +8 -0
- package/dist/assets/src/core/coordinator/contract.ts +6 -0
- package/dist/assets/src/core/coordinator/driver.ts +8 -6
- package/dist/assets/src/core/runEvents.ts +50 -3
- package/dist/assets/src/core/runFriction.ts +2 -1
- package/dist/assets/src/core/runRecord.ts +107 -3
- package/dist/assets/src/core/runUsage.ts +199 -0
- package/dist/assets/src/core/ship/coordinator.ts +4 -2
- package/dist/assets/src/execution/residentRebind.ts +310 -0
- package/dist/assets/src/execution/residentSteps.ts +1 -0
- package/dist/assets/src/execution/sandboxErrors.ts +85 -10
- package/dist/assets/src/execution/sandboxLifecycle.ts +78 -0
- package/dist/assets/web/dist/.vite/manifest.json +18 -18
- package/dist/assets/web/dist/assets/{ResidentDetailPage-BBOpejGX.js → ResidentDetailPage-B1Q9pabX.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-B8kFWHpB.js → ResidentsIndexPage-CP7U_4aK.js} +1 -1
- package/dist/assets/web/dist/assets/{RunRoutePage-COWeVtIE.js → RunRoutePage-dCC25f_b.js} +4 -4
- package/dist/assets/web/dist/assets/{RunsIndexPage-BjH93cKx.js → RunsIndexPage-Cgp4t4C8.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-grlNKvCA.js → ScheduledPage-DthDA2xG.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-BQNoaXO6.js → StatusDot-DDc88Kbs.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-DVCeIzaa.js → Tooltip-CBapNhsh.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-B5Wfk-oY.js → dist-BnwSD1cL.js} +1 -1
- package/dist/assets/web/dist/assets/{main-CN0U7d6s.js → main-ZhQGbZ2E.js} +2 -2
- package/dist/cli.js +1277 -344
- package/package.json +1 -1
- 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
|
]);
|