@coreplane/switchboard 1.249.0 → 1.250.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 +38 -5
- package/dist/assets/deploy/cloudflare-memory/worker.ts +229 -1
- package/dist/assets/deploy/cloudflare-resident/Dockerfile +12 -3
- package/dist/assets/deploy/cloudflare-resident/refresh.ts +5 -3
- package/dist/assets/deploy/cloudflare-resident/worker.ts +62 -4
- package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +7 -0
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +3 -2
- package/dist/assets/project.json +4 -0
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +1 -1
- package/dist/assets/src/core/authz/policy.ts +6 -0
- package/dist/assets/src/core/budgets.ts +13 -0
- package/dist/assets/src/core/coordinator/contract.ts +120 -0
- package/dist/assets/src/core/coordinator/driver.ts +1 -1
- package/dist/assets/src/core/prDescriptionTypes.ts +35 -0
- package/dist/assets/src/core/provider.ts +8 -3
- package/dist/assets/src/core/refusal.ts +11 -0
- package/dist/assets/src/core/runEvents.ts +49 -2
- package/dist/assets/src/core/runFriction.ts +2 -0
- package/dist/assets/src/core/runLedger/decisions.ts +9 -1
- package/dist/assets/src/core/runLedger/sessionLog.ts +10 -0
- package/dist/assets/src/core/runLedger/transcript.ts +13 -3
- package/dist/assets/src/core/runLedger/types.ts +60 -2
- package/dist/assets/src/core/runRecord.ts +18 -0
- package/dist/assets/src/core/ship/coordinator.ts +9 -1
- package/dist/assets/src/core/trace/attrs.ts +13 -2
- package/dist/assets/src/core/trace/workerTrace.ts +3 -2
- package/dist/assets/src/core/types.ts +3 -0
- package/dist/assets/src/execution/residentRefresh.ts +26 -1
- package/dist/assets/src/execution/sandboxErrors.ts +8 -0
- package/dist/assets/web/dist/.vite/manifest.json +30 -30
- package/dist/assets/web/dist/assets/DeliveryPage-CIfBiINK.js +1 -0
- package/dist/assets/web/dist/assets/{HomePage-BpQRky8B.js → HomePage-AnycA57D.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentDetailPage-BIUXyz6K.js → ResidentDetailPage-Cb3sFkfj.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-BZymgSAb.js → ResidentsIndexPage-BZ6n6UxF.js} +1 -1
- package/dist/assets/web/dist/assets/{RunFoldRow-3m4CPRI4.js → RunFoldRow-BRXkjkgO.js} +1 -1
- package/dist/assets/web/dist/assets/{RunRoutePage-bgkjkA0p.js → RunRoutePage-psSMI3fN.js} +3 -3
- package/dist/assets/web/dist/assets/{RunsIndexPage-8S944AzB.js → RunsIndexPage-68YT_RWt.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-8bBtG9y3.js → ScheduledPage-CBUxbeqN.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-DQeNvfaV.js → SettingsPage-CBTnZ9Qv.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-BPE5syBa.js → StatusDot-BnRjWzFN.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-DkoeZfTs.js → Tooltip-Brge0wnd.js} +1 -1
- package/dist/assets/web/dist/assets/{UnitRoutePage-BUzw--Ii.js → UnitRoutePage-o6sLju16.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-D11y9ZJ4.js → dist-rgAhsmE-.js} +1 -1
- package/dist/assets/web/dist/assets/{main-B6LcgNM6.js → main-CeRuGONy.js} +2 -2
- package/dist/cli.js +3535 -2408
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/DeliveryPage-DF4aQypG.js +0 -1
|
@@ -14,10 +14,9 @@ providers:
|
|
|
14
14
|
# OpenAI itself runs on its Responses API (record 0052): every first-party
|
|
15
15
|
# OpenAI card in pi's registry is `openai-responses`, OpenAI recommends the
|
|
16
16
|
# wire for new projects, and its chat route loses tool calling with any
|
|
17
|
-
# effort above `none` from GPT-5.4. The
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# loading as `wire: openai-chat` for one release.
|
|
17
|
+
# effort above `none` from GPT-5.4. The proxy serves the wire at
|
|
18
|
+
# `/v1/responses` — a run on this block is pinned and metered like any other.
|
|
19
|
+
# `type: openai-compatible` keeps loading as `wire: openai-chat` for one release.
|
|
21
20
|
openai:
|
|
22
21
|
wire: openai-responses
|
|
23
22
|
baseUrl: https://api.openai.com/v1
|
|
@@ -57,6 +56,10 @@ providers:
|
|
|
57
56
|
# inputs:
|
|
58
57
|
# image: true
|
|
59
58
|
# cache: automatic
|
|
59
|
+
# # The answer shapes the card takes — a forced tool call, the one-JSON-
|
|
60
|
+
# # object text contract, or both (absent assumes both). The intake gate
|
|
61
|
+
# # refuses at load a card declaring neither under its classify default.
|
|
62
|
+
# answers: ["tool", "text"]
|
|
60
63
|
# A passthrough is merged into the request body on the wires whose adapter
|
|
61
64
|
# takes one — a vendor-only feature such as OpenRouter's routing, never a
|
|
62
65
|
# control: it is not decided, not noted and not in the matrix, and a wrong
|
|
@@ -262,7 +265,7 @@ execution:
|
|
|
262
265
|
# baseUrl: https://switchboard-resident.<account>.workers.dev
|
|
263
266
|
# tokenEnv: RESIDENT_OPERATOR_TOKEN # operator bearer secret (default)
|
|
264
267
|
# adminTokenEnv: RESIDENT_ADMIN_TOKEN # admin bearer for `repo ...` commands (default)
|
|
265
|
-
# probeTimeoutMs:
|
|
268
|
+
# probeTimeoutMs: 8000 # /status probe deadline; a miss = not warm for that dispatch, never an outage
|
|
266
269
|
|
|
267
270
|
# Where per-conversation agent workspaces are created (local execution only).
|
|
268
271
|
# Under docker compose this directory is a named volume, so checkouts survive a restart.
|
|
@@ -445,6 +448,26 @@ workspaceDir: ./workspaces
|
|
|
445
448
|
# model: anthropic/claude-haiku-4-5
|
|
446
449
|
# answer: text
|
|
447
450
|
|
|
451
|
+
# The thread-reply intake gate (docs/decisions/0058-a-thread-reply-is-read-before-it-is-answered-intake-decides-whether-the-bot-was-addressed.md;
|
|
452
|
+
# docs/reference/specs/routing-and-config.md item 27). How an unmentioned reply
|
|
453
|
+
# in a channel thread the bot is part of is treated — NOTHING CALLS THE GATE
|
|
454
|
+
# YET; the Slack wiring lands in a later release, and until then behavior is
|
|
455
|
+
# unchanged whatever this block says. `threadReplies`: `classify` (the code's
|
|
456
|
+
# default — one cheap model turn decides whether the bot was addressed; a
|
|
457
|
+
# silent reply then gets no reaction, no card and no run), `mention` (only a
|
|
458
|
+
# mention is answered; an unmentioned reply is silent with a receipt and no
|
|
459
|
+
# model call) or `always` (today's answer-every-reply path, byte for byte).
|
|
460
|
+
# Also settable per channel and per user: `intake: { threadReplies: <mode> }`
|
|
461
|
+
# on a `channels.<id>` or `users.<id>` scope, user over channel over here.
|
|
462
|
+
# `model` is the verdict's model; default `routing.model`, else
|
|
463
|
+
# `defaults.models.general`. A mention, a DM and a top-level post never pass
|
|
464
|
+
# through the gate in any mode. A model whose operator card (`providers.<p>.
|
|
465
|
+
# models.<id>.answers`) declares it answers neither a forced tool call nor the
|
|
466
|
+
# text contract is refused at load when the default mode is `classify`.
|
|
467
|
+
# intake:
|
|
468
|
+
# threadReplies: classify
|
|
469
|
+
# model: anthropic/claude-haiku-4-5
|
|
470
|
+
|
|
448
471
|
# Linked threads (docs/decisions/0037-a-linked-thread-is-quoted-not-joined.md;
|
|
449
472
|
# docs/reference/specs/routing-and-config.md item 22). Off by default. On, a
|
|
450
473
|
# permalink in a request to a thread in another PUBLIC channel the bot is a
|
|
@@ -535,6 +558,16 @@ workspaceDir: ./workspaces
|
|
|
535
558
|
# catchUp:
|
|
536
559
|
# enabled: true
|
|
537
560
|
# windowMinutes: 30
|
|
561
|
+
# # The apps whose relay footer — `Sent by Claude in <#C…> on behalf of <@U…> ·
|
|
562
|
+
# # <permalink>`, appended by the Claude Slack app to a post it makes from a
|
|
563
|
+
# # Claude Code session — names the person the run is for and billed to
|
|
564
|
+
# # (docs/reference/specs/slack-channel.md item 13). By Slack bot id: the
|
|
565
|
+
# # `bot_id` on the app's messages, the `B…` a run record's `postedBy:
|
|
566
|
+
# # slack:bot:B…` carries. The footer is message text any app can write, so
|
|
567
|
+
# # it is read only from an app listed here; from any other app the app itself
|
|
568
|
+
# # is the requester, footer or not. Absent or empty: no footer is honoured.
|
|
569
|
+
# relayApps:
|
|
570
|
+
# - B0123ABCDE
|
|
538
571
|
|
|
539
572
|
# Where chat-set runtime overrides (`config set`, `config instructions`, `config clear`)
|
|
540
573
|
# persist (docs/reference/specs/routing-and-config.md item 12). Without this block they go to
|
|
@@ -71,25 +71,35 @@ import {
|
|
|
71
71
|
checkFence,
|
|
72
72
|
decideClaim,
|
|
73
73
|
decideClaimWrite,
|
|
74
|
+
decideIntakeInsert,
|
|
74
75
|
phaseTransition,
|
|
75
76
|
reclaimPhase,
|
|
76
77
|
selectReclaim,
|
|
77
78
|
} from "../../src/core/runLedger/decisions.ts";
|
|
79
|
+
import { intakeReceiptRetentionMs } from "../../src/core/budgets.ts";
|
|
78
80
|
import {
|
|
79
81
|
IDEMPOTENCY_KEY_PATTERN,
|
|
82
|
+
capThreadEvent,
|
|
80
83
|
INSTANCE_ID_PATTERN,
|
|
81
84
|
isCoordinatorInstance,
|
|
82
85
|
isCoordinatorUnit,
|
|
86
|
+
isThreadEvent,
|
|
83
87
|
sendRunFinished,
|
|
88
|
+
UNIT_PATTERN,
|
|
84
89
|
type CoordinatorInstance,
|
|
85
90
|
type CoordinatorUnit,
|
|
86
91
|
type RunFinishedSend,
|
|
92
|
+
type ThreadEvent,
|
|
87
93
|
} from "../../src/core/coordinator/contract.ts";
|
|
88
94
|
import {
|
|
89
95
|
GEN_PATTERN,
|
|
96
|
+
isIntakeReceipt,
|
|
90
97
|
type ClaimRequest,
|
|
91
98
|
type ClaimResult,
|
|
92
99
|
type FenceResult,
|
|
100
|
+
type IntakeQuery,
|
|
101
|
+
type IntakeReceipt,
|
|
102
|
+
type IntakeWriteResult,
|
|
93
103
|
type LivePhase,
|
|
94
104
|
type LiveRunRow,
|
|
95
105
|
type ReclaimedRun,
|
|
@@ -1515,6 +1525,22 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
1515
1525
|
PRIMARY KEY (run_id, kind)
|
|
1516
1526
|
);
|
|
1517
1527
|
`);
|
|
1528
|
+
// The intake receipts (run-history item 59): one verdict per message key,
|
|
1529
|
+
// first writer wins, beside the live rows because the reconnect catch-up
|
|
1530
|
+
// reads them through the same store key. `prune_after` is stamped at the
|
|
1531
|
+
// insert (the bound is the writer's window through
|
|
1532
|
+
// `intakeReceiptRetentionMs`) and the alarm sweeps by it.
|
|
1533
|
+
this.sql.exec(`
|
|
1534
|
+
CREATE TABLE IF NOT EXISTS intake_receipts (
|
|
1535
|
+
key TEXT PRIMARY KEY,
|
|
1536
|
+
thread_key TEXT NOT NULL,
|
|
1537
|
+
decided_at INTEGER NOT NULL,
|
|
1538
|
+
prune_after INTEGER NOT NULL,
|
|
1539
|
+
json TEXT NOT NULL
|
|
1540
|
+
);
|
|
1541
|
+
CREATE INDEX IF NOT EXISTS intake_thread ON intake_receipts(thread_key, decided_at);
|
|
1542
|
+
CREATE INDEX IF NOT EXISTS intake_prune ON intake_receipts(prune_after);
|
|
1543
|
+
`);
|
|
1518
1544
|
// The coordinator's parent records (run-history item 49): one row per
|
|
1519
1545
|
// instance, written by the bot at the instance's creation and read by the
|
|
1520
1546
|
// spawn route for the requester, channel and thread every child acts as.
|
|
@@ -1537,6 +1563,20 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
1537
1563
|
PRIMARY KEY (instance_id, unit)
|
|
1538
1564
|
);
|
|
1539
1565
|
`);
|
|
1566
|
+
// The thread events of a unit-owned thread (record 0051's reply-as-event rule): a
|
|
1567
|
+
// sibling table of the unit rows, never a field on them — `putUnits`
|
|
1568
|
+
// replaces a row whole, so an append landing between a route's read and
|
|
1569
|
+
// its put would be lost. Consumption is a column of its own, set once.
|
|
1570
|
+
this.sql.exec(`
|
|
1571
|
+
CREATE TABLE IF NOT EXISTS coordinator_unit_events (
|
|
1572
|
+
instance_id TEXT NOT NULL,
|
|
1573
|
+
unit TEXT NOT NULL,
|
|
1574
|
+
seq INTEGER NOT NULL,
|
|
1575
|
+
json TEXT NOT NULL,
|
|
1576
|
+
consumed_by TEXT,
|
|
1577
|
+
PRIMARY KEY (instance_id, unit, seq)
|
|
1578
|
+
);
|
|
1579
|
+
`);
|
|
1540
1580
|
}
|
|
1541
1581
|
|
|
1542
1582
|
// ---- the coordinator's parent records (run-history item 49) -----------------
|
|
@@ -1613,6 +1653,68 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
1613
1653
|
.map((r) => JSON.parse(r.json) as CoordinatorUnit);
|
|
1614
1654
|
}
|
|
1615
1655
|
|
|
1656
|
+
// ---- the thread events of a unit-owned thread (record 0051's reply-as-event rule) --------------
|
|
1657
|
+
|
|
1658
|
+
/** The next sequence assigned in one transaction, the per-event cap applied
|
|
1659
|
+
* (attachments dropped whole, the row saying how many). */
|
|
1660
|
+
async appendUnitEvent(
|
|
1661
|
+
instanceId: string,
|
|
1662
|
+
unit: string,
|
|
1663
|
+
event: Omit<ThreadEvent, "seq">,
|
|
1664
|
+
): Promise<{ ok: true; seq: number }> {
|
|
1665
|
+
let seq = 1;
|
|
1666
|
+
this.ctx.storage.transactionSync(() => {
|
|
1667
|
+
const max = this.sql
|
|
1668
|
+
.exec<{
|
|
1669
|
+
m: number | null;
|
|
1670
|
+
}>(`SELECT MAX(seq) AS m FROM coordinator_unit_events WHERE instance_id = ? AND unit = ?`, instanceId, unit)
|
|
1671
|
+
.toArray()[0];
|
|
1672
|
+
seq = (max?.m ?? 0) + 1;
|
|
1673
|
+
const capped = capThreadEvent({ ...event, seq });
|
|
1674
|
+
this.sql.exec(
|
|
1675
|
+
`INSERT INTO coordinator_unit_events (instance_id, unit, seq, json) VALUES (?, ?, ?, ?)`,
|
|
1676
|
+
instanceId,
|
|
1677
|
+
unit,
|
|
1678
|
+
seq,
|
|
1679
|
+
JSON.stringify(capped),
|
|
1680
|
+
);
|
|
1681
|
+
});
|
|
1682
|
+
return { ok: true, seq };
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
/** The unit's events in sequence order; `unconsumedOnly` filters to the rows nothing has consumed. */
|
|
1686
|
+
async listUnitEvents(instanceId: string, unit: string, unconsumedOnly: boolean): Promise<ThreadEvent[]> {
|
|
1687
|
+
return this.sql
|
|
1688
|
+
.exec<{ json: string; consumed_by: string | null }>(
|
|
1689
|
+
`SELECT json, consumed_by FROM coordinator_unit_events WHERE instance_id = ? AND unit = ?${
|
|
1690
|
+
unconsumedOnly ? " AND consumed_by IS NULL" : ""
|
|
1691
|
+
} ORDER BY seq ASC`,
|
|
1692
|
+
instanceId,
|
|
1693
|
+
unit,
|
|
1694
|
+
)
|
|
1695
|
+
.toArray()
|
|
1696
|
+
.map((r) => {
|
|
1697
|
+
const e = JSON.parse(r.json) as ThreadEvent;
|
|
1698
|
+
return r.consumed_by !== null ? { ...e, consumedBy: r.consumed_by } : e;
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
/** Consumption set once — idempotent: a row already consumed keeps its first consumer. */
|
|
1703
|
+
async markUnitEventsConsumed(instanceId: string, unit: string, seqs: number[], by: string): Promise<{ ok: true }> {
|
|
1704
|
+
this.ctx.storage.transactionSync(() => {
|
|
1705
|
+
for (const seq of seqs) {
|
|
1706
|
+
this.sql.exec(
|
|
1707
|
+
`UPDATE coordinator_unit_events SET consumed_by = ? WHERE instance_id = ? AND unit = ? AND seq = ? AND consumed_by IS NULL`,
|
|
1708
|
+
by,
|
|
1709
|
+
instanceId,
|
|
1710
|
+
unit,
|
|
1711
|
+
seq,
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1714
|
+
});
|
|
1715
|
+
return { ok: true };
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1616
1718
|
// ---- the live-run ledger (run-history items 28–34) --------------------------
|
|
1617
1719
|
|
|
1618
1720
|
private liveRow(runId: string): LiveRunRow | undefined {
|
|
@@ -1993,6 +2095,59 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
1993
2095
|
return parseEventRows(this.eventRows(runId, 0, Number.MAX_SAFE_INTEGER));
|
|
1994
2096
|
}
|
|
1995
2097
|
|
|
2098
|
+
// ---- the intake receipts (run-history item 59) -------------------------------
|
|
2099
|
+
|
|
2100
|
+
private intakeRow(key: string): IntakeReceipt | undefined {
|
|
2101
|
+
const r = this.sql.exec<{ json: string }>(`SELECT json FROM intake_receipts WHERE key = ?`, key).toArray()[0];
|
|
2102
|
+
return r ? (JSON.parse(r.json) as IntakeReceipt) : undefined;
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
/** Insert-if-absent inside one transaction: the first writer's row stands
|
|
2106
|
+
* and every caller acts on `stored` (`decideIntakeInsert`). `windowMs` is
|
|
2107
|
+
* the writer's reconnect catch-up window; the retention bound is stamped on
|
|
2108
|
+
* the row so the alarm's sweep is one indexed delete. */
|
|
2109
|
+
async recordIntake(key: string, receipt: IntakeReceipt, windowMs: number): Promise<IntakeWriteResult> {
|
|
2110
|
+
let out: IntakeWriteResult = { inserted: false, stored: receipt };
|
|
2111
|
+
this.ctx.storage.transactionSync(() => {
|
|
2112
|
+
out = decideIntakeInsert(this.intakeRow(key), receipt);
|
|
2113
|
+
if (!out.inserted) return;
|
|
2114
|
+
this.sql.exec(
|
|
2115
|
+
`INSERT INTO intake_receipts (key, thread_key, decided_at, prune_after, json) VALUES (?, ?, ?, ?, ?)`,
|
|
2116
|
+
key,
|
|
2117
|
+
receipt.threadKey,
|
|
2118
|
+
receipt.decidedAt,
|
|
2119
|
+
receipt.decidedAt + intakeReceiptRetentionMs(windowMs),
|
|
2120
|
+
JSON.stringify(receipt),
|
|
2121
|
+
);
|
|
2122
|
+
});
|
|
2123
|
+
if ((await this.ctx.storage.getAlarm()) === null)
|
|
2124
|
+
await this.ctx.storage.setAlarm(systemClock() + RUN_SWEEP_INTERVAL_MS);
|
|
2125
|
+
return out;
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
async readIntake(key: string): Promise<IntakeReceipt | null> {
|
|
2129
|
+
return this.intakeRow(key) ?? null;
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
/** A thread's receipts, or the receipts since an instant, oldest first. */
|
|
2133
|
+
async listIntake(query: IntakeQuery): Promise<IntakeReceipt[]> {
|
|
2134
|
+
const clauses: string[] = [];
|
|
2135
|
+
const params: (string | number)[] = [];
|
|
2136
|
+
if (query.threadKey !== undefined) {
|
|
2137
|
+
clauses.push("thread_key = ?");
|
|
2138
|
+
params.push(query.threadKey);
|
|
2139
|
+
}
|
|
2140
|
+
if (query.since !== undefined) {
|
|
2141
|
+
clauses.push("decided_at >= ?");
|
|
2142
|
+
params.push(query.since);
|
|
2143
|
+
}
|
|
2144
|
+
const where = clauses.length ? ` WHERE ${clauses.join(" AND ")}` : "";
|
|
2145
|
+
return this.sql
|
|
2146
|
+
.exec<{ json: string }>(`SELECT json FROM intake_receipts${where} ORDER BY decided_at ASC`, ...params)
|
|
2147
|
+
.toArray()
|
|
2148
|
+
.map((r) => JSON.parse(r.json) as IntakeReceipt);
|
|
2149
|
+
}
|
|
2150
|
+
|
|
1996
2151
|
// ---- policy ---------------------------------------------------------------
|
|
1997
2152
|
|
|
1998
2153
|
/** The persisted policy (defaults until the first proposal lands). */
|
|
@@ -2274,12 +2429,19 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
2274
2429
|
const now = systemClock();
|
|
2275
2430
|
const { policy } = this.policyState();
|
|
2276
2431
|
let deleted = 0;
|
|
2432
|
+
let receipts = 0;
|
|
2277
2433
|
let candidates: { key: string; threadKey: string }[] = [];
|
|
2278
2434
|
this.ctx.storage.transactionSync(() => {
|
|
2279
2435
|
deleted = this.trim(policy, now, undefined).deleted;
|
|
2280
2436
|
// Orphan sweep: events whose run is gone (defensive — `deleteRuns` pairs
|
|
2281
2437
|
// the two deletes, so this is a periodic check, not a per-put cost).
|
|
2282
2438
|
this.sql.exec(`DELETE FROM run_events WHERE run_id NOT IN (SELECT run_id FROM runs)`);
|
|
2439
|
+
// Intake receipts past their bound (item 59): each row carries its own
|
|
2440
|
+
// `prune_after`, stamped at the insert from the writer's window.
|
|
2441
|
+
receipts = this.sql
|
|
2442
|
+
.exec<{ n: number }>(`SELECT COUNT(*) AS n FROM intake_receipts WHERE prune_after <= ?`, now)
|
|
2443
|
+
.one().n;
|
|
2444
|
+
this.sql.exec(`DELETE FROM intake_receipts WHERE prune_after <= ?`, now);
|
|
2283
2445
|
// The sessions no kept run names any more (session-log item 7): decided
|
|
2284
2446
|
// here, on the rows this transaction leaves; dropped after it.
|
|
2285
2447
|
candidates = this.sql
|
|
@@ -2291,7 +2453,9 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
2291
2453
|
.map((r) => ({ key: r.key, threadKey: r.thread_key }));
|
|
2292
2454
|
});
|
|
2293
2455
|
const dropped = await this.sweepSessions(candidates);
|
|
2294
|
-
console.log(
|
|
2456
|
+
console.log(
|
|
2457
|
+
`[runs/alarm] swept ${deleted} rows outside policy, pruned ${receipts} intake receipt(s), dropped ${dropped} session log(s)`,
|
|
2458
|
+
);
|
|
2295
2459
|
await this.ctx.storage.setAlarm(now + RUN_SWEEP_INTERVAL_MS);
|
|
2296
2460
|
root.end("ok", { swept: deleted });
|
|
2297
2461
|
} catch (err) {
|
|
@@ -3538,6 +3702,9 @@ const LEDGER_ROUTES = new Set([
|
|
|
3538
3702
|
"/runs/coordinator/get",
|
|
3539
3703
|
"/runs/coordinator/units/put",
|
|
3540
3704
|
"/runs/coordinator/units/list",
|
|
3705
|
+
"/runs/coordinator/events/append",
|
|
3706
|
+
"/runs/coordinator/events/list",
|
|
3707
|
+
"/runs/coordinator/events/mark-consumed",
|
|
3541
3708
|
"/runs/claim",
|
|
3542
3709
|
"/runs/heartbeat",
|
|
3543
3710
|
"/runs/append",
|
|
@@ -3553,6 +3720,9 @@ const LEDGER_ROUTES = new Set([
|
|
|
3553
3720
|
"/runs/reclaim",
|
|
3554
3721
|
"/runs/live",
|
|
3555
3722
|
"/runs/live-events",
|
|
3723
|
+
"/runs/intake",
|
|
3724
|
+
"/runs/intake/read",
|
|
3725
|
+
"/runs/intake/list",
|
|
3556
3726
|
"/runs/transcript/owner",
|
|
3557
3727
|
"/runs/transcript/write",
|
|
3558
3728
|
"/runs/transcript/read",
|
|
@@ -3926,6 +4096,64 @@ async function handleLedger(pathname: string, body: unknown, env: Env): Promise<
|
|
|
3926
4096
|
return json({ error: "instanceId must be a Workflow instance id" }, 400);
|
|
3927
4097
|
return json({ units: await stub.listUnits(b.instanceId) });
|
|
3928
4098
|
}
|
|
4099
|
+
// The thread events of a unit-owned thread (record 0051's reply-as-event rule): append assigns
|
|
4100
|
+
// the sequence, list filters unconsumed, mark-consumed is idempotent.
|
|
4101
|
+
if (pathname.startsWith("/runs/coordinator/events/")) {
|
|
4102
|
+
if (typeof b.instanceId !== "string" || !INSTANCE_ID_PATTERN.test(b.instanceId))
|
|
4103
|
+
return json({ error: "instanceId must be a Workflow instance id" }, 400);
|
|
4104
|
+
if (typeof b.unit !== "string" || !UNIT_PATTERN.test(b.unit)) return json({ error: "unit must be a unit id" }, 400);
|
|
4105
|
+
if (pathname === "/runs/coordinator/events/append") {
|
|
4106
|
+
if (!isThreadEvent({ ...(b.event as Record<string, unknown>), seq: 1 }))
|
|
4107
|
+
return json({ error: "event must be a thread event (without its seq)" }, 400);
|
|
4108
|
+
// The store assigns the sequence and the consumer: a caller's `seq` or
|
|
4109
|
+
// `consumedBy` is dropped, so no row is born consumed in its JSON while
|
|
4110
|
+
// its column still lists it unconsumed.
|
|
4111
|
+
const { seq: _ignored, consumedBy: _fresh, ...event } = b.event as ThreadEvent;
|
|
4112
|
+
const r = await stub.appendUnitEvent(b.instanceId, b.unit, event as Omit<ThreadEvent, "seq" | "consumedBy">);
|
|
4113
|
+
console.log(`[runs/coordinator/events/append] ${key.value} ${b.instanceId}:${b.unit} seq ${r.seq}`);
|
|
4114
|
+
return json(r);
|
|
4115
|
+
}
|
|
4116
|
+
if (pathname === "/runs/coordinator/events/list") {
|
|
4117
|
+
return json({ events: await stub.listUnitEvents(b.instanceId, b.unit, b.unconsumedOnly === true) });
|
|
4118
|
+
}
|
|
4119
|
+
if (pathname === "/runs/coordinator/events/mark-consumed") {
|
|
4120
|
+
if (!Array.isArray(b.seqs) || !b.seqs.every((s) => typeof s === "number" && Number.isInteger(s) && s >= 1))
|
|
4121
|
+
return json({ error: "seqs must be an array of sequence numbers" }, 400);
|
|
4122
|
+
if (typeof b.by !== "string" || b.by.length === 0 || b.by.length > 200)
|
|
4123
|
+
return json({ error: "by must name the consumer" }, 400);
|
|
4124
|
+
const r = await stub.markUnitEventsConsumed(b.instanceId, b.unit, b.seqs as number[], b.by);
|
|
4125
|
+
console.log(
|
|
4126
|
+
`[runs/coordinator/events/mark-consumed] ${key.value} ${b.instanceId}:${b.unit} ${b.seqs.length} row(s) by ${b.by}`,
|
|
4127
|
+
);
|
|
4128
|
+
return json(r);
|
|
4129
|
+
}
|
|
4130
|
+
}
|
|
4131
|
+
|
|
4132
|
+
// The intake receipts (run-history item 59): keyed by the message, not a run.
|
|
4133
|
+
if (pathname === "/runs/intake" || pathname === "/runs/intake/read") {
|
|
4134
|
+
const receiptKey = b.key;
|
|
4135
|
+
if (typeof receiptKey !== "string" || receiptKey.length === 0 || receiptKey.length > 256)
|
|
4136
|
+
return json({ error: "key must be a non-empty string of at most 256 characters" }, 400);
|
|
4137
|
+
if (pathname === "/runs/intake/read") return json({ receipt: await stub.readIntake(receiptKey) });
|
|
4138
|
+
if (!isIntakeReceipt(b.receipt)) return json({ error: "receipt must be an intake receipt" }, 400);
|
|
4139
|
+
if (b.windowMs !== undefined && (typeof b.windowMs !== "number" || !Number.isFinite(b.windowMs) || b.windowMs < 0))
|
|
4140
|
+
return json({ error: "windowMs must be a non-negative number" }, 400);
|
|
4141
|
+
const r = await stub.recordIntake(receiptKey, b.receipt, typeof b.windowMs === "number" ? b.windowMs : 0);
|
|
4142
|
+
console.log(`[runs/intake] ${key.value} ${receiptKey} → ${r.inserted ? "inserted" : "existing"}`);
|
|
4143
|
+
return json(r);
|
|
4144
|
+
}
|
|
4145
|
+
if (pathname === "/runs/intake/list") {
|
|
4146
|
+
if (b.threadKey !== undefined && (typeof b.threadKey !== "string" || b.threadKey.length === 0))
|
|
4147
|
+
return json({ error: "threadKey must be a non-empty string" }, 400);
|
|
4148
|
+
if (b.since !== undefined && (typeof b.since !== "number" || !Number.isFinite(b.since)))
|
|
4149
|
+
return json({ error: "since must be a number" }, 400);
|
|
4150
|
+
return json({
|
|
4151
|
+
receipts: await stub.listIntake({
|
|
4152
|
+
...(b.threadKey !== undefined ? { threadKey: b.threadKey } : {}),
|
|
4153
|
+
...(b.since !== undefined ? { since: b.since } : {}),
|
|
4154
|
+
}),
|
|
4155
|
+
});
|
|
4156
|
+
}
|
|
3929
4157
|
|
|
3930
4158
|
const runId = parseRunId(b.runId);
|
|
3931
4159
|
if (!runId.ok) return json({ error: runId.error }, 400);
|
|
@@ -36,12 +36,21 @@ RUN git config --system user.name "switchboard-resident" \
|
|
|
36
36
|
# cores once several threads run). These defaults inherit into every thread's
|
|
37
37
|
# `su … -c` shell (su without `-` keeps the environment). CI=1 makes runners
|
|
38
38
|
# non-interactive (no watch mode, no TTY progress spinners in captured
|
|
39
|
-
# output).
|
|
40
|
-
#
|
|
39
|
+
# output). NODE_OPTIONS lets one Node process take 8 GiB of the 12 for its
|
|
40
|
+
# heap. V8's own default is a quarter of physical memory (~3 GiB here), and
|
|
41
|
+
# this image once pinned it lower still, at 1,536 MB — an eighth, meant as a
|
|
42
|
+
# fair share among the 16 seats — under which this repository's own two `tsc`
|
|
43
|
+
# passes die with "heap out of memory" (they pass at 3,072 MB with ~2 GB of
|
|
44
|
+
# RSS each; measured), and which is a quarter of the large-monorepo typecheck
|
|
45
|
+
# the instance size exists for (docs/reference/specs/execution.md item 16). A
|
|
46
|
+
# per-process heap limit never shared memory between processes (16 × 1,536 MB
|
|
47
|
+
# already exceeded the instance); the cgroup does that. So the cap is a
|
|
48
|
+
# runaway guard, sized for the largest legitimate command with room, and the
|
|
49
|
+
# same as the sandbox image's; src/deploy/imageNode.test.ts holds the two equal.
|
|
41
50
|
ENV CI=1 \
|
|
42
51
|
VITEST_MAX_WORKERS=1 VITEST_MAX_THREADS=1 VITEST_MAX_FORKS=1 VITEST_MIN_THREADS=1 VITEST_MIN_FORKS=1 \
|
|
43
52
|
UV_THREADPOOL_SIZE=4 \
|
|
44
|
-
NODE_OPTIONS=--max-old-space-size=
|
|
53
|
+
NODE_OPTIONS=--max-old-space-size=8192
|
|
45
54
|
|
|
46
55
|
# Node 24, not the base's. The 0.13.0-next base ships Node 22.23.2 like the
|
|
47
56
|
# 0.12.9 one (copied from node:22-slim: the binary at /usr/local/bin/node, npm
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from "../../src/execution/residentInstanceId.js";
|
|
22
22
|
import { RESTORE_MAX_MS, type RefreshPlan } from "../../src/execution/residentRefresh.js";
|
|
23
23
|
import { residentText } from "../../src/execution/residentText.js";
|
|
24
|
+
import { RUNTIME_BUSY_REASON } from "../../src/execution/sandboxErrors.js";
|
|
24
25
|
import { graftResidentSteps } from "../../src/execution/residentTrace.js";
|
|
25
26
|
import {
|
|
26
27
|
DEFAULT_EXEC_TIMEOUT_MS,
|
|
@@ -288,9 +289,10 @@ export class ResidentRefresh extends WorkflowEntrypoint<Env, RefreshInstancePara
|
|
|
288
289
|
}
|
|
289
290
|
// Housekeeping rides on every instance whatever the cycle's verdict — a
|
|
290
291
|
// parked resident still releases its idle bindings and re-measures, and
|
|
291
|
-
// neither step wakes a slept container — except an offboarded one
|
|
292
|
-
// nothing is left to keep
|
|
293
|
-
|
|
292
|
+
// neither step wakes a slept container — except an offboarded one
|
|
293
|
+
// (nothing is left to keep) and one whose container turned the cycle
|
|
294
|
+
// away (item 68: the sweep and the measure exec on that same container).
|
|
295
|
+
if (cycle.outcome !== "offboarded" && cycle.outcome !== RUNTIME_BUSY_REASON) {
|
|
294
296
|
const swept = await step.do("sweep", { retries, timeout: stepTimeoutMs(REFRESH_SWEEP_STEP_BUDGET_MS) }, () =>
|
|
295
297
|
stub.refreshInstanceSweep({ resource, instance }),
|
|
296
298
|
);
|
|
@@ -159,7 +159,11 @@ import {
|
|
|
159
159
|
} from "../../src/core/schedules.js";
|
|
160
160
|
import type { ResidentLifecycleState } from "../../src/execution/residentState.js";
|
|
161
161
|
import { RestoreWaiters } from "../../src/execution/restoreWaiters.js";
|
|
162
|
-
import {
|
|
162
|
+
import {
|
|
163
|
+
isRuntimeBusySignal,
|
|
164
|
+
RUNTIME_BUSY_REASON,
|
|
165
|
+
SandboxRuntimeBusyError,
|
|
166
|
+
} from "../../src/execution/sandboxErrors.js";
|
|
163
167
|
import {
|
|
164
168
|
decisivePull,
|
|
165
169
|
effectiveLimits,
|
|
@@ -613,6 +617,12 @@ const LIFECYCLE_KEY = "resident:lifecycle";
|
|
|
613
617
|
* resident, with the step it last reported and the cycle lease it holds, and
|
|
614
618
|
* the last bucket the cron skipped (a live cycle, a duplicate id). */
|
|
615
619
|
const REFRESH_INSTANCE_KEY = "resident:refreshInstance";
|
|
620
|
+
/** The settled state a cycle found before it wrote `refreshing` (item 68):
|
|
621
|
+
* what a cycle that yields to a busy container puts back. Rewritten by every
|
|
622
|
+
* cycle right before its `refreshing` write, so it always names the state
|
|
623
|
+
* under the current marker and nothing older. */
|
|
624
|
+
const REFRESHING_FROM_KEY = "resident:refreshingFrom";
|
|
625
|
+
type RefreshingFrom = ResidentStatus;
|
|
616
626
|
/** A `du` over a multi-GB checkout plus every live tree is seconds warm, tens
|
|
617
627
|
* of seconds on a cold page cache — the same class as a git network step. */
|
|
618
628
|
const DU_TIMEOUT_MS = GIT_NETWORK_TIMEOUT_MS;
|
|
@@ -3358,6 +3368,9 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3358
3368
|
}
|
|
3359
3369
|
}
|
|
3360
3370
|
|
|
3371
|
+
// Item 68: remember what `refreshing` covers, so a cycle the busy container
|
|
3372
|
+
// turns away can put it back — the last snapshot never stopped serving.
|
|
3373
|
+
await this.ctx.storage.put(REFRESHING_FROM_KEY, (await this.getStatus()) satisfies RefreshingFrom);
|
|
3361
3374
|
await this.setResidentState("refreshing");
|
|
3362
3375
|
let sha: string;
|
|
3363
3376
|
try {
|
|
@@ -3380,6 +3393,9 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3380
3393
|
// and die at git-setup). Name the disk instead: not
|
|
3381
3394
|
// serviceable, and the recovery below can free it.
|
|
3382
3395
|
const failure = await this.classifyFailure("fetch", message);
|
|
3396
|
+
// Item 68: the container did not accept the connect — a run's command
|
|
3397
|
+
// has its cores. Not GitHub, not the mirror: the instance step yields.
|
|
3398
|
+
if (failure.busy) throw err;
|
|
3383
3399
|
if (failure.diskFull) {
|
|
3384
3400
|
await this.setResidentState("degraded", failure.reason);
|
|
3385
3401
|
await this.recoverFromDiskFull(failure.reason, selfInFlight);
|
|
@@ -3533,7 +3549,9 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3533
3549
|
* error can surface between steps — with the generic "refresh" step, whose
|
|
3534
3550
|
* failure reason is the `refresh-failed: …` shape. A full disk is a third
|
|
3535
3551
|
* class: `disk-full: …`, never serviceable, and the one failure the
|
|
3536
|
-
* resident can act on itself (recoverFromDiskFull).
|
|
3552
|
+
* resident can act on itself (recoverFromDiskFull). A container that did
|
|
3553
|
+
* not accept the connect (`runtime-busy`, item 68) is a fourth: `busy`,
|
|
3554
|
+
* and the cycle yields to the run that holds it. */
|
|
3537
3555
|
private async classifyCycleError(err: unknown): Promise<RefreshFailure> {
|
|
3538
3556
|
// The control port never answered (item 64): the count decides, and a disk
|
|
3539
3557
|
// probe would only cost another 30 s abort against the same silent port.
|
|
@@ -3911,7 +3929,10 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3911
3929
|
* on purpose (`CycleRestartError`), is thrown to the engine, whose retry
|
|
3912
3930
|
* re-enters the same idempotent method — the row stays `refreshing`, never
|
|
3913
3931
|
* `degraded`, and a `refreshing` younger than the stale bound keeps the
|
|
3914
|
-
* cron from creating a second instance meanwhile. A
|
|
3932
|
+
* cron from creating a second instance meanwhile. A container that turned
|
|
3933
|
+
* the step's connect away (`runtime-busy`, item 68) ends the cycle as
|
|
3934
|
+
* `stopped` with nothing recorded — `yieldCycle` puts back the state the
|
|
3935
|
+
* cycle found. A failure of the repo's
|
|
3915
3936
|
* own is recorded — `degraded` with the reason, the last snapshot still
|
|
3916
3937
|
* serving — and answered `failed`, which ends the cycle; the next cron
|
|
3917
3938
|
* firing starts the next one from that state. */
|
|
@@ -3963,6 +3984,21 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3963
3984
|
return { ...result, startedAt, trace: trace.steps() };
|
|
3964
3985
|
}
|
|
3965
3986
|
const failure = await this.classifyCycleError(err);
|
|
3987
|
+
if (failure.busy) {
|
|
3988
|
+
// Item 68: the container did not accept the cycle's connect — a run's
|
|
3989
|
+
// command has its cores. Nothing ran and nothing about the repository
|
|
3990
|
+
// is known: the cycle yields, the state it found goes back, and no
|
|
3991
|
+
// failure is recorded — not `degraded`, not a rung of item 67's ladder
|
|
3992
|
+
// (three cycles of it used to destroy the container under the run).
|
|
3993
|
+
// The next cron firing tries again; the engine is not asked to retry
|
|
3994
|
+
// into the same busy container.
|
|
3995
|
+
outcome = `yielded (${failure.reason})`;
|
|
3996
|
+
console.log(
|
|
3997
|
+
`refresh instance ${instance}: ${step} yielded — ${failure.reason.slice(0, 400)}; the next cycle retries`,
|
|
3998
|
+
);
|
|
3999
|
+
await this.yieldCycle(instance);
|
|
4000
|
+
return { status: "stopped", why: RUNTIME_BUSY_REASON, startedAt, trace: trace.steps() };
|
|
4001
|
+
}
|
|
3966
4002
|
if (failure.interrupted) {
|
|
3967
4003
|
outcome = `interrupted (${failure.reason}) — the engine retries`;
|
|
3968
4004
|
console.log(`refresh instance ${instance}: ${step} interrupted — ${failure.reason.slice(0, 400)}; retrying`);
|
|
@@ -3984,6 +4020,27 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3984
4020
|
}
|
|
3985
4021
|
}
|
|
3986
4022
|
|
|
4023
|
+
/** A cycle that met the busy container ends here (item 68): the lease it
|
|
4024
|
+
* holds goes back, and the `refreshing` it wrote is undone to the settled
|
|
4025
|
+
* state it found, so the row says what the last snapshot still is — warm,
|
|
4026
|
+
* or the degraded an earlier cycle earned — never a `degraded` of this
|
|
4027
|
+
* cycle's own. A `refreshing` this cycle did not write (a step past the
|
|
4028
|
+
* fetch found the marker of a cycle that died mid-flight) is left to the
|
|
4029
|
+
* watchdog, which normalizes it. */
|
|
4030
|
+
private async yieldCycle(instance: string): Promise<void> {
|
|
4031
|
+
await this.clearInstanceLease(instance);
|
|
4032
|
+
const status = await this.getStatus();
|
|
4033
|
+
if (status.state !== "refreshing") return;
|
|
4034
|
+
const from = await this.ctx.storage.get<RefreshingFrom>(REFRESHING_FROM_KEY);
|
|
4035
|
+
if (!from || (from.state !== "warm" && from.state !== "degraded")) {
|
|
4036
|
+
console.log(
|
|
4037
|
+
`refresh instance ${instance}: yielded from \`refreshing\` over ${from?.state ?? "no recorded state"}; left for the watchdog`,
|
|
4038
|
+
);
|
|
4039
|
+
return;
|
|
4040
|
+
}
|
|
4041
|
+
await this.setResidentState(from.state, from.reason);
|
|
4042
|
+
}
|
|
4043
|
+
|
|
3987
4044
|
/** Step `fetch`: the gates, the cycle lease, the fetch and the plan. The
|
|
3988
4045
|
* instance id is the cycle `fetchMirror` records, so a retry of this step
|
|
3989
4046
|
* finds its fetch done. A rebuild's markers come off here, once per cycle,
|
|
@@ -4227,7 +4284,8 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4227
4284
|
* errno, so a full-disk attach reads like a lock bug without the probe. */
|
|
4228
4285
|
private async classifyFailure(step: string, message: string): Promise<RefreshFailure> {
|
|
4229
4286
|
const direct = classifyRefreshFailure({ step, message });
|
|
4230
|
-
|
|
4287
|
+
// A busy container (item 68) would only refuse the disk probe's exec too.
|
|
4288
|
+
if (direct.diskFull || direct.interrupted || direct.busy) return direct;
|
|
4231
4289
|
return classifyRefreshFailure({ step, message, freeKiB: await this.freeKiB() });
|
|
4232
4290
|
}
|
|
4233
4291
|
|
|
@@ -31,6 +31,13 @@ USER root
|
|
|
31
31
|
# (src/execution/bashTimeout.ts); changing it requires a container rebuild +
|
|
32
32
|
# redeploy.
|
|
33
33
|
ENV COMMAND_TIMEOUT_MS=1240000
|
|
34
|
+
# One Node process may take 8 GiB of the 12 for its heap. V8's own default is
|
|
35
|
+
# a quarter of physical memory (~3 GiB on this instance), which would refuse
|
|
36
|
+
# the very command the instance size was chosen for — a large monorepo's
|
|
37
|
+
# typecheck near 8 GB (docs/reference/specs/execution.md item 16) — while the
|
|
38
|
+
# box sat three-quarters empty. The same cap as the resident image's;
|
|
39
|
+
# src/deploy/imageNode.test.ts holds the two equal.
|
|
40
|
+
ENV NODE_OPTIONS=--max-old-space-size=8192
|
|
34
41
|
# docker.io + iptables: a Docker engine for runs whose tests need containers
|
|
35
42
|
# (act's default job mode, testcontainers, compose, image builds). Nothing
|
|
36
43
|
# starts the daemon at boot — the wrapper below does, on the first `docker`
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.250.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "switchboard",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.250.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.250.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.250.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",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"cli": "tsx src/cli.ts",
|
|
40
40
|
"verify": "npm run verify:root && npm run verify --workspaces --if-present && npm run check:site",
|
|
41
41
|
"verify:root": "npm run check:consistency && npm run typecheck && npm run lint && npm run format:check && npm test && npm run check:dist",
|
|
42
|
-
"check:consistency": "npm run check:lockfile && npm run check:sandbox-pair && npm run skills:check && npm run licenses:check && npm run docs:check && npm run pr-title:check && npm run specs:check && npm run decisions:check && npm run hygiene:check && npm run check:project-facts && npm run agents:check && npm run clock:check && npm run screenshots:check",
|
|
42
|
+
"check:consistency": "npm run check:lockfile && npm run check:sandbox-pair && npm run skills:check && npm run licenses:check && npm run docs:check && npm run pr-title:check && npm run specs:check && npm run decisions:check && npm run hygiene:check && npm run check:project-facts && npm run check:registry-drift && npm run agents:check && npm run clock:check && npm run screenshots:check",
|
|
43
43
|
"ci:gate": "node scripts/ci-gate.mjs",
|
|
44
44
|
"fix": "npm run docs:gen && npm run pr-title:gen && npm run agents:gen && npm run clock:gen && npm run hygiene:gen && npm run deploy:gen && npm run skills:sync && npm run lint:fix && npm run format",
|
|
45
45
|
"deploy:gen": "npm run --silent cli -- deploy init",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"pr-title:gen": "node scripts/pr-title-vocabulary.mjs --write",
|
|
51
51
|
"pr-title:check": "node scripts/pr-title-vocabulary.mjs",
|
|
52
52
|
"check:project-facts": "node scripts/check-project-facts.mjs",
|
|
53
|
+
"check:registry-drift": "tsx scripts/registry-drift.ts",
|
|
53
54
|
"agents:gen": "tsx scripts/agents-gen.ts",
|
|
54
55
|
"clock:gen": "tsx scripts/clock-allowlist.mts --write",
|
|
55
56
|
"clock:check": "tsx scripts/clock-allowlist.mts",
|
package/dist/assets/project.json
CHANGED
|
@@ -127,6 +127,10 @@
|
|
|
127
127
|
"does": "Every copy of the project's names, repository, docs URL and contact address equals `project.json`; its description, topics and npm package fit their rules.",
|
|
128
128
|
"when": "After editing `project.json` or a community file; part of `check:consistency`."
|
|
129
129
|
},
|
|
130
|
+
"check:registry-drift": {
|
|
131
|
+
"does": "Every example-config model ref still resolves against the pinned pi registry.",
|
|
132
|
+
"when": "After a pi bump; part of `check:consistency`."
|
|
133
|
+
},
|
|
130
134
|
"skills:sync": {
|
|
131
135
|
"does": "Vendors the skills listed in `skills/manifest.yaml`.",
|
|
132
136
|
"when": "After changing the manifest; part of `fix`."
|