@coreplane/switchboard 1.242.0 → 1.243.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 +20 -1
- package/dist/assets/deploy/cloudflare-memory/worker.ts +86 -29
- package/dist/assets/deploy/cloudflare-resident/worker.ts +105 -13
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -1
- package/dist/assets/project.json +3 -3
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +30 -34
- package/dist/assets/src/config/profile.ts +68 -3
- package/dist/assets/src/core/authz/policy.ts +4 -0
- package/dist/assets/src/core/budgets.ts +313 -0
- package/dist/assets/src/core/coordinator/driver.ts +0 -10
- package/dist/assets/src/core/costs.ts +9 -76
- package/dist/assets/src/core/modelPricing.ts +212 -0
- package/dist/assets/src/core/prDescriptionTypes.ts +29 -24
- package/dist/assets/src/core/reviewVerdict.ts +7 -0
- package/dist/assets/src/core/runEvents.ts +28 -4
- package/dist/assets/src/core/runFriction.ts +2 -1
- package/dist/assets/src/core/runRecord.ts +47 -0
- package/dist/assets/src/core/runUsage.ts +158 -47
- package/dist/assets/src/core/schedules.ts +3 -0
- package/dist/assets/src/core/ship/coordinator.ts +101 -84
- package/dist/assets/src/core/ship/handoff.ts +9 -0
- package/dist/assets/src/execution/bashTimeout.ts +8 -5
- package/dist/assets/src/execution/residentRefresh.ts +28 -3
- package/dist/assets/src/execution/residentSteps.ts +1 -1
- package/dist/assets/web/dist/.vite/manifest.json +67 -64
- package/dist/assets/web/dist/assets/CostsPage-CwXOmkeQ.js +2 -0
- package/dist/assets/web/dist/assets/HomePage-PRxjQiGG.js +2 -0
- package/dist/assets/web/dist/assets/PendingTurnRow-BT9RhFZ7.js +1 -0
- package/dist/assets/web/dist/assets/{ResidentDetailPage-DTMBgnIW.js → ResidentDetailPage-DACalNVF.js} +1 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-SHPdu6uZ.js +1 -0
- package/dist/assets/web/dist/assets/RunFoldRow-0SdOmOr5.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-BaFS2p8I.js +9 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-DYI-iALj.js +1 -0
- package/dist/assets/web/dist/assets/ScheduledPage-DJ8HiCPt.js +1 -0
- package/dist/assets/web/dist/assets/{SettingsPage-BIGio8Y0.js → SettingsPage-DLiN5IgY.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-ELoXHlFt.js → StatusDot-Dw0T1M-P.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-BoeFwYP2.js → Tooltip-BbLuIAiS.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-DicUG96U.js +1 -0
- package/dist/assets/web/dist/assets/{dist-BU5UivXC.js → dist-twkFmSUY.js} +1 -1
- package/dist/assets/web/dist/assets/format-BldUwl_R.js +1 -0
- package/dist/assets/web/dist/assets/indexRow-B_s5tKyq.js +1 -0
- package/dist/assets/web/dist/assets/{main-B2fX10aW.css → main-B4kEF3Sg.css} +1 -1
- package/dist/assets/web/dist/assets/{main-D4EA1g6n.js → main-d-w-tIKt.js} +2 -2
- package/dist/assets/web/dist/assets/{sseReplay-g7ml86LM.js → sseReplay-C9m_EB8J.js} +4 -4
- package/dist/cli.js +2758 -1445
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/CostsPage-CtmKhhOF.js +0 -2
- package/dist/assets/web/dist/assets/HomePage-QtH1EwYF.js +0 -2
- package/dist/assets/web/dist/assets/PendingTurnRow-BZA_vQt3.js +0 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-CaDvXhzJ.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-mgyLW0oV.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-DypJYMQa.js +0 -6
- package/dist/assets/web/dist/assets/RunsIndexPage-DYraPoWD.js +0 -1
- package/dist/assets/web/dist/assets/ScheduledPage-DXD2gLJk.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-jhCrWW3i.js +0 -1
- package/dist/assets/web/dist/assets/indexRow-BD1VT8o8.js +0 -1
- package/dist/assets/web/dist/assets/localIso-L06jV29p.js +0 -1
|
@@ -11,7 +11,11 @@ providers:
|
|
|
11
11
|
anthropic:
|
|
12
12
|
type: anthropic
|
|
13
13
|
apiKeyEnv: ANTHROPIC_API_KEY
|
|
14
|
-
#
|
|
14
|
+
# OpenAI itself runs on the same compatible adapter: any preset's model may
|
|
15
|
+
# be openai/<model-id> anywhere a model is accepted. The adapter speaks Chat
|
|
16
|
+
# Completions, not the Responses API, sends no reasoning control (our effort
|
|
17
|
+
# tiers do not reach these models), and meters tokens without pricing them.
|
|
18
|
+
# A first-class OpenAI block is a plan's business, not this file's.
|
|
15
19
|
openai:
|
|
16
20
|
type: openai-compatible
|
|
17
21
|
baseUrl: https://api.openai.com/v1
|
|
@@ -65,10 +69,19 @@ defaults:
|
|
|
65
69
|
# clipped (the card and the run record say so); a preset whose identity or
|
|
66
70
|
# class is above a cap is refused by name before any executor exists. Absent →
|
|
67
71
|
# nothing is capped and every preset runs exactly its declared profile.
|
|
72
|
+
# A fourth field, `confirm`, is the front door's and caps no run: the first
|
|
73
|
+
# blast-radius class a command the router bound from prose is handed back at
|
|
74
|
+
# (`To run this: …`) instead of run — `write` (every write is handed back; a
|
|
75
|
+
# read or a repository's own test or build runs) or `destructive` (a write
|
|
76
|
+
# runs at once; only a destructive one is handed back). It intersects toward
|
|
77
|
+
# caution like the run axes — a channel or a user can only ask MORE than what
|
|
78
|
+
# is set here — and unset everywhere it is the built-in `write`. `exec` and
|
|
79
|
+
# `never` are refused by name.
|
|
68
80
|
# boundary:
|
|
69
81
|
# maxMinutes: 120
|
|
70
82
|
# maxIdentity: write
|
|
71
83
|
# machines: [none, blank, repo-cold, repo-resident]
|
|
84
|
+
# confirm: write
|
|
72
85
|
|
|
73
86
|
# PR reviews and the reading diff (docs/reference/specs/reading-diff.md). Every
|
|
74
87
|
# review records its full `git diff`; the ABRIDGED version (meat.dev, run on
|
|
@@ -436,6 +449,12 @@ workspaceDir: ./workspaces
|
|
|
436
449
|
# r2Buckets: # optional: bucket name → label for buckets not named <worker>-cache
|
|
437
450
|
# some-other-bucket: uploads
|
|
438
451
|
# anthropicWorkspaceId: wrkspc_... # this app's Anthropic workspace
|
|
452
|
+
# prices: # optional: USD per million tokens by <provider>/<model>, the ref a run's
|
|
453
|
+
# openai/gpt-5: # spans name — for a model the Anthropic list does not know, or to
|
|
454
|
+
# input: 1.25 # override the list; all four kinds named (a provider with no
|
|
455
|
+
# output: 10 # cache-write charge says 0). A model neither knows is "unpriced",
|
|
456
|
+
# cacheRead: 0.125 # never $0, on the costs page and on a run's own page
|
|
457
|
+
# cacheWrite: 0
|
|
439
458
|
|
|
440
459
|
# Delivery indicators: GET /delivery and `delivery report` (docs/reference/specs/delivery.md).
|
|
441
460
|
# Optional. Needs a GitHub credential (the App triple or GH_TOKEN); read-only on GitHub.
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
isRunSession,
|
|
26
26
|
isRunVisibilityFilter,
|
|
27
27
|
normalizeStored,
|
|
28
|
+
pullRequestNumberOf,
|
|
28
29
|
RETENTION_BOUNDS,
|
|
29
30
|
RUN_EVENTS_DEFAULT_PAGE,
|
|
30
31
|
RUN_EVENTS_MAX_PAGE,
|
|
@@ -59,11 +60,11 @@ import {
|
|
|
59
60
|
} from "../../src/core/runLedger/sessionLog.ts";
|
|
60
61
|
import type { RunEvent } from "../../src/core/runEvents.ts";
|
|
61
62
|
import {
|
|
62
|
-
aggregateUsageByUser,
|
|
63
63
|
isRunUsage,
|
|
64
64
|
usageOfEvents,
|
|
65
65
|
type RunUsage,
|
|
66
|
-
type
|
|
66
|
+
type RunUsageRows,
|
|
67
|
+
type UsageIdentity,
|
|
67
68
|
type UsageRun,
|
|
68
69
|
} from "../../src/core/runUsage.ts";
|
|
69
70
|
import {
|
|
@@ -1265,13 +1266,15 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
1265
1266
|
this.migrateRunsTable();
|
|
1266
1267
|
// The indexes the visibility predicate's leaves walk (`channel_id IN`,
|
|
1267
1268
|
// `channel_visibility IN`, `user_id =`), each ordered like the page; the
|
|
1268
|
-
// session the sweep asks about; the parent a children listing filters on
|
|
1269
|
+
// session the sweep asks about; the parent a children listing filters on;
|
|
1270
|
+
// the pull request a findings listing filters on.
|
|
1269
1271
|
this.sql.exec(`
|
|
1270
1272
|
CREATE INDEX IF NOT EXISTS runs_channel_finished ON runs(channel_id, finished_at DESC, run_id DESC);
|
|
1271
1273
|
CREATE INDEX IF NOT EXISTS runs_visibility_finished ON runs(channel_visibility, finished_at DESC, run_id DESC);
|
|
1272
1274
|
CREATE INDEX IF NOT EXISTS runs_user_finished ON runs(user_id, finished_at DESC, run_id DESC);
|
|
1273
1275
|
CREATE INDEX IF NOT EXISTS runs_session ON runs(session_key);
|
|
1274
1276
|
CREATE INDEX IF NOT EXISTS runs_parent ON runs(parent_run_id, finished_at DESC, run_id DESC);
|
|
1277
|
+
CREATE INDEX IF NOT EXISTS runs_pr ON runs(repo, pr_number, finished_at DESC, run_id DESC);
|
|
1275
1278
|
`);
|
|
1276
1279
|
// The sessions registry (session-log item 7): every session log a run of
|
|
1277
1280
|
// this store claimed, with its thread — the sweep cannot enumerate the
|
|
@@ -1438,7 +1441,11 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
1438
1441
|
* aggregate fills it from the run's stored events. The parent a child names
|
|
1439
1442
|
* (run-history item 46), the column a children listing filters on: the
|
|
1440
1443
|
* record already carries it in `summary_json`, so existing rows are filled
|
|
1441
|
-
* from there once, and every later `put` writes it beside the row.
|
|
1444
|
+
* from there once, and every later `put` writes it beside the row. The pull
|
|
1445
|
+
* request a run names (run-history item 58; `pullRequestNumberOf`), the
|
|
1446
|
+
* column a findings listing filters on: the same one-time fill from
|
|
1447
|
+
* `summary_json` — the coding post-step's `pr`, else a posted review's
|
|
1448
|
+
* target — and every later `put` writes it beside the row. */
|
|
1442
1449
|
migrateRunsTable(): void {
|
|
1443
1450
|
const columns = new Set(
|
|
1444
1451
|
this.sql
|
|
@@ -1457,6 +1464,19 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
1457
1464
|
WHERE json_type(summary_json, '$.parentRunId') = 'text'`,
|
|
1458
1465
|
);
|
|
1459
1466
|
}
|
|
1467
|
+
if (!columns.has("pr_number")) {
|
|
1468
|
+
this.sql.exec(`ALTER TABLE runs ADD COLUMN pr_number INTEGER`);
|
|
1469
|
+
// `pullRequestNumberOf` in SQL: the coding post-step's pull request, else
|
|
1470
|
+
// the one a posted review targeted (a skipped post has no target).
|
|
1471
|
+
this.sql.exec(
|
|
1472
|
+
`UPDATE runs SET pr_number = COALESCE(
|
|
1473
|
+
CASE WHEN json_type(summary_json, '$.pr.number') = 'integer' THEN json_extract(summary_json, '$.pr.number') END,
|
|
1474
|
+
CASE WHEN json_extract(summary_json, '$.reviewPost.posted') = 1
|
|
1475
|
+
AND json_type(summary_json, '$.reviewPost.target.number') = 'integer'
|
|
1476
|
+
THEN json_extract(summary_json, '$.reviewPost.target.number') END
|
|
1477
|
+
)`,
|
|
1478
|
+
);
|
|
1479
|
+
}
|
|
1460
1480
|
}
|
|
1461
1481
|
|
|
1462
1482
|
private liveByThread(threadKey: string): LiveRunRow | undefined {
|
|
@@ -1970,8 +1990,8 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
1970
1990
|
);
|
|
1971
1991
|
this.sql.exec(
|
|
1972
1992
|
`INSERT INTO runs (run_id, label, agent, model, channel_id, user_id, thread_key, channel_visibility, repo, started_at, finished_at, stored_at, status,
|
|
1973
|
-
event_count, stored_event_count, truncated, bytes, diagnosis_json, summary_json, session_key, usage_json, parent_run_id)
|
|
1974
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1993
|
+
event_count, stored_event_count, truncated, bytes, diagnosis_json, summary_json, session_key, usage_json, parent_run_id, pr_number)
|
|
1994
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1975
1995
|
ON CONFLICT(run_id) DO UPDATE SET
|
|
1976
1996
|
label = excluded.label, agent = excluded.agent, model = excluded.model, channel_id = excluded.channel_id,
|
|
1977
1997
|
user_id = excluded.user_id, thread_key = excluded.thread_key, channel_visibility = excluded.channel_visibility,
|
|
@@ -1981,7 +2001,8 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
1981
2001
|
bytes = excluded.bytes, diagnosis_json = excluded.diagnosis_json, summary_json = excluded.summary_json,
|
|
1982
2002
|
session_key = excluded.session_key,
|
|
1983
2003
|
usage_json = COALESCE(excluded.usage_json, runs.usage_json),
|
|
1984
|
-
parent_run_id = excluded.parent_run_id
|
|
2004
|
+
parent_run_id = excluded.parent_run_id,
|
|
2005
|
+
pr_number = excluded.pr_number`,
|
|
1985
2006
|
stored.id,
|
|
1986
2007
|
stored.label ?? null,
|
|
1987
2008
|
stored.agent ?? null,
|
|
@@ -2004,6 +2025,7 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
2004
2025
|
stored.session?.key ?? null,
|
|
2005
2026
|
stored.usage ? JSON.stringify(stored.usage) : null,
|
|
2006
2027
|
stored.parentRunId ?? null,
|
|
2028
|
+
pullRequestNumberOf(stored) ?? null,
|
|
2007
2029
|
);
|
|
2008
2030
|
// The session's registry row learns its newest finish (session-log item
|
|
2009
2031
|
// 7); a record that reaches the store without a claim (the plain put
|
|
@@ -2208,28 +2230,32 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
2208
2230
|
}
|
|
2209
2231
|
|
|
2210
2232
|
/**
|
|
2211
|
-
*
|
|
2212
|
-
*
|
|
2213
|
-
*
|
|
2214
|
-
*
|
|
2215
|
-
*
|
|
2233
|
+
* What the runs that finished in [sinceMs, untilMs) and are inside the
|
|
2234
|
+
* retention window cost (costs.md items 10–10a; run-history item 56): one row
|
|
2235
|
+
* per run — its requester, thread, channel, agent and usage — plus the
|
|
2236
|
+
* identity of every parent a child names that is outside the batch, so the
|
|
2237
|
+
* bot bills the child without a second read. The arithmetic is the bot's; the
|
|
2238
|
+
* Worker only reads its rows. A row written before `usage_json` existed is
|
|
2239
|
+
* filled in here from its stored `model.turn` events — up to
|
|
2240
|
+
* USAGE_BACKFILL_PER_CALL a call, the rest answered without `usage` and
|
|
2241
|
+
* counted in `pending` — so the history heals as it is read, with no
|
|
2216
2242
|
* operator step; a run without turns is written back as the zero usage so it
|
|
2217
|
-
* is not re-read.
|
|
2218
|
-
* outside the range is looked up by id.
|
|
2243
|
+
* is not re-read.
|
|
2219
2244
|
*/
|
|
2220
|
-
|
|
2245
|
+
usage(sinceMs: number, untilMs: number): RunUsageRows {
|
|
2221
2246
|
const now = systemClock();
|
|
2222
2247
|
const { policy } = this.policyState();
|
|
2223
2248
|
const cutoff = now - policy.retentionDays * 86_400_000;
|
|
2224
2249
|
const rows = this.sql
|
|
2225
2250
|
.exec<UsageRow>(
|
|
2226
|
-
`SELECT run_id, user_id, started_at, finished_at, usage_json, summary_json FROM runs
|
|
2251
|
+
`SELECT run_id, user_id, agent, channel_id, thread_key, started_at, finished_at, usage_json, summary_json FROM runs
|
|
2227
2252
|
WHERE finished_at >= ? AND finished_at < ? ORDER BY finished_at ASC, run_id ASC`,
|
|
2228
2253
|
Math.max(sinceMs, cutoff),
|
|
2229
2254
|
untilMs,
|
|
2230
2255
|
)
|
|
2231
2256
|
.toArray();
|
|
2232
2257
|
let backfilled = 0;
|
|
2258
|
+
let pending = 0;
|
|
2233
2259
|
const runs: UsageRun[] = rows.map((row) => {
|
|
2234
2260
|
let usage = parseUsageJson(row.usage_json);
|
|
2235
2261
|
if (usage === undefined && backfilled < USAGE_BACKFILL_PER_CALL) {
|
|
@@ -2237,31 +2263,38 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
2237
2263
|
this.sql.exec(`UPDATE runs SET usage_json = ? WHERE run_id = ?`, JSON.stringify(usage), row.run_id);
|
|
2238
2264
|
backfilled += 1;
|
|
2239
2265
|
}
|
|
2266
|
+
if (usage === undefined) pending += 1;
|
|
2240
2267
|
const who = identityOfSummary(row.summary_json);
|
|
2241
2268
|
return {
|
|
2242
2269
|
id: row.run_id,
|
|
2243
2270
|
userId: row.user_id,
|
|
2244
2271
|
...(who.userName ? { userName: who.userName } : {}),
|
|
2245
2272
|
...(who.parentRunId ? { parentRunId: who.parentRunId } : {}),
|
|
2273
|
+
threadKey: row.thread_key,
|
|
2274
|
+
channelId: row.channel_id,
|
|
2275
|
+
...(row.agent ? { agent: row.agent } : {}),
|
|
2246
2276
|
startedAt: row.started_at,
|
|
2247
2277
|
finishedAt: row.finished_at,
|
|
2248
2278
|
...(usage ? { usage } : {}),
|
|
2249
2279
|
};
|
|
2250
2280
|
});
|
|
2251
|
-
|
|
2281
|
+
// The parents a child names that are not in the batch: looked up once each, known or not.
|
|
2282
|
+
const inBatch = new Set(runs.map((r) => r.id));
|
|
2283
|
+
const parents: Record<string, UsageIdentity> = {};
|
|
2284
|
+
for (const id of new Set(runs.map((r) => r.parentRunId).filter((p): p is string => !!p && !inBatch.has(p)))) {
|
|
2252
2285
|
const p = this.sql
|
|
2253
2286
|
.exec<{ user_id: string; summary_json: string }>(`SELECT user_id, summary_json FROM runs WHERE run_id = ?`, id)
|
|
2254
2287
|
.toArray()[0];
|
|
2255
|
-
if (!p)
|
|
2288
|
+
if (!p) continue;
|
|
2256
2289
|
const who = identityOfSummary(p.summary_json);
|
|
2257
|
-
|
|
2258
|
-
}
|
|
2259
|
-
const { rows: out, pending } = aggregateUsageByUser(runs, lookupParent);
|
|
2290
|
+
parents[id] = { userId: p.user_id, ...(who.userName ? { userName: who.userName } : {}) };
|
|
2291
|
+
}
|
|
2260
2292
|
const earliest = this.sql
|
|
2261
2293
|
.exec<{ m: number | null }>(`SELECT MIN(finished_at) AS m FROM runs WHERE finished_at >= ?`, cutoff)
|
|
2262
2294
|
.one().m;
|
|
2263
2295
|
return {
|
|
2264
|
-
|
|
2296
|
+
runs,
|
|
2297
|
+
parents,
|
|
2265
2298
|
pending,
|
|
2266
2299
|
...(earliest !== null ? { earliestFinishedAt: earliest } : {}),
|
|
2267
2300
|
retentionDays: policy.retentionDays,
|
|
@@ -2343,6 +2376,11 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
2343
2376
|
where.push(`parent_run_id = ?`);
|
|
2344
2377
|
params.push(q.parentRunId);
|
|
2345
2378
|
}
|
|
2379
|
+
if (q.pr !== undefined) {
|
|
2380
|
+
// `namesPullRequest` in SQL: the row's repository and the number it names.
|
|
2381
|
+
where.push(`repo = ?`, `pr_number = ?`);
|
|
2382
|
+
params.push(q.pr.repo, q.pr.number);
|
|
2383
|
+
}
|
|
2346
2384
|
if (q.visibleTo !== undefined && q.visibleTo.kind !== "all") where.push(visibilitySql(q.visibleTo, params));
|
|
2347
2385
|
const select = `SELECT run_id, agent, channel_id, finished_at, bytes, event_count, summary_json FROM runs WHERE ${where.join(" AND ")} ORDER BY finished_at DESC, run_id DESC`;
|
|
2348
2386
|
// `LIMIT` holds on the over-bound path too: the kept set is the newest
|
|
@@ -2423,6 +2461,9 @@ const USAGE_BACKFILL_PER_CALL = 200;
|
|
|
2423
2461
|
type UsageRow = {
|
|
2424
2462
|
run_id: string;
|
|
2425
2463
|
user_id: string;
|
|
2464
|
+
agent: string | null;
|
|
2465
|
+
channel_id: string;
|
|
2466
|
+
thread_key: string;
|
|
2426
2467
|
started_at: number;
|
|
2427
2468
|
finished_at: number;
|
|
2428
2469
|
usage_json: string | null;
|
|
@@ -2590,12 +2631,26 @@ function parseRunList(body: unknown): Validated<{ storeKey: string; query: RunLi
|
|
|
2590
2631
|
if (!id.ok) return invalid("parentRunId must match ^[A-Za-z0-9_-]{1,64}$");
|
|
2591
2632
|
query.parentRunId = id.value;
|
|
2592
2633
|
}
|
|
2634
|
+
if (b.pr !== undefined) {
|
|
2635
|
+
const pr = b.pr as Record<string, unknown> | null;
|
|
2636
|
+
if (
|
|
2637
|
+
typeof pr !== "object" ||
|
|
2638
|
+
pr === null ||
|
|
2639
|
+
typeof pr.repo !== "string" ||
|
|
2640
|
+
!REPO_SLUG.test(pr.repo) ||
|
|
2641
|
+
typeof pr.number !== "number" ||
|
|
2642
|
+
!Number.isInteger(pr.number) ||
|
|
2643
|
+
pr.number < 1
|
|
2644
|
+
)
|
|
2645
|
+
return invalid("pr must be { repo: owner/name, number: a positive integer }");
|
|
2646
|
+
query.pr = { repo: pr.repo, number: pr.number };
|
|
2647
|
+
}
|
|
2593
2648
|
if (b.visibleTo !== undefined) {
|
|
2594
2649
|
// A malformed filter is a 400, never "all": the bot degrades to live rows
|
|
2595
2650
|
// rather than the DO widening what an actor may see.
|
|
2596
2651
|
if (!isRunVisibilityFilter(b.visibleTo)) return invalid("visibleTo must be a run visibility filter");
|
|
2597
|
-
|
|
2598
|
-
|
|
2652
|
+
const headroom = DO_MAX_BOUND_PARAMETERS - RUN_LIST_BASE_PARAMETERS - (query.pr ? RUN_LIST_PR_PARAMETERS : 0);
|
|
2653
|
+
if (boundParameters(b.visibleTo) > headroom) return invalid(`visibleTo names more than ${headroom} ids`);
|
|
2599
2654
|
query.visibleTo = b.visibleTo;
|
|
2600
2655
|
}
|
|
2601
2656
|
return { ok: true, value: { storeKey: key.value, query } };
|
|
@@ -2604,6 +2659,8 @@ function parseRunList(body: unknown): Validated<{ storeKey: string; query: RunLi
|
|
|
2604
2659
|
/** Parameters the page query binds before any filter: the cursor pair (3) and the age floor (1),
|
|
2605
2660
|
* plus `agent`, `channel`, `threadKey`, `parentRunId`, and the LIMIT at most — the headroom `visibleTo` must fit under. */
|
|
2606
2661
|
const RUN_LIST_BASE_PARAMETERS = 9;
|
|
2662
|
+
/** The two more a `pr` filter binds (`repo`, `pr_number`), taken from the same headroom only when asked. */
|
|
2663
|
+
const RUN_LIST_PR_PARAMETERS = 2;
|
|
2607
2664
|
|
|
2608
2665
|
/** How many `?` a filter binds (one per id, one per user). */
|
|
2609
2666
|
function boundParameters(f: RunVisibilityFilter): number {
|
|
@@ -3800,13 +3857,13 @@ async function handleRuns(pathname: string, body: unknown, env: Env): Promise<Re
|
|
|
3800
3857
|
);
|
|
3801
3858
|
return json(result ?? { events: null });
|
|
3802
3859
|
}
|
|
3803
|
-
if (pathname === "/runs/usage
|
|
3860
|
+
if (pathname === "/runs/usage") {
|
|
3804
3861
|
const parsed = parseRunUsageQuery(body);
|
|
3805
3862
|
if (!parsed.ok) return json({ error: parsed.error }, 400);
|
|
3806
3863
|
const { storeKey, sinceMs, untilMs } = parsed.value;
|
|
3807
|
-
const
|
|
3808
|
-
console.log(`[runs/usage
|
|
3809
|
-
return json(
|
|
3864
|
+
const rows = await stub(storeKey).usage(sinceMs, untilMs);
|
|
3865
|
+
console.log(`[runs/usage] ${storeKey} -> ${rows.runs.length} runs, ${rows.pending} pending`);
|
|
3866
|
+
return json(rows);
|
|
3810
3867
|
}
|
|
3811
3868
|
// /runs/delete
|
|
3812
3869
|
const parsed = parseRunTarget(body);
|
|
@@ -3831,7 +3888,7 @@ const ROUTES = new Set([
|
|
|
3831
3888
|
"/runs/summary",
|
|
3832
3889
|
"/runs/list",
|
|
3833
3890
|
"/runs/events",
|
|
3834
|
-
"/runs/usage
|
|
3891
|
+
"/runs/usage",
|
|
3835
3892
|
"/runs/delete",
|
|
3836
3893
|
...LEDGER_ROUTES,
|
|
3837
3894
|
]);
|
|
@@ -179,6 +179,7 @@ import {
|
|
|
179
179
|
isRuntimeUnreachableSignal,
|
|
180
180
|
killStaleBuildProcessesCommand,
|
|
181
181
|
planRefresh,
|
|
182
|
+
RUNTIME_MOVED_WORDING,
|
|
182
183
|
RUNTIME_REPLACEMENT_WORDING,
|
|
183
184
|
RUNTIME_UNREACHABLE_DOWN_AT,
|
|
184
185
|
runtimeUnreachableReason,
|
|
@@ -766,9 +767,34 @@ function isRuntimeReplacement(err: unknown): boolean {
|
|
|
766
767
|
return false;
|
|
767
768
|
}
|
|
768
769
|
|
|
770
|
+
/** Whether the SDK's failure vouches that the runtime under the command MOVED
|
|
771
|
+
* — a new incarnation serves the thread, so the container the command was
|
|
772
|
+
* aimed at is gone whatever else the resident knows: the typed classes for
|
|
773
|
+
* a stale process handle and an inactive runtime identity, an interruption
|
|
774
|
+
* whose reason is a replaced runtime, the platform's superseded-isolate
|
|
775
|
+
* reset, and the SDK's wordings for the same (`RUNTIME_MOVED_WORDING`, on the
|
|
776
|
+
* error and its cause chain). NOT the words for a container that is merely
|
|
777
|
+
* down (`STOPPED_CONTAINER_WORDING`: "The container is not running", "Process
|
|
778
|
+
* supervisor is closed") and NOT a transport lost under the call
|
|
779
|
+
* (`RPCTransportError`): an asleep or starting container and a network blip
|
|
780
|
+
* answer those too, so on `/exec` they are the word only when the resident
|
|
781
|
+
* itself knows the container it held is gone (`replacedExecAnswer`). The
|
|
782
|
+
* resident's own steps keep the union (`isRuntimeReplacement`): a step
|
|
783
|
+
* interrupted either way is retried onto the container that comes back. */
|
|
784
|
+
function sdkVouchesRuntimeMoved(err: unknown): boolean {
|
|
785
|
+
if (err instanceof StaleProcessHandleError) return true;
|
|
786
|
+
if (err instanceof RuntimeIdentityInactiveError) return true;
|
|
787
|
+
if (err instanceof OperationInterruptedError && RUNTIME_REPLACED_REASONS.has(err.reason)) return true;
|
|
788
|
+
if (isDurableObjectCodeUpdateReset(err)) return true;
|
|
789
|
+
for (const link of selfAndCauses(err)) if (RUNTIME_MOVED_WORDING.test(errMsg(link))) return true;
|
|
790
|
+
return false;
|
|
791
|
+
}
|
|
792
|
+
|
|
769
793
|
/** The named ThreadErr every thread route (exec/read/write) answers for a
|
|
770
794
|
* runtime replacement, so the client can classify it (409 like the other
|
|
771
|
-
* recoverable thread states; `reason` is the discriminator).
|
|
795
|
+
* recoverable thread states; `reason` is the discriminator). On `/exec` the
|
|
796
|
+
* answer goes through `replacedExecAnswer` first: the word only when the SDK
|
|
797
|
+
* vouched the runtime moved or the resident knows the container is gone. */
|
|
772
798
|
function runtimeReplacedErr(err: RuntimeReplacedError): ThreadErr {
|
|
773
799
|
return { error: err.message, status: 409, reason: "runtime-replaced" };
|
|
774
800
|
}
|
|
@@ -1592,6 +1618,52 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
1592
1618
|
* survives, so hydration state is always probed from disk. */
|
|
1593
1619
|
private hydration: Promise<void> | null = null;
|
|
1594
1620
|
|
|
1621
|
+
/** The container stop the platform delivered to `onStop` and no command has
|
|
1622
|
+
* answered since: the resident's own knowledge that the container it held
|
|
1623
|
+
* is gone (docs/reference/specs/resident-repos.md item 43). In memory on
|
|
1624
|
+
* purpose — a Durable Object that restarts learns the same fact from the
|
|
1625
|
+
* restore its next command's wake path starts (`restoring`), and a stop
|
|
1626
|
+
* recorded in storage would outlive the container that answers now. */
|
|
1627
|
+
private containerStop: { at: number; exitCode: number; reason: string } | undefined;
|
|
1628
|
+
|
|
1629
|
+
/** The platform's signal that the container stopped — the rollout's
|
|
1630
|
+
* `runtime_signal`, or an exit of its own — recorded before the SDK's
|
|
1631
|
+
* reconciliation runs, so an `/exec` that meets the stopped container from
|
|
1632
|
+
* here on answers `runtime-replaced` (`replacedExecAnswer`) rather than the
|
|
1633
|
+
* binding's bare "The container is not running". The Sandbox base declares
|
|
1634
|
+
* the hook without parameters; the Container base beneath it passes them. */
|
|
1635
|
+
async onStop(params?: { exitCode: number; reason: string }): Promise<void> {
|
|
1636
|
+
this.containerStop = { at: systemClock(), exitCode: params?.exitCode ?? 0, reason: params?.reason ?? "unknown" };
|
|
1637
|
+
console.log(
|
|
1638
|
+
`container: stopped (exit ${this.containerStop.exitCode}, ${this.containerStop.reason}) — the container this resident held is gone; /exec answers runtime-replaced until a command answers again`,
|
|
1639
|
+
);
|
|
1640
|
+
await super.onStop();
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
/** Whether this resident knows the container it held is gone: it saw the
|
|
1644
|
+
* container stop and no command has answered since, or a restore is under
|
|
1645
|
+
* way (`restoring`, persisted before any restore work). Never the
|
|
1646
|
+
* platform's `running` flag — an asleep or starting container answers
|
|
1647
|
+
* false to it too, and an `/exec` that meets one must say what the SDK
|
|
1648
|
+
* said so the harness probes rather than relaunch into a container that
|
|
1649
|
+
* was never replaced. */
|
|
1650
|
+
private async knowsContainerGone(): Promise<boolean> {
|
|
1651
|
+
if (this.containerStop !== undefined) return true;
|
|
1652
|
+
return (await this.getStatus()).state === "restoring";
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
/** The `/exec` answer for a command the SDK failed with a runtime
|
|
1656
|
+
* replacement (item 43; harness.md item 6): the word — `reason:
|
|
1657
|
+
* "runtime-replaced"`, the resident's sentence — when the SDK vouched the
|
|
1658
|
+
* runtime moved or the resident knows the container it held is gone;
|
|
1659
|
+
* otherwise what the SDK said, with no word and no `reason`, so the
|
|
1660
|
+
* harness's one more command decides. The command is never re-issued
|
|
1661
|
+
* either way: it may have started. */
|
|
1662
|
+
private async replacedExecAnswer(err: RuntimeReplacedError): Promise<ThreadErr> {
|
|
1663
|
+
if (sdkVouchesRuntimeMoved(err.cause) || (await this.knowsContainerGone())) return runtimeReplacedErr(err);
|
|
1664
|
+
return { error: errMsg(err.cause), status: 409 };
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1595
1667
|
/** The step trace of the request in flight (docs/reference/specs/tracing.md item 19):
|
|
1596
1668
|
* `attachThread` and `runOp` each run inside their own collector, so the
|
|
1597
1669
|
* commands `runOk` runs and the mirror-lock waits land on the answer that
|
|
@@ -1978,7 +2050,9 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
1978
2050
|
proc = await createExtensionProcessSandbox(this).exec(argv as unknown as SandboxCommand, launch);
|
|
1979
2051
|
}
|
|
1980
2052
|
// The spawn is the proof the control port answers: a persisted count of
|
|
1981
|
-
// unanswered connects ends here, whatever the command goes on to do
|
|
2053
|
+
// unanswered connects ends here, whatever the command goes on to do — and
|
|
2054
|
+
// a container stop seen before it is history (`knowsContainerGone`).
|
|
2055
|
+
this.containerStop = undefined;
|
|
1982
2056
|
await this.clearRuntimeUnreachable();
|
|
1983
2057
|
try {
|
|
1984
2058
|
const out = await proc.output({ encoding: "utf8", timeout: timeout + 30_000 });
|
|
@@ -6233,11 +6307,30 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6233
6307
|
return res;
|
|
6234
6308
|
}
|
|
6235
6309
|
|
|
6310
|
+
/** The exec route's one gate for a runtime replacement (item 43): every
|
|
6311
|
+
* `RuntimeReplacedError` the route meets — the preflight's own probe of the
|
|
6312
|
+
* worktree, the command itself — is judged by `replacedExecAnswer`, so the
|
|
6313
|
+
* word reaches the harness only when the SDK vouched the runtime moved or
|
|
6314
|
+
* the resident knows the container it held is gone. */
|
|
6236
6315
|
private async execThreadImpl(
|
|
6237
6316
|
threadKey: string,
|
|
6238
6317
|
command: string,
|
|
6239
6318
|
timeoutMs: number,
|
|
6240
6319
|
env?: Record<string, string>,
|
|
6320
|
+
): Promise<{ stdout: string; stderr: string; exitCode: number; truncated: boolean } | ThreadErr> {
|
|
6321
|
+
try {
|
|
6322
|
+
return await this.execThreadBody(threadKey, command, timeoutMs, env);
|
|
6323
|
+
} catch (err) {
|
|
6324
|
+
if (err instanceof RuntimeReplacedError) return this.replacedExecAnswer(err);
|
|
6325
|
+
throw err;
|
|
6326
|
+
}
|
|
6327
|
+
}
|
|
6328
|
+
|
|
6329
|
+
private async execThreadBody(
|
|
6330
|
+
threadKey: string,
|
|
6331
|
+
command: string,
|
|
6332
|
+
timeoutMs: number,
|
|
6333
|
+
env?: Record<string, string>,
|
|
6241
6334
|
): Promise<{ stdout: string; stderr: string; exitCode: number; truncated: boolean } | ThreadErr> {
|
|
6242
6335
|
const pre = await this.threadPreflight(threadKey);
|
|
6243
6336
|
if ("error" in pre) return pre;
|
|
@@ -6253,13 +6346,8 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6253
6346
|
: {}),
|
|
6254
6347
|
} satisfies ThreadBinding);
|
|
6255
6348
|
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
r = await this.threadRunCapped(binding.user, binding.worktreePath, command, timeoutMs, EXEC_OUTPUT_CAP, env);
|
|
6259
|
-
} catch (err) {
|
|
6260
|
-
if (err instanceof RuntimeReplacedError) return runtimeReplacedErr(err);
|
|
6261
|
-
throw err;
|
|
6262
|
-
}
|
|
6349
|
+
// A runtime replacement under the command propagates to `execThreadImpl`'s gate.
|
|
6350
|
+
const r = await this.threadRunCapped(binding.user, binding.worktreePath, command, timeoutMs, EXEC_OUTPUT_CAP, env);
|
|
6263
6351
|
const truncated = r.stdout.length > EXEC_OUTPUT_CAP || r.stderr.length > EXEC_OUTPUT_CAP || r.truncated === true;
|
|
6264
6352
|
const notes: string[] = [];
|
|
6265
6353
|
if (r.timedOut)
|
|
@@ -7411,13 +7499,17 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
7411
7499
|
return { infraStreak: { ...row, rung: infraStreakRung(count) } };
|
|
7412
7500
|
}
|
|
7413
7501
|
|
|
7414
|
-
/** Fault injection for item 67: corrupt the mirror on disk
|
|
7415
|
-
*
|
|
7416
|
-
*
|
|
7502
|
+
/** Fault injection for item 67: corrupt the mirror on disk so the next
|
|
7503
|
+
* cycle's `fetch` fails in a resident step. The mirror's `config` goes (its
|
|
7504
|
+
* `origin` remote with it: `'origin' does not appear to be a git
|
|
7505
|
+
* repository`, exit 128) — NOT its objects: the wake check (`readyStamp`)
|
|
7506
|
+
* tests `objects/`, and a missing directory is a disk to restore, which the
|
|
7507
|
+
* wake did before any step could fail the first time this was tried live.
|
|
7508
|
+
* The snapshots in R2 are untouched — the ladder's recreate restores them.
|
|
7417
7509
|
* Test-only semantics; admin scope. */
|
|
7418
7510
|
async debugBreakMirror(): Promise<{ broken: boolean; error?: string }> {
|
|
7419
7511
|
try {
|
|
7420
|
-
await this.runOk(["rm", "-
|
|
7512
|
+
await this.runOk(["rm", "-f", `${MIRROR_DIR}/config`], "break-mirror");
|
|
7421
7513
|
return { broken: true };
|
|
7422
7514
|
} catch (err) {
|
|
7423
7515
|
return { broken: false, error: errMsg(err) };
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.243.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "switchboard",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.243.0",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"workspaces": [
|
|
12
12
|
"web",
|
|
@@ -20445,7 +20445,7 @@
|
|
|
20445
20445
|
},
|
|
20446
20446
|
"packages/switchboard": {
|
|
20447
20447
|
"name": "@coreplane/switchboard",
|
|
20448
|
-
"version": "1.
|
|
20448
|
+
"version": "1.243.0",
|
|
20449
20449
|
"license": "Apache-2.0",
|
|
20450
20450
|
"dependencies": {
|
|
20451
20451
|
"@earendil-works/pi-ai": "0.85.1",
|
package/dist/assets/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.243.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Mention it in Slack and an agent reviews the PR, ships the fix, or answers the question — on the model you choose, with its tools running where you decide.",
|
|
6
6
|
"license": "Apache-2.0",
|
package/dist/assets/project.json
CHANGED
|
@@ -168,11 +168,11 @@
|
|
|
168
168
|
"when": "Part of `check:consistency`."
|
|
169
169
|
},
|
|
170
170
|
"clock:gen": {
|
|
171
|
-
"does": "Regenerates
|
|
171
|
+
"does": "Regenerates both clock allowlists from the tree: wall-clock reads (empty) and duration literals outside `src/core/budgets.ts`.",
|
|
172
172
|
"when": "Part of `fix`."
|
|
173
173
|
},
|
|
174
174
|
"clock:check": {
|
|
175
|
-
"does": "No production file reads the wall clock directly
|
|
175
|
+
"does": "No production file reads the wall clock directly or gained a duration literal outside `src/core/budgets.ts`; both allowlists match the tree.",
|
|
176
176
|
"when": "Part of `check:consistency`."
|
|
177
177
|
},
|
|
178
178
|
"docs:changed": {
|
|
@@ -212,7 +212,7 @@
|
|
|
212
212
|
"when": "Part of `check:consistency`."
|
|
213
213
|
},
|
|
214
214
|
"load": {
|
|
215
|
-
"does": "Load harness: `-- history|resident|sandbox|e2e|cards|provider|pi|route`.",
|
|
215
|
+
"does": "Load harness: `-- history|resident|sandbox|e2e|cards|provider|pi|route|door`.",
|
|
216
216
|
"when": "Capacity receipts (docs/reference/specs/load-harness.md)."
|
|
217
217
|
}
|
|
218
218
|
}
|
package/dist/assets/source.json
CHANGED