omp-conductor 0.18.2 → 0.19.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/README.md +105 -40
- package/REFERENCE.md +865 -30
- package/package.json +1 -1
- package/schema/config.schema.json +26 -0
- package/src/admission.ts +212 -26
- package/src/ask.ts +288 -1
- package/src/briefs/orchestrator.md +6 -5
- package/src/cli.ts +5 -1
- package/src/command-help.ts +9 -1
- package/src/command-manifest.ts +36 -3
- package/src/commands/arm.ts +5 -1
- package/src/commands/context.ts +2 -0
- package/src/commands/message.ts +26 -2
- package/src/commands/reconcile-units.ts +104 -0
- package/src/commands/release-composition.ts +232 -0
- package/src/commands/resume.ts +2 -27
- package/src/commands/setup.ts +101 -16
- package/src/commands/stats.ts +11 -30
- package/src/commands/tail.ts +31 -1
- package/src/commands/upgrade.ts +20 -3
- package/src/commands/verb.ts +2 -1
- package/src/config-schema.ts +19 -0
- package/src/config.ts +80 -0
- package/src/credential-class.ts +366 -0
- package/src/daemon.ts +1218 -288
- package/src/dashboard/app.js +504 -2
- package/src/dashboard/controls.ts +336 -0
- package/src/dashboard/index.html +30 -0
- package/src/dashboard/server.ts +271 -30
- package/src/dashboard/style.css +116 -0
- package/src/dashboard/transcript.ts +173 -0
- package/src/doctor.ts +377 -20
- package/src/failure-class.ts +59 -0
- package/src/fleet.ts +497 -15
- package/src/host.ts +6 -130
- package/src/omp.ts +29 -0
- package/src/orchestrator-tick.ts +343 -88
- package/src/pause.ts +233 -0
- package/src/settlement.ts +159 -2
- package/src/setup-answers.ts +97 -0
- package/src/setup-host.ts +321 -1155
- package/src/setup-install.ts +204 -27
- package/src/setup-wizard.ts +111 -50
- package/src/setup.ts +33 -0
- package/src/spend-telemetry.ts +117 -0
- package/src/stats.ts +35 -0
- package/src/status-render.ts +348 -19
- package/src/store.ts +1229 -55
- package/src/telegram-freshness.ts +269 -0
- package/src/to-spec.ts +27 -0
- package/src/types.ts +697 -4
- package/src/unblock.ts +22 -0
- package/src/unit-reconcile.ts +303 -0
- package/src/upgrade-verify.ts +8 -1
- package/src/upgrade.ts +299 -12
- package/src/verbs/actions.ts +124 -10
- package/src/verbs/protocol.ts +70 -2
- package/src/verbs/server.ts +447 -8
- package/src/wake.ts +48 -0
- package/src/worker.ts +403 -3
package/src/setup.ts
CHANGED
|
@@ -174,6 +174,12 @@ export interface SetupAnswers {
|
|
|
174
174
|
*/
|
|
175
175
|
modelFallbacks?: string[];
|
|
176
176
|
modelFallbackThreshold?: number;
|
|
177
|
+
/**
|
|
178
|
+
* The one stronger tier a spinning cap retries on (#807). Hand-edited like
|
|
179
|
+
* {@link modelFallbacks}, so it exists here only to survive an amend of some
|
|
180
|
+
* other area (#369).
|
|
181
|
+
*/
|
|
182
|
+
workerEscalationModel?: string;
|
|
177
183
|
/**
|
|
178
184
|
* The project's omp settings overlay (#537): an opaque map layered into
|
|
179
185
|
* every worker session via the fleet-owned settings channel. The wizard asks
|
|
@@ -241,6 +247,15 @@ export interface SetupAnswers {
|
|
|
241
247
|
* stale-base interlock from a project that relies on it (#428).
|
|
242
248
|
*/
|
|
243
249
|
criticalBase?: ProjectConfig["criticalBase"];
|
|
250
|
+
/**
|
|
251
|
+
* Hand-edited providers that must bill to a subscription credential, carried
|
|
252
|
+
* through setup unchanged like {@link criticalBase} (#852). The wizard never
|
|
253
|
+
* invents one — which providers a fleet pays for by subscription is the
|
|
254
|
+
* operator's billing arrangement, not something repo reading can answer — and
|
|
255
|
+
* dropping it during an unrelated amend would silently remove the fence that
|
|
256
|
+
* stops a disabled OAuth grant billing to an API key.
|
|
257
|
+
*/
|
|
258
|
+
requireOauthProviders?: ProjectConfig["requireOauthProviders"];
|
|
244
259
|
/**
|
|
245
260
|
* Whether to render `ORCHESTRATOR.md` into the project's workspace root. Not
|
|
246
261
|
* part of the config — the brief is the operator's file, and the conductor
|
|
@@ -1001,6 +1016,12 @@ export function buildProject(a: SetupAnswers): ProjectConfig {
|
|
|
1001
1016
|
// would pin every run of the project onto a dead provider again (#286).
|
|
1002
1017
|
...(a.modelFallbacks === undefined ? {} : { modelFallbacks: [...a.modelFallbacks] }),
|
|
1003
1018
|
...(a.modelFallbackThreshold === undefined ? {} : { modelFallbackThreshold: a.modelFallbackThreshold }),
|
|
1019
|
+
// Hand-edited like the failover chain, and the same reason to keep it: an
|
|
1020
|
+
// unrelated amend that dropped it would silently take the fleet's only
|
|
1021
|
+
// spinning-cap recovery away (#807).
|
|
1022
|
+
...(a.workerEscalationModel === undefined
|
|
1023
|
+
? {}
|
|
1024
|
+
: { workerEscalationModel: a.workerEscalationModel }),
|
|
1004
1025
|
// The omp settings overlay is an opaque map the wizard collects as free-form
|
|
1005
1026
|
// YAML; an unrelated amend must not delete it (#537).
|
|
1006
1027
|
...(a.ompSettings === undefined ? {} : { ompSettings: a.ompSettings }),
|
|
@@ -1026,6 +1047,9 @@ export function buildProject(a: SetupAnswers): ProjectConfig {
|
|
|
1026
1047
|
// Hand-edited safety markers carried unchanged on an unrelated amend
|
|
1027
1048
|
// (#428): dropping them would silently disarm the stale-base interlock.
|
|
1028
1049
|
...(a.criticalBase === undefined ? {} : { criticalBase: [...a.criticalBase] }),
|
|
1050
|
+
...(a.requireOauthProviders === undefined
|
|
1051
|
+
? {}
|
|
1052
|
+
: { requireOauthProviders: [...a.requireOauthProviders] }),
|
|
1029
1053
|
// Written out even when it is the default, so an operator amending the
|
|
1030
1054
|
// volume has a line in the file to point at. Opting into availability makes
|
|
1031
1055
|
// the schedule explicit and daily; omitting it preserves the preset's
|
|
@@ -1232,6 +1256,7 @@ export function answersFromProject(p: ProjectConfig): SetupAnswers {
|
|
|
1232
1256
|
if (p.groomBelow !== undefined) answers.groomBelow = p.groomBelow;
|
|
1233
1257
|
if (p.modelFallbacks !== undefined) answers.modelFallbacks = [...p.modelFallbacks];
|
|
1234
1258
|
if (p.modelFallbackThreshold !== undefined) answers.modelFallbackThreshold = p.modelFallbackThreshold;
|
|
1259
|
+
if (p.workerEscalationModel !== undefined) answers.workerEscalationModel = p.workerEscalationModel;
|
|
1235
1260
|
if (p.ompSettings !== undefined) answers.ompSettings = { ...p.ompSettings };
|
|
1236
1261
|
if (p.escalation.telegramChatId !== undefined) answers.telegramChatId = p.escalation.telegramChatId;
|
|
1237
1262
|
if (p.escalation.telegramTopicId !== undefined) answers.telegramTopicId = p.escalation.telegramTopicId;
|
|
@@ -1239,6 +1264,9 @@ export function answersFromProject(p: ProjectConfig): SetupAnswers {
|
|
|
1239
1264
|
answers.recoveryMerges = p.recoveryMerges.map((entry) => ({ ...entry }));
|
|
1240
1265
|
}
|
|
1241
1266
|
if (p.criticalBase !== undefined) answers.criticalBase = [...p.criticalBase];
|
|
1267
|
+
if (p.requireOauthProviders !== undefined) {
|
|
1268
|
+
answers.requireOauthProviders = [...p.requireOauthProviders];
|
|
1269
|
+
}
|
|
1242
1270
|
if (p.reporting?.digest.at !== undefined) answers.dailyDigestAt = p.reporting.digest.at;
|
|
1243
1271
|
if (p.reporting?.digest.timezone !== undefined) {
|
|
1244
1272
|
answers.reportingTimezone = p.reporting.digest.timezone;
|
|
@@ -1776,6 +1804,11 @@ export function summarisePlan(
|
|
|
1776
1804
|
if (a.workerModel !== undefined && a.workerModel.trim().length > 0) {
|
|
1777
1805
|
lines.push(` ${"worker model".padEnd(CAPS_PLAN_KEY_PAD)}${a.workerModel.trim()} (answered)`);
|
|
1778
1806
|
}
|
|
1807
|
+
if (a.workerEscalationModel !== undefined && a.workerEscalationModel.trim().length > 0) {
|
|
1808
|
+
lines.push(
|
|
1809
|
+
` ${"cap escalation".padEnd(CAPS_PLAN_KEY_PAD)}${a.workerEscalationModel.trim()} (hand-edited)`,
|
|
1810
|
+
);
|
|
1811
|
+
}
|
|
1779
1812
|
|
|
1780
1813
|
lines.push("", "escalation");
|
|
1781
1814
|
if (a.telegramChatId !== undefined && a.telegramChatId.trim().length > 0) {
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is the daily spend cap's own input still arriving? (#970)
|
|
3
|
+
*
|
|
4
|
+
* The cap compares `store.spendSince(project, startOfToday())` — a sum of the
|
|
5
|
+
* per-run `spendUsd` column — against its ceiling. When the harness stops
|
|
6
|
+
* reporting cost, that column reads 0.00 and the cap silently measures a
|
|
7
|
+
* fraction of reality. #46 named this once already: **$0.00 spend is not proof
|
|
8
|
+
* of no spend.**
|
|
9
|
+
*
|
|
10
|
+
* Measured on this fleet, counting only runs that actually did work:
|
|
11
|
+
*
|
|
12
|
+
* | day | worked | reported $0.00 |
|
|
13
|
+
* |------------|--------|----------------|
|
|
14
|
+
* | 2026-08-15 | 53 | 3.8% |
|
|
15
|
+
* | … | | 0–2.5% |
|
|
16
|
+
* | 2026-08-20 | 15 | 0.0% |
|
|
17
|
+
* | 2026-08-21 | 22 | 31.8% |
|
|
18
|
+
* | 2026-08-22 | 12 | 75.0% |
|
|
19
|
+
*
|
|
20
|
+
* Six days of ≤3.8% baseline, then 32%, then 75% — with #614 running 213 turns
|
|
21
|
+
* for $0.00.
|
|
22
|
+
*
|
|
23
|
+
* Two properties are load-bearing, and both were wrong in the predicate this
|
|
24
|
+
* replaces:
|
|
25
|
+
*
|
|
26
|
+
* - **Only runs that did work are judged.** A run killed before its first turn
|
|
27
|
+
* records $0.00 honestly; 26 of this project's 48 zero-spend rows are exactly
|
|
28
|
+
* that. Counting them inflates the ratio *and* lets a run of administrative
|
|
29
|
+
* kills fire a telemetry finding that says nothing about telemetry.
|
|
30
|
+
* - **A ratio, not `every`.** The old check fired only when every sampled run
|
|
31
|
+
* reported zero. Replayed over all 410 windows in this fleet's history it
|
|
32
|
+
* would have fired 14 times while staying silent through 37 windows that had
|
|
33
|
+
* lost a majority of their telemetry — so the common case, partial loss, was
|
|
34
|
+
* the invisible one. On 2026-08-21, at 31.8% loss, every window that day
|
|
35
|
+
* contained one metered run and the probe was silent all day.
|
|
36
|
+
*
|
|
37
|
+
* One judgement, two consumers: `doctor`'s finding and the `status` row. The
|
|
38
|
+
* cap's figure and the statement about whether that figure means anything must
|
|
39
|
+
* never come from two implementations that can disagree.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/** One sampled run, reduced to what the judgement needs. */
|
|
43
|
+
export interface SpendSample {
|
|
44
|
+
/** Turns taken. Zero means the run never worked, so its $0.00 is honest. */
|
|
45
|
+
turns: number;
|
|
46
|
+
spendUsd: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The share of *working* runs whose spend is missing at or above which this is
|
|
51
|
+
* reported.
|
|
52
|
+
*
|
|
53
|
+
* A majority, deliberately — the observed healthy baseline is 0–3.8%, not zero.
|
|
54
|
+
* A legitimately free or aborted-after-one-turn run happens, and a check that
|
|
55
|
+
* fires on one of them is a check that gets ignored, which is the failure the
|
|
56
|
+
* install-surface rows already name: a warning repeated every fifteen minutes
|
|
57
|
+
* trains an operator to ignore the row that matters.
|
|
58
|
+
*/
|
|
59
|
+
export const SPEND_MISSING_SHARE = 0.5;
|
|
60
|
+
|
|
61
|
+
export type SpendTelemetryVerdict =
|
|
62
|
+
/** Enough working runs, and most of them metered. */
|
|
63
|
+
| { kind: "healthy"; worked: number; missing: number; totalUsd: number }
|
|
64
|
+
/** Not enough working runs to say anything — never a pass by default. */
|
|
65
|
+
| { kind: "insufficient"; worked: number; needed: number }
|
|
66
|
+
/** A majority of working runs reported nothing. */
|
|
67
|
+
| { kind: "partial"; worked: number; missing: number }
|
|
68
|
+
/** Every working run reported nothing — the same fact, stated louder. */
|
|
69
|
+
| { kind: "absent"; worked: number };
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Judge a newest-first sample.
|
|
73
|
+
*
|
|
74
|
+
* `limit` is how many *working* runs are wanted, not how many rows to read: the
|
|
75
|
+
* caller over-samples and this takes the first `limit` runs that did work, so a
|
|
76
|
+
* burst of 0-turn kills cannot push the real evidence out of the window.
|
|
77
|
+
*/
|
|
78
|
+
export function judgeSpendTelemetry(
|
|
79
|
+
samples: readonly SpendSample[],
|
|
80
|
+
limit: number,
|
|
81
|
+
): SpendTelemetryVerdict {
|
|
82
|
+
const working = samples.filter((row) => row.turns > 0).slice(0, limit);
|
|
83
|
+
if (working.length < limit) {
|
|
84
|
+
return { kind: "insufficient", worked: working.length, needed: limit };
|
|
85
|
+
}
|
|
86
|
+
const missing = working.filter((row) => row.spendUsd === 0).length;
|
|
87
|
+
if (missing === working.length) return { kind: "absent", worked: working.length };
|
|
88
|
+
if (missing / working.length >= SPEND_MISSING_SHARE) {
|
|
89
|
+
return { kind: "partial", worked: working.length, missing };
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
kind: "healthy",
|
|
93
|
+
worked: working.length,
|
|
94
|
+
missing,
|
|
95
|
+
totalUsd: working.reduce((sum, row) => sum + row.spendUsd, 0),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The operator-facing sentence, or undefined when there is nothing to say.
|
|
100
|
+
* Shared so `doctor` and `status` cannot word the same fact differently. */
|
|
101
|
+
export function spendTelemetryDetail(verdict: SpendTelemetryVerdict): string | undefined {
|
|
102
|
+
switch (verdict.kind) {
|
|
103
|
+
case "absent":
|
|
104
|
+
return (
|
|
105
|
+
`the last ${verdict.worked} completed runs that did any work all recorded $0.00 spend` +
|
|
106
|
+
" — the USD cap cannot fire on zeros, and $0.00 is not proof of no spend"
|
|
107
|
+
);
|
|
108
|
+
case "partial":
|
|
109
|
+
return (
|
|
110
|
+
`${verdict.missing} of the last ${verdict.worked} completed runs that did any work recorded ` +
|
|
111
|
+
"$0.00 spend — the USD cap is comparing a fraction of actual spend against its ceiling"
|
|
112
|
+
);
|
|
113
|
+
case "healthy":
|
|
114
|
+
case "insufficient":
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
}
|
package/src/stats.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* counted separately as **unmetered** and never averaged into a per-issue cost.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
+
import { utcDay } from "./store.ts";
|
|
22
23
|
import type { RunRecord } from "./types.ts";
|
|
23
24
|
|
|
24
25
|
/** The bounded window a report answers over. Day keys are UTC (`YYYY-MM-DD`). */
|
|
@@ -181,6 +182,40 @@ function summarize(args: {
|
|
|
181
182
|
* settled there. Rows within an issue must be ordered by `startedAt`, so the
|
|
182
183
|
* first row of a chain is its first queue-label claim.
|
|
183
184
|
*/
|
|
185
|
+
/** `7d` / `30d` / any positive whole-day duration. */
|
|
186
|
+
const DURATION_FORM = /^(\d+)d$/;
|
|
187
|
+
/** A bare UTC date: the window starts at 00:00:00Z that day. */
|
|
188
|
+
const DATE_FORM = /^\d{4}-\d{2}-\d{2}$/;
|
|
189
|
+
const DAY_MS = 86_400_000;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* `--since` (or `?since=`) to a full window, or `undefined` when it is not one.
|
|
193
|
+
*
|
|
194
|
+
* Here rather than in the CLI because the dashboard needs the identical parse
|
|
195
|
+
* (#297): two parsers would eventually disagree about what `30d` means, and the
|
|
196
|
+
* page's numbers are supposed to equal `stats --json` for the same window. The
|
|
197
|
+
* CLI turns `undefined` into its exit-2 message and the endpoint turns it into a
|
|
198
|
+
* 400 — neither silently defaults a typo'd window, because measuring the wrong
|
|
199
|
+
* week and saying nothing is the failure this shape prevents.
|
|
200
|
+
*/
|
|
201
|
+
export function parseStatsWindow(raw: string, now: number): StatsWindow | undefined {
|
|
202
|
+
const until = { untilEpochMs: now, untilDay: utcDay(now) };
|
|
203
|
+
const duration = DURATION_FORM.exec(raw);
|
|
204
|
+
if (duration !== null) {
|
|
205
|
+
const days = Number(duration[1]);
|
|
206
|
+
if (days >= 1) {
|
|
207
|
+
const sinceEpochMs = now - days * DAY_MS;
|
|
208
|
+
return { sinceEpochMs, sinceDay: utcDay(sinceEpochMs), ...until };
|
|
209
|
+
}
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
if (DATE_FORM.test(raw)) {
|
|
213
|
+
const sinceEpochMs = Date.parse(`${raw}T00:00:00Z`);
|
|
214
|
+
if (Number.isFinite(sinceEpochMs)) return { sinceEpochMs, sinceDay: raw, ...until };
|
|
215
|
+
}
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
184
219
|
export function computeStats(args: {
|
|
185
220
|
project: string;
|
|
186
221
|
window: StatsWindow;
|