@coreplane/switchboard 1.228.0 → 1.230.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 +17 -13
- package/dist/assets/deploy/cloudflare-memory/worker.ts +56 -29
- package/dist/assets/deploy/cloudflare-resident/worker.ts +80 -36
- package/dist/assets/package-lock.json +2400 -26
- package/dist/assets/package.json +3 -2
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +4 -34
- package/dist/assets/src/core/authz/actor.ts +24 -6
- package/dist/assets/src/core/authz/policy.ts +8 -0
- package/dist/assets/src/core/authz/resource.ts +3 -1
- package/dist/assets/src/core/authz/types.ts +10 -1
- package/dist/assets/src/core/chatMessage.ts +1 -1
- package/dist/assets/src/core/coordinator/contract.ts +19 -0
- package/dist/assets/src/core/runEvents.ts +38 -15
- package/dist/assets/src/core/runFriction.ts +5 -4
- package/dist/assets/src/core/runLedger/sessionLog.ts +13 -0
- package/dist/assets/src/core/runRecord.ts +33 -0
- package/dist/assets/src/core/trace/attrs.ts +3 -0
- package/dist/assets/src/core/trace/streamSpans.ts +4 -1
- package/dist/assets/src/execution/residentDepCache.ts +34 -8
- package/dist/assets/src/execution/residentRebind.ts +27 -4
- package/dist/assets/web/dist/.vite/manifest.json +20 -20
- package/dist/assets/web/dist/assets/CostsPage-B08bdD-1.js +2 -0
- package/dist/assets/web/dist/assets/{ResidentDetailPage-B1Q9pabX.js → ResidentDetailPage-DYxDruS0.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-CP7U_4aK.js → ResidentsIndexPage-DIBRBmuq.js} +1 -1
- package/dist/assets/web/dist/assets/RunRoutePage-B4s8hJlW.js +13 -0
- package/dist/assets/web/dist/assets/{RunsIndexPage-Cgp4t4C8.js → RunsIndexPage-CaW07KOf.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-DthDA2xG.js → ScheduledPage-Cr7269s9.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-DDc88Kbs.js → StatusDot-lCLi16zY.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-CBapNhsh.js → Tooltip-DKMPwNyy.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-BnwSD1cL.js → dist-DRkTB2rt.js} +1 -1
- package/dist/assets/web/dist/assets/main-CAVqMbiX.css +1 -0
- package/dist/assets/web/dist/assets/{main-ZhQGbZ2E.js → main-CXKOiCOK.js} +2 -2
- package/dist/cli.js +8166 -8242
- package/package.json +1 -2
- package/dist/assets/web/dist/assets/CostsPage-5pl3HB2F.js +0 -2
- package/dist/assets/web/dist/assets/RunRoutePage-dCC25f_b.js +0 -12
- package/dist/assets/web/dist/assets/main-xLAsdkfB.css +0 -1
|
@@ -317,19 +317,13 @@ workspaceDir: ./workspaces
|
|
|
317
317
|
# maxRounds: 3 # review rounds per unit
|
|
318
318
|
# maxMinutes: 120 # the ship preset's wall-clock budget in minutes (the registry's default)
|
|
319
319
|
|
|
320
|
-
#
|
|
321
|
-
#
|
|
322
|
-
# inside the run's own execution container
|
|
323
|
-
#
|
|
324
|
-
#
|
|
325
|
-
#
|
|
326
|
-
#
|
|
327
|
-
# pi in the run's container; a preset without one (general, research,
|
|
328
|
-
# conductor) runs it as a child of the bot, with none of pi's own shell or
|
|
329
|
-
# file tools. A preset the registry does not know, or a value that is
|
|
330
|
-
# neither harness, fails the load by name.
|
|
331
|
-
# harness:
|
|
332
|
-
# coding: pi
|
|
320
|
+
# `harness` is a retired key (docs/reference/specs/harness-pi.md item 1). It
|
|
321
|
+
# moved the presets onto pi one at a time; every preset runs on pi now — the pi
|
|
322
|
+
# coding agent started inside the run's own execution container (or as a child
|
|
323
|
+
# of the bot for a preset without a workspace) with the run's model-proxy
|
|
324
|
+
# bearer as its only key — and there is no other loop. A block written during
|
|
325
|
+
# the series still loads when every value is `pi`; `native` names the deleted
|
|
326
|
+
# loop and fails the load by name, as does an unknown preset. Remove the block.
|
|
333
327
|
|
|
334
328
|
# What the harness writes into pi's per-run settings for every run on pi
|
|
335
329
|
# (docs/reference/specs/harness-pi.md item 4): pi's compaction thresholds, in
|
|
@@ -369,6 +363,16 @@ workspaceDir: ./workspaces
|
|
|
369
363
|
# model: anthropic/claude-haiku-4-5
|
|
370
364
|
# answer: text
|
|
371
365
|
|
|
366
|
+
# Linked threads (docs/decisions/0037-a-linked-thread-is-quoted-not-joined.md;
|
|
367
|
+
# docs/reference/specs/routing-and-config.md item 22). Off by default. On, a
|
|
368
|
+
# permalink in a request to a thread in another PUBLIC channel the bot is a
|
|
369
|
+
# member of is quoted onto the request turn as an untrusted block — text only,
|
|
370
|
+
# at most three per request, never a private channel from elsewhere, never a
|
|
371
|
+
# DM or a Slack Connect channel — and the run's record and page show what was
|
|
372
|
+
# quoted. A bot older than 1.228.0 refuses a config that carries this key.
|
|
373
|
+
# references:
|
|
374
|
+
# enabled: true
|
|
375
|
+
|
|
372
376
|
# Spend reporting: GET /costs (Access-gated). Optional. See docs/reference/specs/costs.md.
|
|
373
377
|
# costs:
|
|
374
378
|
# cloudflareAccountId: <32-hex account id>
|
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
planSessionTrim,
|
|
52
52
|
roleOfStoredRow,
|
|
53
53
|
rowKind,
|
|
54
|
+
SEARCH_MAX_HITS,
|
|
54
55
|
sessionsToDrop,
|
|
55
56
|
tailCut,
|
|
56
57
|
textOfStoredRow,
|
|
@@ -148,7 +149,7 @@ const traceSinks = [workerLogSink((line) => console.log(line))];
|
|
|
148
149
|
// keeps the 512 KB one.
|
|
149
150
|
// POST /runs/put {storeKey, record, policy?, policyUpdatedAt?} → {ok, retained, stored, rewritten}
|
|
150
151
|
// POST /runs/get {storeKey, id} → {record: RunRecord | null} (unknown/expired: null, 200)
|
|
151
|
-
// POST /runs/list {storeKey, limit?, before?, beforeId?, sinceMs?, agent?, channel?, threadKey?}
|
|
152
|
+
// POST /runs/list {storeKey, limit?, before?, beforeId?, sinceMs?, agent?, channel?, threadKey?, parentRunId?}
|
|
152
153
|
// → {items: RunListItem[], nextBefore?: {finishedAt, id}} (cursor = the last row's list key)
|
|
153
154
|
// POST /runs/events {storeKey, id, afterSeq?, limit?} → {events: (RunEvent & {seq})[] | null, nextAfterSeq?}
|
|
154
155
|
// (`events: null` when the run is unknown or hidden by retention; `seq` is the registry's stamp)
|
|
@@ -225,7 +226,6 @@ const MAX_MATCH_TOKENS = 24;
|
|
|
225
226
|
/** A `recall` query's size and the most hits one answers (session-log item 10):
|
|
226
227
|
* a query is a few words, and the tool's default is five. */
|
|
227
228
|
const MAX_SEARCH_QUERY_BYTES = 1_024;
|
|
228
|
-
const MAX_SEARCH_HITS = 50;
|
|
229
229
|
/** Request body ceiling, checked against Content-Length before parsing. A full
|
|
230
230
|
* batch (50 × 4000-char texts + keywords + envelope) fits comfortably. */
|
|
231
231
|
const MAX_BODY_BYTES = 512 * 1024;
|
|
@@ -1152,31 +1152,16 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
1152
1152
|
value TEXT NOT NULL
|
|
1153
1153
|
);
|
|
1154
1154
|
`);
|
|
1155
|
-
|
|
1156
|
-
//
|
|
1157
|
-
//
|
|
1158
|
-
//
|
|
1159
|
-
// `channel_visibility IN`, `user_id =`), each ordered like the page.
|
|
1160
|
-
const columns = new Set(
|
|
1161
|
-
this.sql
|
|
1162
|
-
.exec<{ name: string }>(`PRAGMA table_info(runs)`)
|
|
1163
|
-
.toArray()
|
|
1164
|
-
.map((c) => c.name),
|
|
1165
|
-
);
|
|
1166
|
-
if (!columns.has("channel_visibility"))
|
|
1167
|
-
this.sql.exec(`ALTER TABLE runs ADD COLUMN channel_visibility TEXT NOT NULL DEFAULT 'unknown'`);
|
|
1168
|
-
// The session a run was a range of (session-log item 7), so the sweep can
|
|
1169
|
-
// tell which sessions still have a kept run; null for a record without one.
|
|
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`);
|
|
1155
|
+
this.migrateRunsTable();
|
|
1156
|
+
// The indexes the visibility predicate's leaves walk (`channel_id IN`,
|
|
1157
|
+
// `channel_visibility IN`, `user_id =`), each ordered like the page; the
|
|
1158
|
+
// session the sweep asks about; the parent a children listing filters on.
|
|
1175
1159
|
this.sql.exec(`
|
|
1176
1160
|
CREATE INDEX IF NOT EXISTS runs_channel_finished ON runs(channel_id, finished_at DESC, run_id DESC);
|
|
1177
1161
|
CREATE INDEX IF NOT EXISTS runs_visibility_finished ON runs(channel_visibility, finished_at DESC, run_id DESC);
|
|
1178
1162
|
CREATE INDEX IF NOT EXISTS runs_user_finished ON runs(user_id, finished_at DESC, run_id DESC);
|
|
1179
1163
|
CREATE INDEX IF NOT EXISTS runs_session ON runs(session_key);
|
|
1164
|
+
CREATE INDEX IF NOT EXISTS runs_parent ON runs(parent_run_id, finished_at DESC, run_id DESC);
|
|
1180
1165
|
`);
|
|
1181
1166
|
// The sessions registry (session-log item 7): every session log a run of
|
|
1182
1167
|
// this store claimed, with its thread — the sweep cannot enumerate the
|
|
@@ -1333,6 +1318,37 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
1333
1318
|
return r ? rowToLive(r) : undefined;
|
|
1334
1319
|
}
|
|
1335
1320
|
|
|
1321
|
+
/** The `runs` table's column migrations, run at every construction and
|
|
1322
|
+
* idempotent: a table created before a column existed gains it, with the
|
|
1323
|
+
* value a row written back then should read. The run-visibility stamp: a
|
|
1324
|
+
* run written before the stamp is `unknown`, never public. The session a
|
|
1325
|
+
* run was a range of (session-log item 7), so the sweep can tell which
|
|
1326
|
+
* sessions still have a kept run; null for a record without one. What the
|
|
1327
|
+
* run cost in tokens (costs.md, cost by user), NULL until the by-user
|
|
1328
|
+
* aggregate fills it from the run's stored events. The parent a child names
|
|
1329
|
+
* (run-history item 46), the column a children listing filters on: the
|
|
1330
|
+
* record already carries it in `summary_json`, so existing rows are filled
|
|
1331
|
+
* from there once, and every later `put` writes it beside the row. */
|
|
1332
|
+
migrateRunsTable(): void {
|
|
1333
|
+
const columns = new Set(
|
|
1334
|
+
this.sql
|
|
1335
|
+
.exec<{ name: string }>(`PRAGMA table_info(runs)`)
|
|
1336
|
+
.toArray()
|
|
1337
|
+
.map((c) => c.name),
|
|
1338
|
+
);
|
|
1339
|
+
if (!columns.has("channel_visibility"))
|
|
1340
|
+
this.sql.exec(`ALTER TABLE runs ADD COLUMN channel_visibility TEXT NOT NULL DEFAULT 'unknown'`);
|
|
1341
|
+
if (!columns.has("session_key")) this.sql.exec(`ALTER TABLE runs ADD COLUMN session_key TEXT`);
|
|
1342
|
+
if (!columns.has("usage_json")) this.sql.exec(`ALTER TABLE runs ADD COLUMN usage_json TEXT`);
|
|
1343
|
+
if (!columns.has("parent_run_id")) {
|
|
1344
|
+
this.sql.exec(`ALTER TABLE runs ADD COLUMN parent_run_id TEXT`);
|
|
1345
|
+
this.sql.exec(
|
|
1346
|
+
`UPDATE runs SET parent_run_id = json_extract(summary_json, '$.parentRunId')
|
|
1347
|
+
WHERE json_type(summary_json, '$.parentRunId') = 'text'`,
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1336
1352
|
private liveByThread(threadKey: string): LiveRunRow | undefined {
|
|
1337
1353
|
const r = this.sql.exec<LiveRow>(`SELECT * FROM live_runs WHERE thread_key = ?`, threadKey).toArray()[0];
|
|
1338
1354
|
return r ? rowToLive(r) : undefined;
|
|
@@ -1832,8 +1848,8 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
1832
1848
|
);
|
|
1833
1849
|
this.sql.exec(
|
|
1834
1850
|
`INSERT INTO runs (run_id, label, agent, model, channel_id, user_id, thread_key, channel_visibility, repo, started_at, finished_at, stored_at, status,
|
|
1835
|
-
event_count, stored_event_count, truncated, bytes, diagnosis_json, summary_json, session_key, usage_json)
|
|
1836
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1851
|
+
event_count, stored_event_count, truncated, bytes, diagnosis_json, summary_json, session_key, usage_json, parent_run_id)
|
|
1852
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1837
1853
|
ON CONFLICT(run_id) DO UPDATE SET
|
|
1838
1854
|
label = excluded.label, agent = excluded.agent, model = excluded.model, channel_id = excluded.channel_id,
|
|
1839
1855
|
user_id = excluded.user_id, thread_key = excluded.thread_key, channel_visibility = excluded.channel_visibility,
|
|
@@ -1842,7 +1858,8 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
1842
1858
|
event_count = excluded.event_count, stored_event_count = excluded.stored_event_count, truncated = excluded.truncated,
|
|
1843
1859
|
bytes = excluded.bytes, diagnosis_json = excluded.diagnosis_json, summary_json = excluded.summary_json,
|
|
1844
1860
|
session_key = excluded.session_key,
|
|
1845
|
-
usage_json = COALESCE(excluded.usage_json, runs.usage_json)
|
|
1861
|
+
usage_json = COALESCE(excluded.usage_json, runs.usage_json),
|
|
1862
|
+
parent_run_id = excluded.parent_run_id`,
|
|
1846
1863
|
stored.id,
|
|
1847
1864
|
stored.label ?? null,
|
|
1848
1865
|
stored.agent ?? null,
|
|
@@ -1864,6 +1881,7 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
1864
1881
|
JSON.stringify(summary),
|
|
1865
1882
|
stored.session?.key ?? null,
|
|
1866
1883
|
stored.usage ? JSON.stringify(stored.usage) : null,
|
|
1884
|
+
stored.parentRunId ?? null,
|
|
1867
1885
|
);
|
|
1868
1886
|
// The session's registry row learns its newest finish (session-log item
|
|
1869
1887
|
// 7); a record that reaches the store without a claim (the plain put
|
|
@@ -2199,6 +2217,10 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
2199
2217
|
where.push(`thread_key = ?`);
|
|
2200
2218
|
params.push(q.threadKey);
|
|
2201
2219
|
}
|
|
2220
|
+
if (q.parentRunId !== undefined) {
|
|
2221
|
+
where.push(`parent_run_id = ?`);
|
|
2222
|
+
params.push(q.parentRunId);
|
|
2223
|
+
}
|
|
2202
2224
|
if (q.visibleTo !== undefined && q.visibleTo.kind !== "all") where.push(visibilitySql(q.visibleTo, params));
|
|
2203
2225
|
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`;
|
|
2204
2226
|
// `LIMIT` holds on the over-bound path too: the kept set is the newest
|
|
@@ -2441,6 +2463,11 @@ function parseRunList(body: unknown): Validated<{ storeKey: string; query: RunLi
|
|
|
2441
2463
|
return invalid(`${field} must be a string of at most ${MAX_KEY_CHARS} characters`);
|
|
2442
2464
|
query[field] = v;
|
|
2443
2465
|
}
|
|
2466
|
+
if (b.parentRunId !== undefined) {
|
|
2467
|
+
const id = parseRunId(b.parentRunId);
|
|
2468
|
+
if (!id.ok) return invalid("parentRunId must match ^[A-Za-z0-9_-]{1,64}$");
|
|
2469
|
+
query.parentRunId = id.value;
|
|
2470
|
+
}
|
|
2444
2471
|
if (b.visibleTo !== undefined) {
|
|
2445
2472
|
// A malformed filter is a 400, never "all": the bot degrades to live rows
|
|
2446
2473
|
// rather than the DO widening what an actor may see.
|
|
@@ -2453,8 +2480,8 @@ function parseRunList(body: unknown): Validated<{ storeKey: string; query: RunLi
|
|
|
2453
2480
|
}
|
|
2454
2481
|
|
|
2455
2482
|
/** Parameters the page query binds before any filter: the cursor pair (3) and the age floor (1),
|
|
2456
|
-
* plus `agent`, `channel`, `threadKey`, and the LIMIT at most — the headroom `visibleTo` must fit under. */
|
|
2457
|
-
const RUN_LIST_BASE_PARAMETERS =
|
|
2483
|
+
* plus `agent`, `channel`, `threadKey`, `parentRunId`, and the LIMIT at most — the headroom `visibleTo` must fit under. */
|
|
2484
|
+
const RUN_LIST_BASE_PARAMETERS = 9;
|
|
2458
2485
|
|
|
2459
2486
|
/** How many `?` a filter binds (one per id, one per user). */
|
|
2460
2487
|
function boundParameters(f: RunVisibilityFilter): number {
|
|
@@ -3400,8 +3427,8 @@ async function handleLedger(pathname: string, body: unknown, env: Env): Promise<
|
|
|
3400
3427
|
)
|
|
3401
3428
|
return json({ error: `query must be a non-empty string of at most ${MAX_SEARCH_QUERY_BYTES} bytes` }, 400);
|
|
3402
3429
|
const limit = b.limit;
|
|
3403
|
-
if (typeof limit !== "number" || !Number.isInteger(limit) || limit < 1 || limit >
|
|
3404
|
-
return json({ error: `limit must be an integer in 1..${
|
|
3430
|
+
if (typeof limit !== "number" || !Number.isInteger(limit) || limit < 1 || limit > SEARCH_MAX_HITS)
|
|
3431
|
+
return json({ error: `limit must be an integer in 1..${SEARCH_MAX_HITS}` }, 400);
|
|
3405
3432
|
const hits = await stub.search(b.query, limit);
|
|
3406
3433
|
const gaps =
|
|
3407
3434
|
hits.length > 1
|
|
@@ -1110,7 +1110,9 @@ const NO_REF_HINT_REASON: RefHintReason = { ownPr: null, refByDefault: false };
|
|
|
1110
1110
|
type RebindOutcome =
|
|
1111
1111
|
| { kind: "none"; binding: ThreadBinding }
|
|
1112
1112
|
| { kind: "refuse"; refused: RebindRefused }
|
|
1113
|
-
|
|
1113
|
+
/** `note`: the move was the record alone — a dirty tree already on the
|
|
1114
|
+
* branch, no checkout run — in the verdict's words, for the log line. */
|
|
1115
|
+
| { kind: "rebound"; moved: ThreadBinding; rebound: Rebound; note?: string };
|
|
1114
1116
|
|
|
1115
1117
|
/** Result of one /op test/build execution. `ok` is the command's
|
|
1116
1118
|
* verdict — a failing test run is a RESULT with ok:false, never an error. */
|
|
@@ -4654,29 +4656,44 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4654
4656
|
);
|
|
4655
4657
|
tree.readable = status.exitCode === 0;
|
|
4656
4658
|
if (tree.readable) tree.dirty = status.stdout.trim() !== "";
|
|
4659
|
+
if (tree.dirty) {
|
|
4660
|
+
// A dirty tree is refused unless its HEAD is already the branch
|
|
4661
|
+
// (the run made it here and left an edit after pushing): the
|
|
4662
|
+
// one fact that tells the two apart, measured only when it
|
|
4663
|
+
// decides anything.
|
|
4664
|
+
const head = await this.threadRun(
|
|
4665
|
+
current.user,
|
|
4666
|
+
wt,
|
|
4667
|
+
"git rev-parse --abbrev-ref HEAD",
|
|
4668
|
+
DEFAULT_EXEC_TIMEOUT_MS,
|
|
4669
|
+
);
|
|
4670
|
+
if (head.exitCode === 0) tree.head = head.stdout.trim();
|
|
4671
|
+
}
|
|
4657
4672
|
}
|
|
4658
4673
|
// Judged on the re-read plan: it carries whether the branch is the
|
|
4659
4674
|
// thread's own, which the pre-lock plan lacks when it was a recreate.
|
|
4660
4675
|
const verdict = rebindVerdict(again, tree);
|
|
4661
4676
|
if (verdict.kind === "recreate") return this.recreateAtOwnBranch(current, again, fetchToken);
|
|
4662
4677
|
if (verdict.kind !== "rebind") return verdict;
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4678
|
+
if (verdict.checkout) {
|
|
4679
|
+
const startedAt = systemClock();
|
|
4680
|
+
const checkout = await this.threadRun(
|
|
4681
|
+
current.user,
|
|
4682
|
+
wt,
|
|
4683
|
+
`git checkout --quiet ${shellQuote(again.to)}`,
|
|
4684
|
+
DEFAULT_EXEC_TIMEOUT_MS,
|
|
4685
|
+
);
|
|
4686
|
+
this.stepTrace.getStore()?.record("rebind-checkout", {
|
|
4687
|
+
startedAt,
|
|
4688
|
+
endedAt: systemClock(),
|
|
4689
|
+
exitCode: checkout.exitCode,
|
|
4690
|
+
timedOut: checkout.timedOut,
|
|
4691
|
+
});
|
|
4692
|
+
if (checkout.exitCode !== 0) {
|
|
4693
|
+
const detail =
|
|
4694
|
+
checkout.stderr.trim().split("\n")[0]?.slice(0, 200) || `git checkout exited ${checkout.exitCode}`;
|
|
4695
|
+
return { kind: "refuse", refused: rebindRefused(again, "checkout-failed", detail) };
|
|
4696
|
+
}
|
|
4680
4697
|
}
|
|
4681
4698
|
const rebound: Rebound = {
|
|
4682
4699
|
from: current.ref,
|
|
@@ -4686,7 +4703,9 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4686
4703
|
};
|
|
4687
4704
|
const moved: ThreadBinding = { ...current, ref: again.to, rebound };
|
|
4688
4705
|
await this.ctx.storage.put(key, moved);
|
|
4689
|
-
return
|
|
4706
|
+
return verdict.checkout
|
|
4707
|
+
? { kind: "rebound", moved, rebound }
|
|
4708
|
+
: { kind: "rebound", moved, rebound, note: verdict.note };
|
|
4690
4709
|
}, ATTACH_MUTEX_WAIT_MS)
|
|
4691
4710
|
).value;
|
|
4692
4711
|
} catch (err) {
|
|
@@ -4705,7 +4724,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4705
4724
|
}
|
|
4706
4725
|
const { moved, rebound } = outcome;
|
|
4707
4726
|
console.log(
|
|
4708
|
-
`attach ${prior.threadKey}: rebound ${rebound.from} → ${rebound.to} (the thread's own pull request #${rebound.pr})`,
|
|
4727
|
+
`attach ${prior.threadKey}: rebound ${rebound.from} → ${rebound.to} (the thread's own pull request #${rebound.pr})${outcome.note ? ` — ${outcome.note}` : ""}`,
|
|
4709
4728
|
);
|
|
4710
4729
|
return { binding: moved, rebound };
|
|
4711
4730
|
}
|
|
@@ -5417,14 +5436,13 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
5417
5436
|
"deps-seed",
|
|
5418
5437
|
REFRESH_BUILD_TIMEOUT_MS,
|
|
5419
5438
|
);
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
}
|
|
5439
|
+
await this.swapMutableCaches(
|
|
5440
|
+
seedNm,
|
|
5441
|
+
`${scratch}/node_modules`,
|
|
5442
|
+
BUILD_USER,
|
|
5443
|
+
parsed.mutableListing,
|
|
5444
|
+
"deps-seed-swap",
|
|
5445
|
+
);
|
|
5428
5446
|
console.log(`deps: ${key.slice(0, 8)} seeded from ${seed.slice(0, 8)} (${parsed.mech})`);
|
|
5429
5447
|
}
|
|
5430
5448
|
// Unprivileged and token-free, in a tree only this attempt
|
|
@@ -5527,16 +5545,42 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
5527
5545
|
"deps-materialize",
|
|
5528
5546
|
REFRESH_BUILD_TIMEOUT_MS,
|
|
5529
5547
|
);
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5548
|
+
await this.swapMutableCaches(
|
|
5549
|
+
entryNodeModules ?? `${CHECKOUT_DIR}/node_modules`,
|
|
5550
|
+
`${tree}/node_modules`,
|
|
5551
|
+
user,
|
|
5552
|
+
parsed.mutableListing,
|
|
5553
|
+
"deps-mutable-swap",
|
|
5554
|
+
);
|
|
5555
|
+
return parsed.mech;
|
|
5556
|
+
}
|
|
5557
|
+
|
|
5558
|
+
/** The tool-cache swap for a freshly hardlinked node_modules: the listing
|
|
5559
|
+
* through `mutableCachePaths`, every swap in one fork
|
|
5560
|
+
* (`mutableCacheSwapScript`). A path the source has no counterpart for is
|
|
5561
|
+
* a tree-private entry the script leaves in place and names on a
|
|
5562
|
+
* `skipped=` line; the log carries those names so a refresh that read warm
|
|
5563
|
+
* still says what it left alone (the failure path already quotes the
|
|
5564
|
+
* script's stdout). */
|
|
5565
|
+
private async swapMutableCaches(
|
|
5566
|
+
srcNodeModules: string,
|
|
5567
|
+
dstNodeModules: string,
|
|
5568
|
+
user: string,
|
|
5569
|
+
mutableListing: string[],
|
|
5570
|
+
step: "deps-mutable-swap" | "deps-seed-swap",
|
|
5571
|
+
): Promise<void> {
|
|
5572
|
+
const paths = mutableCachePaths(dstNodeModules, mutableListing);
|
|
5573
|
+
if (paths.length === 0) return;
|
|
5574
|
+
const swapped = await this.runDepScript(
|
|
5575
|
+
mutableCacheSwapScript(srcNodeModules, dstNodeModules, user, paths),
|
|
5576
|
+
step,
|
|
5577
|
+
GIT_NETWORK_TIMEOUT_MS,
|
|
5578
|
+
);
|
|
5579
|
+
if (swapped.skipped.length > 0) {
|
|
5580
|
+
console.log(
|
|
5581
|
+
`deps: ${step} left ${swapped.skipped.length} tree-private node_modules entr${swapped.skipped.length === 1 ? "y" : "ies"} with no store counterpart in place: ${swapped.skipped.join(", ")}`,
|
|
5537
5582
|
);
|
|
5538
5583
|
}
|
|
5539
|
-
return parsed.mech;
|
|
5540
5584
|
}
|
|
5541
5585
|
|
|
5542
5586
|
/** Adopt a checkout that already holds node_modules but whose key has no
|