@coreplane/switchboard 1.254.0 → 1.255.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/.dockerignore +3 -0
- package/dist/assets/Dockerfile +12 -1
- package/dist/assets/config/config.example.yaml +6 -1
- package/dist/assets/deploy/cloudflare/worker.ts +39 -23
- package/dist/assets/deploy/cloudflare-memory/worker.ts +552 -5
- package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +5 -0
- package/dist/assets/deploy/cloudflare-resident/Dockerfile +13 -1
- package/dist/assets/deploy/cloudflare-resident/levels.ts +84 -0
- package/dist/assets/deploy/cloudflare-resident/prepare-commit-msg +17 -0
- package/dist/assets/deploy/cloudflare-resident/worker.ts +202 -31
- package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +11 -1
- package/dist/assets/deploy/cloudflare-sandbox/prepare-commit-msg +17 -0
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +21 -4
- package/dist/assets/deploy/hooks/prepare-commit-msg +17 -0
- package/dist/assets/deploy/secrets.manifest.json +12 -0
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +21 -0
- package/dist/assets/src/core/budgets.ts +35 -2
- package/dist/assets/src/core/coordinator/contract.ts +6 -0
- package/dist/assets/src/core/coordinator/driver.ts +49 -6
- package/dist/assets/src/core/costs.ts +39 -16
- package/dist/assets/src/core/pipelineStanding.ts +5 -0
- package/dist/assets/src/core/plane/decide.ts +389 -22
- package/dist/assets/src/core/refusal.ts +3 -0
- package/dist/assets/src/core/reviewVerdict.ts +15 -2
- package/dist/assets/src/core/runEvents.ts +32 -13
- package/dist/assets/src/core/runLedger/sessionLog.ts +128 -0
- package/dist/assets/src/core/runLedger/types.ts +3 -0
- package/dist/assets/src/core/runRecord.ts +24 -7
- package/dist/assets/src/core/ship/coordinator.ts +327 -61
- package/dist/assets/src/core/trace/workerTrace.ts +3 -0
- package/dist/assets/src/execution/sandboxErrors.ts +77 -6
- package/dist/assets/web/dist/.vite/manifest.json +59 -58
- package/dist/assets/web/dist/assets/CostsPage-BaeWnm-o.js +1 -0
- package/dist/assets/web/dist/assets/{DeliveryPage-ngPsO2to.js → DeliveryPage-BBAyLwPq.js} +1 -1
- package/dist/assets/web/dist/assets/{HomePage-DvxTHzPx.js → HomePage-Be7jLLnU.js} +1 -1
- package/dist/assets/web/dist/assets/PendingTurnRow-DGINv9XT.js +1 -0
- package/dist/assets/web/dist/assets/{PlanePage-DpWfiX4C.js → PlanePage-JEj-lqgz.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentDetailPage-DG86v39Y.js → ResidentDetailPage-D_RD6wLo.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-x6p689VH.js → ResidentsIndexPage-BLkSuCxo.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-CfuZqf_O.js +1 -0
- package/dist/assets/web/dist/assets/{RunRoutePage-ysJBY8xQ.js → RunRoutePage-CmWYGR36.js} +4 -4
- package/dist/assets/web/dist/assets/{RunsIndexPage-CuzFchcn.js → RunsIndexPage-DW-HHuZa.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-BuLmfcbG.js → ScheduledPage-3aYsDf-q.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-BujWkdU_.js → SettingsPage-DG-p5Xy1.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-C8Bc0pTX.js → StatusDot-CEnGlyAL.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-BWwJx27K.js → Tooltip-CiunVowT.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-iOYnmTcQ.js +1 -0
- package/dist/assets/web/dist/assets/budgets-c1eumrqD.js +1 -0
- package/dist/assets/web/dist/assets/{dist-CpnyQGOb.js → dist-luhv3YSo.js} +1 -1
- package/dist/assets/web/dist/assets/indexRow-DborJPFp.js +1 -0
- package/dist/assets/web/dist/assets/{main-Comxmwi4.js → main-mAKx_zo9.js} +2 -2
- package/dist/assets/web/dist/assets/{sseReplay-IzTdD4-3.js → sseReplay-DE6wv1Ua.js} +6 -6
- package/dist/cli.js +4154 -3061
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/CostsPage-BuKjw3nv.js +0 -1
- package/dist/assets/web/dist/assets/PendingTurnRow-ZYIRCCZ2.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-DG29LOTs.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-B9kjA1AT.js +0 -1
- package/dist/assets/web/dist/assets/budgets-CbIyPAER.js +0 -1
- package/dist/assets/web/dist/assets/indexRow-DABQtONT.js +0 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.255.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "switchboard",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.255.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.255.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.255.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/source.json
CHANGED
|
@@ -47,6 +47,14 @@ export function machineNeedsRepo(machine: MachineClass): boolean {
|
|
|
47
47
|
export const IDENTITIES = ["none", "read", "write"] as const;
|
|
48
48
|
export type Identity = (typeof IDENTITIES)[number];
|
|
49
49
|
|
|
50
|
+
/** The model tiers a preset may run on (the one-door plan's tiers rule): the
|
|
51
|
+
* `fast` tier is the router's own model (`routing.model`), everything else is
|
|
52
|
+
* `strong`. Each preset declares its allowed set below (`AgentDef.tiers`);
|
|
53
|
+
* a parent choosing a child's model at spawn is held to the child preset's
|
|
54
|
+
* set, and escalation is a new run — a run's tier is fixed at dispatch. */
|
|
55
|
+
export const MODEL_TIERS = ["fast", "strong"] as const;
|
|
56
|
+
export type ModelTier = (typeof MODEL_TIERS)[number];
|
|
57
|
+
|
|
50
58
|
/** The wall clocks live in `src/core/budgets.ts` (docs/decisions/0046): a
|
|
51
59
|
* preset's ask, the turn cap derived from it and every allowance are rows
|
|
52
60
|
* there, and this registry reads them. Re-exported for the readers that
|
|
@@ -89,6 +97,12 @@ export interface AgentDef {
|
|
|
89
97
|
* worktree flag and the token the sandbox env and the `repo-cold` vet mint
|
|
90
98
|
* read this, through the run's effective profile. */
|
|
91
99
|
identity: Identity;
|
|
100
|
+
/** The model tiers this preset may run on (`MODEL_TIERS`): what a parent's
|
|
101
|
+
* spawn — and the operator's bind — may put in the child's request slot.
|
|
102
|
+
* A preset that writes code (`coding`, `ship`, `review`) never includes
|
|
103
|
+
* `fast`: a wrong approval or a wrong edit costs more than the tokens
|
|
104
|
+
* saved; `explore` and `research` read, and may run fast. */
|
|
105
|
+
tiers: readonly ModelTier[];
|
|
92
106
|
/** Whether the request router (docs/reference/specs/routing-and-config.md
|
|
93
107
|
* item 21) may pick this preset for a plain message. Absent means yes: the
|
|
94
108
|
* router's table is rendered from this registry. `false` keeps a preset
|
|
@@ -664,6 +678,7 @@ const WORK_PRESETS = {
|
|
|
664
678
|
identity: "none",
|
|
665
679
|
maxTokens: 16000,
|
|
666
680
|
...loopBudget("general"),
|
|
681
|
+
tiers: ["fast", "strong"],
|
|
667
682
|
},
|
|
668
683
|
coding: {
|
|
669
684
|
name: "coding",
|
|
@@ -678,6 +693,7 @@ const WORK_PRESETS = {
|
|
|
678
693
|
// `config set channel efforts.coding=…`, or `effort:` per request).
|
|
679
694
|
machine: "repo-resident",
|
|
680
695
|
identity: "write", // pushes branches and opens pull requests
|
|
696
|
+
tiers: ["strong"], // code-writing never runs fast (the one-door plan's tiers rule)
|
|
681
697
|
// Never routed: a plain write ask deserves the coding → review loop, so
|
|
682
698
|
// the router's table offers `ship` in coding's seat — a routed ship runs
|
|
683
699
|
// a generated one-unit plan whose merge is a person's, never the runner's.
|
|
@@ -693,6 +709,7 @@ const WORK_PRESETS = {
|
|
|
693
709
|
toolset: "readonly",
|
|
694
710
|
machine: "repo-resident",
|
|
695
711
|
identity: "read", // a read-scoped token and a read-only worktree: it cannot post or push from inside
|
|
712
|
+
tiers: ["strong"], // a wrong finding costs a merge decision: reviews never run fast
|
|
696
713
|
maxTokens: 64000,
|
|
697
714
|
...loopBudget("review"), // a safety net — typical reviews land in ~5 minutes
|
|
698
715
|
effort: "medium", // fast turns; one big-context pass does the deep work
|
|
@@ -717,6 +734,7 @@ const WORK_PRESETS = {
|
|
|
717
734
|
toolset: "full",
|
|
718
735
|
machine: "repo-resident",
|
|
719
736
|
identity: "write",
|
|
737
|
+
tiers: ["strong"], // the pipeline's children write and review code: never fast
|
|
720
738
|
// Routable: a routed ship runs a generated one-unit plan whose merge is a
|
|
721
739
|
// person's (`merge: person`) and never a seeded plan (the hand-off refuses
|
|
722
740
|
// a routed `plan <path>.md` naming `agent:ship`), so a wrong route costs a
|
|
@@ -733,6 +751,7 @@ const WORK_PRESETS = {
|
|
|
733
751
|
toolset: "web",
|
|
734
752
|
machine: "none", // web I/O only; no workspace is provisioned
|
|
735
753
|
identity: "none",
|
|
754
|
+
tiers: ["fast", "strong"], // reads and reports: may run fast
|
|
736
755
|
maxTokens: 24000,
|
|
737
756
|
...loopBudget("research"),
|
|
738
757
|
effort: "medium",
|
|
@@ -747,6 +766,7 @@ const WORK_PRESETS = {
|
|
|
747
766
|
// review depends on: a two-hour job shares no container with anyone.
|
|
748
767
|
machine: "repo-cold",
|
|
749
768
|
identity: "read", // a read-scoped token: it can clone and read, never push — whatever the caller holds
|
|
769
|
+
tiers: ["fast", "strong"], // reads and reports: may run fast
|
|
750
770
|
maxTokens: 64000,
|
|
751
771
|
...loopBudget("explore"),
|
|
752
772
|
// No built-in effort: the deployment decides, as for coding.
|
|
@@ -761,6 +781,7 @@ export const AGENTS: Record<string, AgentDef> = {
|
|
|
761
781
|
"Coordinates other runs: spawns child runs as the requester — each in a thread of its own, under their permissions — follows them, and reports. No workspace or shell.",
|
|
762
782
|
system: conductorSystem(Object.values(WORK_PRESETS)),
|
|
763
783
|
toolset: "conductor",
|
|
784
|
+
tiers: ["fast", "strong"], // reads and coordinates: may run fast
|
|
764
785
|
// Nothing is provisioned and no credential minted: the run tools call the
|
|
765
786
|
// dispatcher, the GitHub reads are REST in the bot process.
|
|
766
787
|
machine: "none",
|
|
@@ -46,6 +46,20 @@ export const CONFIRMATION_TTL_MS = 10 * MINUTE_MS;
|
|
|
46
46
|
* and only loses answers. People answer a question in minutes to hours. */
|
|
47
47
|
export const QUESTION_TTL_MS = DAY_MS;
|
|
48
48
|
|
|
49
|
+
/** How long a resolved author binding is reused before the stored `{ login,
|
|
50
|
+
* id }` pair is re-read from GitHub (docs/decisions/0062;
|
|
51
|
+
* docs/reference/specs/authorization.md item 18). The commit identity pairs
|
|
52
|
+
* are resolved per EXEC (`gitIdentityEnvs`), and the pi harness's log polls,
|
|
53
|
+
* FIFO sends and file writes ride the same executor — without a cache a
|
|
54
|
+
* write run costs one `GET /user/<id>` per command, drawing down the
|
|
55
|
+
* installation's shared rate limit for a freshness nothing needs: the
|
|
56
|
+
* identity rewrite re-reads the binding fresh before the PR opens, so a
|
|
57
|
+
* rename is caught there whatever this window holds. Read by `bindingOf`
|
|
58
|
+
* (`src/execution/authorBinding.ts`) for the resolved pair and the rename
|
|
59
|
+
* refusal alike (one `[identity]` line per window, not one per exec); a read
|
|
60
|
+
* that FAILED is never cached — the next read asks again. */
|
|
61
|
+
export const AUTHOR_BINDING_TTL_MS = 15 * MINUTE_MS;
|
|
62
|
+
|
|
49
63
|
/** How long a dispatch's FIRST attach to a resident — a fresh run's, or a
|
|
50
64
|
* resumed run's re-attach to its recorded worktree — waits for the resident
|
|
51
65
|
* to wake when the Worker typed its refusal as the platform's transient
|
|
@@ -172,6 +186,13 @@ export const FLOORS: Readonly<Record<RoundKind, number>> = {
|
|
|
172
186
|
* when the remainder allows it. */
|
|
173
187
|
export const MERGE_WAIT_ASK_MINUTES = 60;
|
|
174
188
|
|
|
189
|
+
/** The provider retry ladder (issue 1932): the backoff before each retry of a
|
|
190
|
+
* transient model-call failure — a gateway 5xx, a stream cut before
|
|
191
|
+
* `message_stop`, a gateway timeout. Three attempts with growing waits,
|
|
192
|
+
* charged to the run's lease; the waits stay small next to the run's minutes
|
|
193
|
+
* because the observed blips are edge transients of seconds. */
|
|
194
|
+
export const PROVIDER_RETRY_BACKOFFS_MS = [5_000, 15_000, 45_000] as const;
|
|
195
|
+
|
|
175
196
|
/** A hosted ship parent's deadline margin past the pipeline's wall clock
|
|
176
197
|
* (record 0060): the row's `state.hosting.until` is the hand-off time plus
|
|
177
198
|
* the instance's `caps.maxMinutes` plus this hour, absorbing the runner's own
|
|
@@ -183,8 +204,11 @@ export const HOSTED_DEADLINE_MARGIN_MINUTES = 60;
|
|
|
183
204
|
* and the pause before a busy spawn is asked again. */
|
|
184
205
|
export const SHIP_WAIT = { marginMinutes: 5, chunkMinutes: 5, mergeChunkMinutes: 5, busyRetryMinutes: 2 } as const;
|
|
185
206
|
|
|
186
|
-
/** The plane's table (docs/decisions/0064): how long a finished run stays on
|
|
187
|
-
|
|
207
|
+
/** The plane's table (docs/decisions/0064): how long a finished run stays on
|
|
208
|
+
* it, a reservation's window, and the default re-ask cadence — how often a
|
|
209
|
+
* silent resident something waits on is probed (`plane.reaskMinutes`
|
|
210
|
+
* overrides it per deployment). */
|
|
211
|
+
export const PLANE = { recentMinutes: 60, reservationMinutes: 2, reaskMinutes: 2 } as const;
|
|
188
212
|
|
|
189
213
|
/** The named amounts a lease holds back, in minutes. Each stands for a step
|
|
190
214
|
* every run or round pays: `provision` is attach and restore before the
|
|
@@ -473,6 +497,15 @@ export const DEFAULT_GRANT: Grant = { renewals: 0 };
|
|
|
473
497
|
* in one message before a plan should carry it. */
|
|
474
498
|
export const GRANT_RENEWALS_MAX = 12;
|
|
475
499
|
|
|
500
|
+
// ---- the structured-answer seam (record 0067: a violation is re-asked) ----
|
|
501
|
+
|
|
502
|
+
/** The most re-asks one structured ask may make (`askStructured`,
|
|
503
|
+
* src/core/dispatch/structured.ts): a named violation is quoted back to the
|
|
504
|
+
* same model at most this many times; the next violation is the caller's
|
|
505
|
+
* declared floor. A count, not a duration — the caller's one timeout covers
|
|
506
|
+
* the whole loop. */
|
|
507
|
+
export const STRUCTURED_RETRIES_MAX = 2;
|
|
508
|
+
|
|
476
509
|
// ---- the idle unit (record 0051: a pipeline idles instead of ending) ----
|
|
477
510
|
|
|
478
511
|
/** `ship.idleDays`' default: zero — today's endings byte for byte. It moves to
|
|
@@ -347,6 +347,11 @@ export interface CoordinatorInstance {
|
|
|
347
347
|
* attempt ended reruns the units not merged under `plan-<plan-id>-<attempt>`);
|
|
348
348
|
* absent for the first. */
|
|
349
349
|
attempt?: number;
|
|
350
|
+
/** The hard stop's mark (record 0060; issue 1924): written when the hosted
|
|
351
|
+
* parent is sealed, read by the runner before every unit start and before
|
|
352
|
+
* every child spawn — it honours the mark by ending the remaining units
|
|
353
|
+
* `stopped` and running nothing more. */
|
|
354
|
+
stop?: { at: number };
|
|
350
355
|
}
|
|
351
356
|
|
|
352
357
|
export interface UnitSegment {
|
|
@@ -491,6 +496,7 @@ export function isCoordinatorInstance(v: unknown): v is CoordinatorInstance {
|
|
|
491
496
|
if (!isOptionalText(r.runId) || !isOptionalText(r.label)) return false;
|
|
492
497
|
if (r.verbosity !== undefined && !isVerbosity(r.verbosity)) return false;
|
|
493
498
|
if (r.attempt !== undefined && !(Number.isInteger(r.attempt) && (r.attempt as number) >= 2)) return false;
|
|
499
|
+
if (r.stop !== undefined && !(isObject(r.stop) && isFinite(r.stop.at))) return false;
|
|
494
500
|
return true;
|
|
495
501
|
}
|
|
496
502
|
|
|
@@ -209,6 +209,9 @@ interface PlanFacts {
|
|
|
209
209
|
generated: boolean;
|
|
210
210
|
/** The runs page base the bot answered: the report links a child's write-up to its run page with it. */
|
|
211
211
|
runPageBase?: string;
|
|
212
|
+
/** The hard stop's mark (record 0060; issue 1924): the hosted parent was
|
|
213
|
+
* sealed, so the walk ends the remaining units stopped and runs nothing more. */
|
|
214
|
+
stopped: boolean;
|
|
212
215
|
repo: string;
|
|
213
216
|
base: string;
|
|
214
217
|
caps: ShipCaps;
|
|
@@ -263,6 +266,7 @@ function readPlan(a: BotAnswer): PlanFacts {
|
|
|
263
266
|
idleDays: readIdleDays(b.idleDays),
|
|
264
267
|
generated: b.generated === true,
|
|
265
268
|
...(typeof b.runPageBase === "string" && b.runPageBase.length > 0 ? { runPageBase: b.runPageBase } : {}),
|
|
269
|
+
stopped: b.stopped === true,
|
|
266
270
|
repo: b.repo,
|
|
267
271
|
base: b.base,
|
|
268
272
|
caps: { maxRounds: b.caps.maxRounds, maxMinutes: b.caps.maxMinutes },
|
|
@@ -288,6 +292,9 @@ function spawnReturn(step: string, a: BotAnswer): StepReturn {
|
|
|
288
292
|
return { type: "spawn", step, outcome: alreadySpawned === true ? "alreadySpawned" : "spawned", runId, at };
|
|
289
293
|
if (a.status === 409 && error === "busy")
|
|
290
294
|
return { type: "spawn", step, outcome: "busy", ...(typeof runId === "string" ? { runId } : {}), at };
|
|
295
|
+
// The hosted parent's hard stop (record 0060; issue 1924): the spawn is
|
|
296
|
+
// refused over the instance row's stop mark, and the unit ends stopped.
|
|
297
|
+
if (a.status === 409 && error === "stopped") return { type: "spawn", step, outcome: "stopped", at };
|
|
291
298
|
if (a.status === 403 && typeof error === "string")
|
|
292
299
|
return {
|
|
293
300
|
type: "spawn",
|
|
@@ -306,7 +313,10 @@ function readRecordReturn(step: string, a: BotAnswer): StepReturn {
|
|
|
306
313
|
const run = a.body.run;
|
|
307
314
|
if (a.body.ok !== true || !isRecord(run) || typeof run.finished !== "boolean")
|
|
308
315
|
throw new UnreadableAnswer("read-record", a, "run");
|
|
309
|
-
|
|
316
|
+
// The hard stop's mark, as the bot's answer carries it (record 0060; issue
|
|
317
|
+
// 1924): a finished child's unit ends stopped on it.
|
|
318
|
+
const stopped = a.body.stopped === true ? { stopped: true as const } : {};
|
|
319
|
+
if (!run.finished) return { type: "read-record", step, run: { finished: false }, ...stopped, at: a.body.at };
|
|
310
320
|
if (typeof run.status !== "string") throw new UnreadableAnswer("read-record", a, "status");
|
|
311
321
|
// The typed artifacts as the bot's record carries them — shape-checked where
|
|
312
322
|
// they were written (the run record's validator), read here as they are.
|
|
@@ -326,10 +336,12 @@ function readRecordReturn(step: string, a: BotAnswer): StepReturn {
|
|
|
326
336
|
leaseStartedAt,
|
|
327
337
|
costUsd,
|
|
328
338
|
handoffLists,
|
|
339
|
+
failure,
|
|
329
340
|
} = facts;
|
|
330
341
|
return {
|
|
331
342
|
type: "read-record",
|
|
332
343
|
step,
|
|
344
|
+
...stopped,
|
|
333
345
|
run: {
|
|
334
346
|
finished: true,
|
|
335
347
|
status: run.status as Extract<ChildFacts, { finished: true }>["status"],
|
|
@@ -348,6 +360,9 @@ function readRecordReturn(step: string, a: BotAnswer): StepReturn {
|
|
|
348
360
|
...(typeof leaseStartedAt === "number" ? { leaseStartedAt } : {}),
|
|
349
361
|
...(typeof costUsd === "number" || costUsd === null ? { costUsd } : {}),
|
|
350
362
|
...(handoffLists !== undefined ? { handoffLists } : {}),
|
|
363
|
+
// The failure by name (run-history item 57), shape-checked: a
|
|
364
|
+
// `provider_transient` drives the round-0 re-run (agent-ship item 9).
|
|
365
|
+
...(isRecord(failure) && typeof failure.kind === "string" ? { failure: { kind: failure.kind } } : {}),
|
|
351
366
|
},
|
|
352
367
|
at: a.body.at,
|
|
353
368
|
};
|
|
@@ -453,7 +468,11 @@ function mergeReturn(step: string, a: BotAnswer): StepReturn {
|
|
|
453
468
|
return { type: "merge", step, outcome: "merged", by: "other", sha, mergedAt, at };
|
|
454
469
|
if (ok === true && outcome === "merged" && typeof sha === "string")
|
|
455
470
|
return { type: "merge", step, outcome: "merged", sha, at };
|
|
456
|
-
if (
|
|
471
|
+
if (
|
|
472
|
+
ok === true &&
|
|
473
|
+
(outcome === "pending" || outcome === "refused" || outcome === "enqueued" || outcome === "removed") &&
|
|
474
|
+
typeof reason === "string"
|
|
475
|
+
)
|
|
457
476
|
return { type: "merge", step, outcome, reason, at };
|
|
458
477
|
throw new UnreadableAnswer("merge", a, "outcome");
|
|
459
478
|
}
|
|
@@ -642,7 +661,12 @@ async function perform(
|
|
|
642
661
|
answerOf(
|
|
643
662
|
"merge",
|
|
644
663
|
await step.do(action.step, STEP_CONFIG, () =>
|
|
645
|
-
call(bot, "merge", {
|
|
664
|
+
call(bot, "merge", {
|
|
665
|
+
...tag,
|
|
666
|
+
prNumber: action.prNumber,
|
|
667
|
+
headSha: action.headSha,
|
|
668
|
+
...(action.queued === true ? { queued: true } : {}),
|
|
669
|
+
}),
|
|
646
670
|
),
|
|
647
671
|
),
|
|
648
672
|
);
|
|
@@ -807,6 +831,11 @@ async function runUnit(
|
|
|
807
831
|
}
|
|
808
832
|
}
|
|
809
833
|
|
|
834
|
+
/** The report of a unit the hard stop ended before it ran (record 0060; issue 1924). */
|
|
835
|
+
function stoppedReport(unit: string): string {
|
|
836
|
+
return `⏹ Stopped: the pipeline's hosted parent was hard-stopped, so ${unit} was ended without running. Re-issue the plan naming the remaining units to run them.`;
|
|
837
|
+
}
|
|
838
|
+
|
|
810
839
|
function blockedReport(unit: string, dep: string, depEnding: string): string {
|
|
811
840
|
if (depEnding === "blocked")
|
|
812
841
|
return `⛔ Blocked: ${unit} waits on ${dep}, which is blocked itself. Re-issue the plan naming the remaining units once it is resolved.`;
|
|
@@ -877,6 +906,18 @@ async function walk(step: StepRunner, bot: CoordinatorBot, instanceId: string):
|
|
|
877
906
|
let cursor = openPlanCursor(graph);
|
|
878
907
|
const endings: Record<string, string> = {};
|
|
879
908
|
for (;;) {
|
|
909
|
+
// The hard stop's mark (record 0060; issue 1924), read before every unit
|
|
910
|
+
// start: the walk ends every unit not yet ended `stopped` — the rows say
|
|
911
|
+
// why they never ran — and starts nothing more. A stop that lands once
|
|
912
|
+
// every unit has ended changes nothing: there is nothing left to end.
|
|
913
|
+
if (plan.stopped) {
|
|
914
|
+
for (const id of cursor.order.filter((u) => endings[u] === undefined)) {
|
|
915
|
+
endings[id] = "stopped";
|
|
916
|
+
const body = { parentInstanceId: instanceId, unit: id, ending: { kind: "stopped", report: stoppedReport(id) } };
|
|
917
|
+
await step.do(`${id}/end`, STEP_CONFIG, () => call(bot, "unit-end", body));
|
|
918
|
+
}
|
|
919
|
+
break;
|
|
920
|
+
}
|
|
880
921
|
const [next] = readyUnits(graph, cursor);
|
|
881
922
|
if (next === undefined) break;
|
|
882
923
|
cursor = startUnit(graph, cursor, next);
|
|
@@ -914,8 +955,9 @@ async function walk(step: StepRunner, bot: CoordinatorBot, instanceId: string):
|
|
|
914
955
|
// Blocked units, in the plan's order: each told its own ending, so the rows
|
|
915
956
|
// and the summary say why it never ran. Every blocked unit's ending is known
|
|
916
957
|
// before any report is rendered — a plan may list a dependent before the
|
|
917
|
-
// dependency that blocks it.
|
|
918
|
-
|
|
958
|
+
// dependency that blocks it. A stopped walk skips this: every unended unit
|
|
959
|
+
// was already ended `stopped` above, and its cursor never finishes.
|
|
960
|
+
const blocked = plan.stopped ? [] : cursor.order.filter((id) => cursor.status[id] === "blocked");
|
|
919
961
|
for (const id of blocked) endings[id] = "blocked";
|
|
920
962
|
for (const id of blocked) {
|
|
921
963
|
const node = graph.units.find((u) => u.id === id)!;
|
|
@@ -927,7 +969,8 @@ async function walk(step: StepRunner, bot: CoordinatorBot, instanceId: string):
|
|
|
927
969
|
};
|
|
928
970
|
await step.do(`${id}/end`, STEP_CONFIG, () => call(bot, "unit-end", body));
|
|
929
971
|
}
|
|
930
|
-
if (!
|
|
972
|
+
if (!plan.stopped && !cursorFinished(cursor))
|
|
973
|
+
throw new Error(`the plan's cursor did not finish: ${JSON.stringify(cursor.status)}`);
|
|
931
974
|
return {
|
|
932
975
|
instance: instanceId,
|
|
933
976
|
...(plan.planId !== undefined ? { planId: plan.planId } : {}),
|
|
@@ -762,12 +762,19 @@ export interface BillerTieOutDay {
|
|
|
762
762
|
invoiceUsd?: number;
|
|
763
763
|
invoiceFeeUsd?: number;
|
|
764
764
|
invoiceByokUsd?: number;
|
|
765
|
-
/** The summed `model.turn` dollars whose ref names this block
|
|
765
|
+
/** The summed `model.turn` dollars whose ref names this block — the meter's
|
|
766
|
+
* charged figure from each turn's own `usd` (the provider's, the operator's
|
|
767
|
+
* or the registry's price at the time), never the price table's repricing
|
|
768
|
+
* the dimension views show (`buildCostsByReport`). */
|
|
766
769
|
attributedUsd: number;
|
|
767
770
|
/** The summed BYOK fees — the half an aggregator's `usage` ties against. */
|
|
768
771
|
attributedFeeUsd?: number;
|
|
769
772
|
/** `attributedUsd − attributedFeeUsd` — the half `byok_usage_inference` ties against. */
|
|
770
773
|
attributedUpstreamUsd?: number;
|
|
774
|
+
/** The day's tokens whose turns carried no `usd` (null or absent) — spend the
|
|
775
|
+
* attributed side could not count, so a gap against the invoice is explained
|
|
776
|
+
* rather than silent. Absent when every turn was priced. */
|
|
777
|
+
unpricedTokens?: number;
|
|
771
778
|
}
|
|
772
779
|
|
|
773
780
|
/** One biller's daily tie-out over the range. */
|
|
@@ -784,15 +791,20 @@ const billerOfRef = (ref: string): string | undefined => (ref.includes("/") ? pa
|
|
|
784
791
|
|
|
785
792
|
/**
|
|
786
793
|
* Pure: each biller's invoice compared with the summed `model.turn` rows whose
|
|
787
|
-
* ref names that block, per UTC day of the range.
|
|
788
|
-
*
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
794
|
+
* ref names that block, per UTC day of the range. The attributed side is the
|
|
795
|
+
* meter's charged figure — each turn's own `usd`, never the price table's
|
|
796
|
+
* repricing. An aggregator's invoice halves (`feeUsd`, `byokUsd`) sit beside
|
|
797
|
+
* the summed fees and the remainder. A model whose `usd` is null or absent
|
|
798
|
+
* (unpriced, or from before the meter row) contributes nothing to
|
|
799
|
+
* `attributedUsd` — its tokens are counted as the day's `unpricedTokens`
|
|
800
|
+
* instead, so the understated side is visible: honest, never invented — and
|
|
801
|
+
* a day the source returned no row for keeps its
|
|
792
802
|
* invoice side absent (the page renders —): the source holds closed days only,
|
|
793
803
|
* and a trailing uninvoiced day beside real spend is not a $0 invoice. A day
|
|
794
|
-
* with
|
|
795
|
-
*
|
|
804
|
+
* with no invoice row, no nonzero attributed figure and no unpriced tokens is
|
|
805
|
+
* dropped — but unpriced spend alone keeps its day, so the token count renders
|
|
806
|
+
* instead of vanishing; a biller without a source (`invoice: false`) ties out
|
|
807
|
+
* against nothing.
|
|
796
808
|
*/
|
|
797
809
|
export function buildBillerTieOuts(
|
|
798
810
|
billers: ReadonlyArray<{ name: string; invoice: boolean }>,
|
|
@@ -801,16 +813,22 @@ export function buildBillerTieOuts(
|
|
|
801
813
|
range: DateRange,
|
|
802
814
|
): BillerTieOut[] {
|
|
803
815
|
return billers.map(({ name, invoice }) => {
|
|
804
|
-
const attributed = new Map<string, { usd: number; fee: number; hasFee: boolean }>();
|
|
816
|
+
const attributed = new Map<string, { usd: number; fee: number; hasFee: boolean; unpricedTokens: number }>();
|
|
805
817
|
for (const cell of cells) {
|
|
806
818
|
if (cell.day < range.from || cell.day > range.to) continue;
|
|
807
819
|
for (const [ref, m] of Object.entries(cell.usage.byModel)) {
|
|
808
820
|
if (billerOfRef(ref) !== name) continue;
|
|
809
|
-
const day = attributed.get(cell.day) ?? { usd: 0, fee: 0, hasFee: false };
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
day.
|
|
821
|
+
const day = attributed.get(cell.day) ?? { usd: 0, fee: 0, hasFee: false, unpricedTokens: 0 };
|
|
822
|
+
// An unpriced model's feeUsd is skipped with its usd: adding the fee
|
|
823
|
+
// alone would drive attributedUpstreamUsd (usd − fee) negative.
|
|
824
|
+
if (typeof m.usd === "number") {
|
|
825
|
+
day.usd += m.usd;
|
|
826
|
+
if (m.feeUsd !== undefined) {
|
|
827
|
+
day.fee += m.feeUsd;
|
|
828
|
+
day.hasFee = true;
|
|
829
|
+
}
|
|
830
|
+
} else {
|
|
831
|
+
day.unpricedTokens += m.inputTokens + m.outputTokens + m.cacheReadTokens + m.cacheWriteTokens;
|
|
814
832
|
}
|
|
815
833
|
attributed.set(cell.day, day);
|
|
816
834
|
}
|
|
@@ -820,7 +838,7 @@ export function buildBillerTieOuts(
|
|
|
820
838
|
for (const d of invoices[name] ?? []) if (d.date >= range.from && d.date <= range.to) invoiced.set(d.date, d);
|
|
821
839
|
const dates = [...new Set([...attributed.keys(), ...invoiced.keys()])].sort().filter((date) => {
|
|
822
840
|
const a = attributed.get(date);
|
|
823
|
-
return invoiced.has(date) || (a !== undefined && (a.usd !== 0 || a.hasFee));
|
|
841
|
+
return invoiced.has(date) || (a !== undefined && (a.usd !== 0 || a.hasFee || a.unpricedTokens > 0));
|
|
824
842
|
});
|
|
825
843
|
const days: BillerTieOutDay[] = dates.map((date) => {
|
|
826
844
|
const a = attributed.get(date);
|
|
@@ -832,6 +850,7 @@ export function buildBillerTieOuts(
|
|
|
832
850
|
...(inv?.byokUsd !== undefined ? { invoiceByokUsd: inv.byokUsd } : {}),
|
|
833
851
|
attributedUsd: a?.usd ?? 0,
|
|
834
852
|
...(a?.hasFee ? { attributedFeeUsd: a.fee, attributedUpstreamUsd: a.usd - a.fee } : {}),
|
|
853
|
+
...(a !== undefined && a.unpricedTokens > 0 ? { unpricedTokens: a.unpricedTokens } : {}),
|
|
835
854
|
};
|
|
836
855
|
});
|
|
837
856
|
return {
|
|
@@ -922,7 +941,11 @@ export class OpenAICostsSource implements LlmInvoiceSource {
|
|
|
922
941
|
next_page?: string | null;
|
|
923
942
|
};
|
|
924
943
|
for (const bucket of body.data ?? []) {
|
|
925
|
-
|
|
944
|
+
// A bucket without a finite start_time is skipped, never dated to epoch 0;
|
|
945
|
+
// out-of-range buckets are dropped like the OpenRouter source's rows.
|
|
946
|
+
if (typeof bucket.start_time !== "number" || !Number.isFinite(bucket.start_time)) continue;
|
|
947
|
+
const date = new Date(bucket.start_time * 1000).toISOString().slice(0, 10);
|
|
948
|
+
if (date < range.from || date > range.to) continue;
|
|
926
949
|
for (const r of bucket.results ?? []) {
|
|
927
950
|
const currency = str(r.amount?.currency).toLowerCase();
|
|
928
951
|
if (currency !== "usd") throw new Error(`openai organization costs: unexpected currency ${currency || "?"}`);
|
|
@@ -94,6 +94,9 @@ export const ROUND_STAGE = {
|
|
|
94
94
|
request_changes: "fix",
|
|
95
95
|
no_verdict: "hold",
|
|
96
96
|
checks_failed: "fix",
|
|
97
|
+
transient: "hold",
|
|
98
|
+
enqueued: "approved",
|
|
99
|
+
dequeued: "fix",
|
|
97
100
|
aborted: "hold",
|
|
98
101
|
stopped: "hold",
|
|
99
102
|
continued: "continue",
|
|
@@ -111,12 +114,14 @@ export const ENDING_STAGE = {
|
|
|
111
114
|
merged: "merged",
|
|
112
115
|
already_landed: "merged",
|
|
113
116
|
merge_ready: "merge-ready",
|
|
117
|
+
held: "ended",
|
|
114
118
|
merge_refused: "ended",
|
|
115
119
|
round_cap: "ended",
|
|
116
120
|
wall_clock_cap: "ended",
|
|
117
121
|
review_pending: "ended",
|
|
118
122
|
stopped: "ended",
|
|
119
123
|
aborted: "ended",
|
|
124
|
+
transient: "ended",
|
|
120
125
|
continued: "hold",
|
|
121
126
|
no_verdict: "ended",
|
|
122
127
|
interrupted: "ended",
|