omp-conductor 0.20.1 → 0.20.2

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.
@@ -23,6 +23,7 @@ import { availabilityState, type AvailabilityState } from "../availability.ts";
23
23
  import { configPath, findProject, loadConfig, resolveCaps, resolveReleaseGrants, resolveReview, stateDir } from "../config.ts";
24
24
  import { digestScheduleState, type DigestScheduleState } from "../digest-schedule.ts";
25
25
  import { SPEND_SAMPLE_ROWS, SPEND_SAMPLE_RUNS } from "../doctor.ts";
26
+ import { EFFECTIVE_BUDGET_SAMPLE_RUNS, observeTurnBudget, type ObservedTurnBudget } from "../failure-class.ts";
26
27
  import type { CodeGraphHealth } from "../graph-health.ts";
27
28
  import { isPaused, pauseProvenance, setPaused } from "../pause.ts";
28
29
  import { branchName, route } from "../routing.ts";
@@ -189,6 +190,11 @@ export interface StatusSnapshot {
189
190
  */
190
191
  reporting?: ReportingSummary;
191
192
  caps: Caps;
193
+ /** #1063: the turn count the wall-clock ceiling actually buys at this
194
+ * project's observed per-turn latency, with the sample it was derived
195
+ * from. Absent when no qualifying completed run exists — never derived
196
+ * from the configured constants, whose ratio is a constant. */
197
+ workerTurnBudgetObserved?: ObservedTurnBudget;
192
198
  /**
193
199
  * The effective per-shape release grants. On the snapshot rather than re-read
194
200
  * by each renderer because #122 began with a grant nobody had looked at in
@@ -552,6 +558,17 @@ export function statusSnapshotFromStore(
552
558
  const observed = store.installSurfaces();
553
559
  return observed === undefined ? {} : { installSurfaces: observed };
554
560
  })(),
561
+ // #1063, the same read-not-probe discipline as the spend judgement above:
562
+ // derived from completed-run rows on this read, never probed at render
563
+ // time. The renderer decides whether the figure is materially lower.
564
+ ...(() => {
565
+ const budget = observeTurnBudget(
566
+ store.recentLatencySamples(p.name, EFFECTIVE_BUDGET_SAMPLE_RUNS),
567
+ caps,
568
+ p.workerModel,
569
+ );
570
+ return budget === undefined ? {} : { workerTurnBudgetObserved: budget };
571
+ })(),
555
572
  ...(dispatch === undefined ? {} : { dispatch }),
556
573
  ...(planUsage === undefined ? {} : { planUsage }),
557
574
  // Written by the tracker's hooks rather than polled, so the renderer does
package/src/daemon.ts CHANGED
@@ -34,7 +34,7 @@ export type {
34
34
  export { completionLastError, exhaustedSessionReason, verbDeps } from "./daemon/deps.ts";
35
35
 
36
36
  export type { CreateDrainOptions, DrainProblem, DrainRecord, DrainVerdict } from "./daemon/drain.ts";
37
- export { cancelDrain, consumeDrain, createDrain, drainPath, readDrain } from "./daemon/drain.ts";
37
+ export { cancelDrain, consumeDrain, createDrain, drainPath, markDrained, readDrain } from "./daemon/drain.ts";
38
38
 
39
39
  export type { AdmissionAckRecord } from "./daemon/ack.ts";
40
40
  export { admissionAckPath, daemonGeneration, readAdmissionAck, wakeDaemon, writeAdmissionAck } from "./daemon/ack.ts";
@@ -52,6 +52,7 @@ export {
52
52
  wakeOrchestratorForBlockedRun,
53
53
  wakeOrchestratorForMetConditions,
54
54
  watchOrchestrator,
55
+ watchWorkerProgress,
55
56
  } from "./daemon/supervision.ts";
56
57
 
57
58
  export { reconcilePanes } from "./daemon/panes.ts";
package/src/decisions.ts CHANGED
@@ -45,26 +45,35 @@ const NPM_SPEC = /^(@[^/@\s]+\/)?[^@\s]+@[^\s]+$/;
45
45
  const GREEN_CHECK_STATES: Record<string, true> = { success: true, neutral: true };
46
46
 
47
47
  /**
48
- * The run states a review revision may start from (#795) — the single
48
+ * The run states a review revision may start from (#795, #1101) — the single
49
49
  * definition shared by `conductor_pr_review` and the `pr-review-ready` watch,
50
50
  * so the verb's gate and the condition can never name different sets.
51
51
  *
52
52
  * A revision round resumes the exact run whose row owns the PR, so the
53
- * revisable states are exactly the terminal runs that pushed one: a settled
54
- * `pushed-green` row, or a `failed` / `killed` row — a run that capped or
55
- * failed *after* pushing a green PR. The PR is the durable artefact, the
56
- * exact-head green verification is the gate on "green at the reviewed SHA",
57
- * and a terminal row proves no worker is in flight, so findings are returned
53
+ * revisable states are exactly the terminal runs that own the named PR: a
54
+ * settled `pushed-green` row, a `failed` / `killed` row — a run that capped or
55
+ * failed *after* pushing a green PR and, since #1101, a `stopped` row that
56
+ * pushed one. Ownership is not carried by the state alone: selection runs
57
+ * through `runsForProjectPr`, so only a row that itself recorded the reviewed
58
+ * PR can ever reach this gate. The PR is the durable artefact, the exact-head
59
+ * green verification is the gate on "green at the reviewed SHA", and a
60
+ * terminal row proves no worker is in flight, so findings are returned
58
61
  * without the close-PR → unblock → continuation dance.
59
62
  *
63
+ * A `stopped` row has no settle sweep keeping it honest (nothing transitions a
64
+ * stopped row when its PR merges or closes), so like `failed` / `killed` its
65
+ * rounds re-read PR-open and green-at-head decisively before the claim — the
66
+ * dispatch pass refuses to wake a worker against a dead or moved PR.
67
+ *
60
68
  * Closed on purpose: a live row (`running` / `claimed`) is already doing its
61
69
  * own work, a `pushed-pending` PR is not green yet, and a `blocked` /
62
- * `orphaned` / `stopped` / `merged` row is not work returned for revision.
70
+ * `orphaned` / `merged` row is not work returned for revision.
63
71
  */
64
72
  export const REVISABLE_RUN_STATES: Record<string, true> = {
65
73
  "pushed-green": true,
66
74
  failed: true,
67
75
  killed: true,
76
+ stopped: true,
68
77
  };
69
78
 
70
79
  /**
@@ -92,8 +101,10 @@ export const PR_LOOKUP_WINDOW_MS = 30 * 24 * 60 * 60_000;
92
101
  * `running`, and a watch keyed only to checks woke the orchestrator before
93
102
  * `conductor_pr_review` was actionable), `pushed-pending` checks are still
94
103
  * settling, `blocked` may resume, `orphaned` is reconciled back to live at
95
- * startup, and `merged` means the PR lifecycle is over. When every row is
96
- * `stopped`, the newest one answers and the predicate fails closed.
104
+ * startup, and `merged` means the PR lifecycle is over. When every row of the
105
+ * history is `stopped`, the newest one answers and since #1101 it answers
106
+ * `ready` when it owns the named PR: stopping a worker that had already pushed
107
+ * must leave the PR reviewable rather than stranded.
97
108
  *
98
109
  * `no-owner` and `not-revisable` both fail closed: a review can never act, so
99
110
  * a watch must not wake, even when the checks are green.
package/src/diff-flags.ts CHANGED
@@ -37,7 +37,7 @@
37
37
  * repository.
38
38
  */
39
39
 
40
- import type { FileLane, PrDiff, PrDiffFile, SettlementFlag } from "./types.ts";
40
+ import type { FileLane, PrDiff, PrDiffFile, SettlementFlag, SettlementFlagKind } from "./types.ts";
41
41
 
42
42
  // ------------------------------------------------------------------ diff parse
43
43
 
@@ -657,21 +657,24 @@ export const UNREADABLE_TREE_FLAG: SettlementFlag = {
657
657
 
658
658
  /**
659
659
  * Whether a diff path counts as inside the declared lane: an explicitly
660
- * declared path, or the co-located test of one `foo.ts` vouches for
661
- * `foo.test.ts`, which is the "obviously intended" case. Other test shapes
662
- * (`.spec.ts`, pytest's `test_` prefix) are not vouched for: the rule is the
663
- * shape the fleet actually uses, and a lane that wants a differently-shaped
664
- * sibling declares it. Deliberately one-directional: a lane that declares a
665
- * *test* file does not vouch for its source, because declaring the test alone
666
- * is a narrower promise and widening it silently is exactly what this flag
667
- * exists to name. A containing directory never vouches for its contents
668
- * either the lane grammar names files, and a lane that means "everything
669
- * under `src/`" fails open exactly as an undeclared one would if it cannot
670
- * name them.
660
+ * declared path, the co-located test of one, or a descendant of a declared
661
+ * directory (#1091) admission's grammar accepts a trailing-`/` entry
662
+ * ("everything under `src/`"), so settlement honours the same contract rather
663
+ * than narrowing it back to exact files. The co-located allowance is the
664
+ * "obviously intended" case: `foo.ts` vouches for `foo.test.ts`. Other test
665
+ * shapes (`.spec.ts`, pytest's `test_` prefix) are not vouched for: the rule
666
+ * is the shape the fleet actually uses, and a lane that wants a
667
+ * differently-shaped sibling declares it. Deliberately one-directional: a lane
668
+ * that declares a *test* file does not vouch for its source, because declaring
669
+ * the test alone is a narrower promise and widening it silently is exactly
670
+ * what this flag exists to name. The directory rule keeps its trailing-slash
671
+ * boundary explicit: the entry already carries the separator, so `foo/`
672
+ * covers `foo/bar.ts` at any depth and can never vouch for `foobar/x.ts`.
671
673
  */
672
674
  function withinLane(path: string, declared: readonly string[]): boolean {
673
675
  if (declared.includes(path)) return true;
674
676
  for (const d of declared) {
677
+ if (d.endsWith("/") && path.startsWith(d)) return true;
675
678
  if (coLocatedTest(d) === path) return true;
676
679
  }
677
680
  return false;
@@ -693,11 +696,22 @@ function coLocatedTest(declared: string): string | undefined {
693
696
  * The lane is the effective declaration admission resolved at dispatch —
694
697
  * `effectiveLane(body, comments)`, so a pre-dispatch comment beats an older
695
698
  * body declaration — and the flag uses that resolved snapshot, never a re-parse
696
- * of the body. The finding names every delivered file outside it, which is the
699
+ * of the body. Each finding names the delivered files outside it, which is the
697
700
  * part a reviewer is worst placed to notice: the diff's own file list is the
698
701
  * only surface that shows the escape, and reading PR file lists by hand is
699
702
  * exactly what nothing else in the loop does.
700
703
  *
704
+ * An escape splits into up to two findings because it is up to two different
705
+ * events (#1096): an undeclared production module is scope escaping into code,
706
+ * while an undeclared test file is usually coverage arriving beside the
707
+ * behaviour it pins — the #1064/#1080 shape, required work the declaration
708
+ * could not have named. Naming the class on each finding keeps the second
709
+ * readable as what it usually is without letting it disguise the first. The
710
+ * declared source's own `.test` twin is admitted by {@link withinLane}, so
711
+ * the obvious case raises nothing — and the rule stays one-directional, so a
712
+ * lane declaring only the test still names its source when the worker edits
713
+ * it.
714
+ *
701
715
  * Fail-open, like admission: an issue with no lane declaration has nothing to
702
716
  * escape, so no flag — a flag on every undeclared run would be noise within a
703
717
  * day, worse than no flag. Advisory like every other flag here: a widened lane
@@ -710,11 +724,23 @@ function detectLaneEscape(audit: SettlementAudit, flags: SettlementFlag[]): void
710
724
  .map((f) => f.path)
711
725
  .filter((path) => !withinLane(path, lane.files));
712
726
  if (outside.length === 0) return;
713
- flags.push({
714
- kind: "lane-escape",
715
- file: "(lane)",
716
- detail: `PR diff touches files outside the declared file lane: ${outside.join(", ")}`,
717
- });
727
+ // Production first: the rarer, heavier event leads the report block.
728
+ const production = outside.filter((path) => !isTestPath(path));
729
+ const otherTests = outside.filter((path) => isTestPath(path));
730
+ if (production.length > 0) {
731
+ flags.push({
732
+ kind: "lane-escape",
733
+ file: "(lane)",
734
+ detail: `PR diff touches production files outside the declared file lane: ${production.join(", ")}`,
735
+ });
736
+ }
737
+ if (otherTests.length > 0) {
738
+ flags.push({
739
+ kind: "lane-escape",
740
+ file: "(lane)",
741
+ detail: `PR diff touches unrelated test files outside the declared file lane: ${otherTests.join(", ")}`,
742
+ });
743
+ }
718
744
  }
719
745
 
720
746
  function detectWeakening(audit: SettlementAudit, flags: SettlementFlag[]): void {
@@ -1229,6 +1255,21 @@ const RENDERED_FLAGS = 20;
1229
1255
 
1230
1256
  const HEADING = "settlement audit";
1231
1257
 
1258
+ /**
1259
+ * The kinds that mean coverage got weaker — the quiet signals the audit
1260
+ * exists to deliver, and the family mining.ts mines. Presentation keeps them
1261
+ * ahead of everything else and lane escapes last (#1096): a check that fires
1262
+ * on half of all merges gets skimmed, and a skimmed finding must not sit
1263
+ * between the reviewer and these. Kept in step with `WEAKENING_KINDS` in
1264
+ * mining.ts, which is the same four kinds spelled for mining.
1265
+ */
1266
+ const TEST_INTEGRITY_KINDS: Partial<Record<SettlementFlagKind, true>> = {
1267
+ "test-file-deleted": true,
1268
+ "test-disabled": true,
1269
+ "assertions-removed": true,
1270
+ "test-timeout-raised": true,
1271
+ };
1272
+
1232
1273
  /**
1233
1274
  * The flag block appended to a settlement report, or no lines at all.
1234
1275
  *
@@ -1253,7 +1294,17 @@ export function formatSettlementFlags(
1253
1294
  `${HEADING}: ${flags.length} advisory flag(s) — the run's state is unchanged by them` +
1254
1295
  (diff.truncated ? ", and the PR diff was too large to read in full" : ""),
1255
1296
  ];
1256
- for (const flag of flags.slice(0, RENDERED_FLAGS)) {
1297
+ // Presentation order is structural, not incidental: test-integrity flags
1298
+ // first, then everything else, lane escapes last (#1096). The analyser
1299
+ // emits them in roughly this order already, but this surface also renders
1300
+ // stored rows and post-settlement appends, whose order nobody chose.
1301
+ const rank = (flag: SettlementFlag): number =>
1302
+ TEST_INTEGRITY_KINDS[flag.kind] === true ? 0 : flag.kind === "lane-escape" ? 2 : 1;
1303
+ const ordered = flags
1304
+ .map((flag, index) => ({ flag, index }))
1305
+ .sort((a, b) => rank(a.flag) - rank(b.flag) || a.index - b.index)
1306
+ .map((entry) => entry.flag);
1307
+ for (const flag of ordered.slice(0, RENDERED_FLAGS)) {
1257
1308
  if (flag.kind === "pr-adopted") {
1258
1309
  lines.push(` ${flag.kind} — ${flag.detail}`);
1259
1310
  continue;
@@ -1270,10 +1321,30 @@ export function formatSettlementFlags(
1270
1321
  }
1271
1322
 
1272
1323
  /** The one-line form for `omp-conductor status`, where a flagged run has to be
1273
- * visible long after its escalation was delivered and deduplicated. */
1324
+ * visible long after its escalation was delivered and deduplicated. Lane
1325
+ * escapes are presented apart from the test-integrity family (#1096), so the
1326
+ * noisy check cannot bury the quiet one in one comma join. */
1274
1327
  export function settlementFlagSummary(flags: readonly SettlementFlag[] | undefined): string | undefined {
1275
1328
  if (flags === undefined || flags.length === 0) return undefined;
1276
- const kinds = [...new Set(flags.map((f) => f.kind))].join(", ");
1329
+ const integrity = new Set<SettlementFlagKind>();
1330
+ const lane = new Set<SettlementFlagKind>();
1331
+ const rest = new Set<SettlementFlagKind>();
1332
+ for (const { kind } of flags) {
1333
+ if (TEST_INTEGRITY_KINDS[kind] === true) integrity.add(kind);
1334
+ else if (kind === "lane-escape") lane.add(kind);
1335
+ else rest.add(kind);
1336
+ }
1337
+ const segments: { label?: string; kinds: SettlementFlagKind[] }[] = [];
1338
+ if (integrity.size > 0) segments.push({ label: "test-integrity", kinds: [...integrity] });
1339
+ if (rest.size > 0) segments.push({ kinds: [...rest] });
1340
+ if (lane.size > 0) segments.push({ label: "lane", kinds: [...lane] });
1341
+ // One family alone reads exactly as before — a label with nothing to
1342
+ // separate is furniture.
1343
+ const parts = segments.map((segment) =>
1344
+ segments.length > 1 && segment.label !== undefined
1345
+ ? `${segment.label}: ${segment.kinds.join(", ")}`
1346
+ : segment.kinds.join(", "),
1347
+ );
1277
1348
  const loud = flags.some((f) => f.unattributed === true) ? ", some unattributed" : "";
1278
- return `${HEADING}: ${flags.length} flag(s) — ${kinds}${loud}`;
1349
+ return `${HEADING}: ${flags.length} flag(s) — ${parts.join("; ")}${loud}`;
1279
1350
  }
package/src/doctor.ts CHANGED
@@ -71,6 +71,7 @@ import {
71
71
  killOf,
72
72
  readTelegramChannel,
73
73
  readTelegramDmOwner,
74
+ readTelegramMisroutes,
74
75
  readTelegramPollState,
75
76
  resolveClaimedSessionFile,
76
77
  resolveProjectClaim,
@@ -82,6 +83,7 @@ import {
82
83
  type TelegramChannelState,
83
84
  type TelegramDmOwnerState,
84
85
  type TelegramPollState,
86
+ type TelegramMisroute,
85
87
  } from "./escalate.ts";
86
88
  import {
87
89
  herdrConductorPluginConfigDir,
@@ -286,6 +288,11 @@ export interface DoctorDeps {
286
288
  * `unavailable` when the registry cannot be read or does not parse — the
287
289
  * caller must not read that as "no claims" (#626). */
288
290
  claimedTopics?: () => ClaimedTopicsResult;
291
+ /** Recorded flat-chat deliveries this project's stale pin already caused
292
+ * (#1094), read from conductor state. The topic-pin row renders them as
293
+ * observed evidence until the pin is repaired — rows match the *current*
294
+ * pin id, so re-pinning is what retires them. */
295
+ topicMisroutes?: () => readonly TelegramMisroute[];
289
296
  /** Whether the bridge's access.json runs per-session topic tidy on. */
290
297
  topicsTidy?: () => boolean;
291
298
  /** The typed bot.lock poll-ownership read, told apart so absent/malformed
@@ -1390,14 +1397,24 @@ function topicPinProbe(probes: Probes, p: ProjectConfig): Finding {
1390
1397
  `[${p.name}] no pinned topic id — sends go to the flat chat (live-claim substitution only follows a stale pin)`,
1391
1398
  );
1392
1399
  }
1400
+ // #1094: sends that already rode the flat chat because of THIS pin turn the
1401
+ // warnings below from hypothetical into observed. Rows are keyed to the pin
1402
+ // id, so re-pinning retires them — nothing deletes them.
1403
+ const misroutes = probes.topicMisroutes().filter(
1404
+ (row) => row.project === p.name && row.staleTopicId === pinned,
1405
+ );
1406
+ const misrouteNote =
1407
+ misroutes.length === 0
1408
+ ? ""
1409
+ : ` It has happened: ${misroutes.length} send(s) were delivered to the flat chat because of this pin, last at ${new Date(misroutes[misroutes.length - 1]!.at).toISOString()}`;
1393
1410
  const result = probes.claimedTopics();
1394
1411
  if (result.kind !== "ok") {
1395
1412
  return warnFinding(
1396
1413
  "topic-pin",
1397
1414
  `[${p.name}] pinned topic ${pinned} — ` +
1398
1415
  (result.kind === "missing"
1399
- ? `omp-telegram has no claim registry (threads.json in its state dir), so doctor cannot tell whether the pin is live or a claim answers to this project`
1400
- : `omp-telegram's claim registry is unreadable (${result.problem}), so doctor cannot tell whether the pin is live or a claim answers to this project`),
1416
+ ? `omp-telegram has no claim registry (threads.json in its state dir), so doctor cannot tell whether the pin is live or a claim answers to this project${misrouteNote}`
1417
+ : `omp-telegram's claim registry is unreadable (${result.problem}), so doctor cannot tell whether the pin is live or a claim answers to this project${misrouteNote}`),
1401
1418
  `check omp-telegram's claim registry (threads.json in its state dir) is readable and the bridge is running, then re-run doctor — until then sends keep the pinned topic and degrade to the flat chat on a missing thread (#318)`,
1402
1419
  );
1403
1420
  }
@@ -1423,7 +1440,7 @@ function topicPinProbe(probes: Probes, p: ProjectConfig): Finding {
1423
1440
  if (dead.length > 0) {
1424
1441
  return warnFinding(
1425
1442
  "topic-pin",
1426
- `[${p.name}] pinned topic ${pinned} — every claim in omp-telegram's registry is dead${deadNote}`,
1443
+ `[${p.name}] pinned topic ${pinned} — every claim in omp-telegram's registry is dead${deadNote}${misrouteNote}`,
1427
1444
  `run /cleanup in Telegram so the bridge closes those topics and drops their rows itself, then re-run setup to re-pin escalation.telegramTopicId to a live claim — never delete rows from threads.json by hand, because the row is the only index to the remote topic (#987)`,
1428
1445
  );
1429
1446
  }
@@ -1437,7 +1454,7 @@ function topicPinProbe(probes: Probes, p: ProjectConfig): Finding {
1437
1454
  if (claims.some((claim) => claim.threadId === pinned)) {
1438
1455
  return passFinding("topic-pin", `[${p.name}] pinned topic ${pinned} is a live claim${deadNote}`);
1439
1456
  }
1440
- const match = resolveProjectClaim(claims, p.name, probes.pidAlive);
1457
+ const match = resolveProjectClaim(claims, p.name, probes.pidAlive, p.workspaceRoot);
1441
1458
  if (match.kind === "match") {
1442
1459
  return passFinding(
1443
1460
  "topic-pin",
@@ -1454,7 +1471,7 @@ function topicPinProbe(probes: Probes, p: ProjectConfig): Finding {
1454
1471
  return warnFinding(
1455
1472
  "topic-pin",
1456
1473
  `[${p.name}] pinned topic ${pinned} is not among omp-telegram's live claims, and several live claims answer to this project ` +
1457
- `(topics ${match.claimants.map((c) => c.threadId).join(", ")}) — substitution would be a coin toss, so sends keep the stale pin`,
1474
+ `(topics ${match.claimants.map((c) => c.threadId).join(", ")}) — substitution would be a coin toss, so sends keep the stale pin${misrouteNote}`,
1458
1475
  `make exactly one live claim answer to this project: rename the other panes' herdr spaces or re-claim them one at a time, then re-run doctor — live-claim substitution follows the unique claim from there`,
1459
1476
  );
1460
1477
  }
@@ -1463,14 +1480,14 @@ function topicPinProbe(probes: Probes, p: ProjectConfig): Finding {
1463
1480
  "topic-pin",
1464
1481
  `[${p.name}] pinned topic ${pinned} is not among omp-telegram's live claims, no live claim answers ` +
1465
1482
  `to this project, and this host runs topicsTidy — the pin is vestigial and can never be live again ` +
1466
- `(every pane exit closes its topic); sends keep the stale pin`,
1483
+ `(every pane exit closes its topic); sends keep the stale pin${misrouteNote}`,
1467
1484
  `turn topicsTidy off, then re-run setup to re-pin escalation.telegramTopicId to a currently claimed topic`,
1468
1485
  );
1469
1486
  }
1470
1487
  return warnFinding(
1471
1488
  "topic-pin",
1472
1489
  `[${p.name}] pinned topic ${pinned} is not among omp-telegram's live claims (${claims.map((c) => c.threadId).join(", ")}), ` +
1473
- `and no live claim answers to this project — sends keep the stale pin and degrade to the flat chat`,
1490
+ `and no live claim answers to this project — sends keep the stale pin and degrade to the flat chat${misrouteNote}`,
1474
1491
  `update escalation.telegramTopicId to a currently claimed topic, or re-run setup to re-pin`,
1475
1492
  );
1476
1493
  }
@@ -2163,6 +2180,7 @@ export function defaultProbes(): Probes {
2163
2180
  herdrConfig: defaultHerdrConfig,
2164
2181
  herdrEnv: defaultHerdrEnv,
2165
2182
  claimedTopics: () => claimedTelegramTopics(),
2183
+ topicMisroutes: () => readTelegramMisroutes(),
2166
2184
  topicsTidy: () => telegramTopicsTidy(),
2167
2185
  pollState: () => readTelegramPollState(),
2168
2186
  dmOwner: () => readTelegramDmOwner(),