omp-conductor 0.16.2 → 0.17.1
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 +38 -4
- package/REFERENCE.md +18 -12
- package/package.json +2 -1
- package/schema/config.schema.json +16 -0
- package/src/admission.ts +159 -43
- package/src/availability.ts +27 -1
- package/src/briefs/worker.md +2 -0
- package/src/clack-ui.ts +83 -0
- package/src/command-manifest.ts +16 -7
- package/src/commands/arm.ts +11 -3
- package/src/commands/decision.ts +17 -7
- package/src/commands/doctor.ts +18 -1
- package/src/commands/hold.ts +9 -7
- package/src/commands/ledger.ts +25 -4
- package/src/commands/message.ts +32 -4
- package/src/commands/setup.ts +61 -10
- package/src/commands/stats.ts +9 -5
- package/src/commands/status.ts +32 -5
- package/src/commands/tail.ts +13 -1
- package/src/commands/watch.ts +16 -7
- package/src/config-schema.ts +20 -0
- package/src/config.ts +37 -0
- package/src/daemon.ts +1240 -18
- package/src/doctor.ts +310 -22
- package/src/escalate.ts +560 -57
- package/src/failure-class.ts +56 -13
- package/src/fleet.ts +224 -47
- package/src/gitops.ts +103 -24
- package/src/lifecycle.ts +7 -2
- package/src/orchestrator-tick.ts +372 -157
- package/src/privileged.ts +3 -0
- package/src/release-policy.ts +177 -5
- package/src/setup-answers.ts +135 -0
- package/src/setup-host.ts +193 -4
- package/src/setup-install.ts +2 -0
- package/src/setup-probe.ts +1 -0
- package/src/setup-wizard.ts +1296 -101
- package/src/setup.ts +60 -3
- package/src/status-render.ts +11 -1
- package/src/store.ts +333 -12
- package/src/tracker/github.ts +562 -13
- package/src/types.ts +204 -2
- package/src/ui/progress.ts +32 -0
- package/src/ui/style.ts +11 -0
- package/src/upgrade.ts +50 -19
- package/src/verbs/actions.ts +66 -18
- package/src/verbs/protocol.ts +45 -0
- package/src/verbs/server.ts +212 -11
- package/src/wizard-ui.ts +14 -5
- package/src/worker.ts +26 -0
- package/systemd/omp-conductor-recover.sh +73 -0
- package/systemd/recover-unit-test.sh +61 -0
package/src/doctor.ts
CHANGED
|
@@ -54,16 +54,29 @@ import {
|
|
|
54
54
|
DEFAULT_HERDR_UNIT,
|
|
55
55
|
probeTelegramHealth,
|
|
56
56
|
resolveHerdrSessionWithBridge,
|
|
57
|
+
resolveTickConfig,
|
|
57
58
|
sessionsRoot,
|
|
58
59
|
sessionDirForCwd,
|
|
59
60
|
telegramStateDir,
|
|
60
61
|
} from "./fleet.ts";
|
|
61
62
|
import type { TelegramHealth } from "./status-render.ts";
|
|
62
63
|
import {
|
|
63
|
-
claimForProject,
|
|
64
64
|
claimedTelegramTopics,
|
|
65
|
+
lockPidAlive,
|
|
66
|
+
pidAlive,
|
|
67
|
+
readTelegramChannel,
|
|
68
|
+
readTelegramDmOwner,
|
|
69
|
+
readTelegramPollState,
|
|
70
|
+
resolveClaimedSessionFile,
|
|
71
|
+
resolveProjectClaim,
|
|
72
|
+
resolveProjectTopicId,
|
|
73
|
+
telegramPlumbingVerdict,
|
|
65
74
|
telegramTopicsTidy,
|
|
66
|
-
|
|
75
|
+
TELEGRAM_LOCK_FRESH_MS,
|
|
76
|
+
type ClaimedTopicsResult,
|
|
77
|
+
type TelegramChannelState,
|
|
78
|
+
type TelegramDmOwnerState,
|
|
79
|
+
type TelegramPollState,
|
|
67
80
|
} from "./escalate.ts";
|
|
68
81
|
import {
|
|
69
82
|
herdrConductorPluginConfigDir,
|
|
@@ -212,10 +225,44 @@ export interface DoctorDeps {
|
|
|
212
225
|
herdrConfig?: () => string | undefined;
|
|
213
226
|
/** The live herdr-conductor plugin `config.env`, or undefined. */
|
|
214
227
|
herdrEnv?: () => string | undefined;
|
|
215
|
-
/** Live omp-telegram topic claims (threads.json), for the #600 probes.
|
|
216
|
-
|
|
228
|
+
/** Live omp-telegram topic claims (threads.json), for the #600 probes.
|
|
229
|
+
* `unavailable` when the registry cannot be read or does not parse — the
|
|
230
|
+
* caller must not read that as "no claims" (#626). */
|
|
231
|
+
claimedTopics?: () => ClaimedTopicsResult;
|
|
217
232
|
/** Whether the bridge's access.json runs per-session topic tidy on. */
|
|
218
233
|
topicsTidy?: () => boolean;
|
|
234
|
+
/** The typed bot.lock poll-ownership read, told apart so absent/malformed
|
|
235
|
+
* state is a #612 failure — the lock, not daemon.json, is what says whether
|
|
236
|
+
* the bridge is actually polling. */
|
|
237
|
+
pollState?: () => TelegramPollState;
|
|
238
|
+
/** The typed dm-owner.json read — the untopiced-DM recipient a flat
|
|
239
|
+
* challenge's reply routes through (`/telegram own`). Absent/malformed is a
|
|
240
|
+
* named "no pinned owner", not a pass. */
|
|
241
|
+
dmOwner?: () => TelegramDmOwnerState;
|
|
242
|
+
/** The paired inbound channel state, probed exactly as armTicks probes it
|
|
243
|
+
* (readTelegramChannel over the tick's accessFile). */
|
|
244
|
+
channelState?: (project: ProjectConfig) => TelegramChannelState;
|
|
245
|
+
/** Whether the project can ride inbound at all — a readable tick config
|
|
246
|
+
* naming an accessFile. The tick runtime treats a missing accessFile as no
|
|
247
|
+
* fleet bridge, so that shape is neutral, never a fault. */
|
|
248
|
+
projectArms?: (project: ProjectConfig) => boolean;
|
|
249
|
+
/** The topic id an arm challenge would send into for this project —
|
|
250
|
+
* resolveProjectTopicId's pin-or-live-claim; undefined means flat chat. */
|
|
251
|
+
armSendTopic?: (project: ProjectConfig) => number | undefined;
|
|
252
|
+
/** The session directories arm scans for the challenge reply on this
|
|
253
|
+
* project — tick-cwd-derived plus the live claim's own directory. */
|
|
254
|
+
armScanDirs?: (project: ProjectConfig) => readonly string[];
|
|
255
|
+
/** Whether a recorded claim or dm-owner pid is live, with omp-telegram's
|
|
256
|
+
* topics.ts semantics (EPERM is dead). */
|
|
257
|
+
pidAlive?: (pid: number) => boolean;
|
|
258
|
+
/** Whether a bot.lock owner pid is live, with omp-telegram's api.ts
|
|
259
|
+
* semantics (EPERM is live). Distinct from {@link pidAlive} because the
|
|
260
|
+
* bridge itself uses two rules. */
|
|
261
|
+
lockPidAlive?: (pid: number) => boolean;
|
|
262
|
+
/** Whether a lock-file mtime is younger than LOCK_FRESH_MS — the bridge's
|
|
263
|
+
* own liveness rule: a fresh heartbeat keeps the poll live even when the
|
|
264
|
+
* recorded pid is gone. */
|
|
265
|
+
lockFresh?: (mtimeMs: number) => boolean;
|
|
219
266
|
/** The fleet agent name the tick config of one project names, or undefined
|
|
220
267
|
* when there is no (readable) tick — the expected live herdr pane identity. */
|
|
221
268
|
tickAgentName?: (project: ProjectConfig) => string | undefined;
|
|
@@ -1094,15 +1141,52 @@ function herdrAgentNameProbe(probes: Probes, p: ProjectConfig): Finding {
|
|
|
1094
1141
|
* doctor fails rather than letting the fleet sit disarmed.
|
|
1095
1142
|
*/
|
|
1096
1143
|
function armSessionDirProbe(probes: Probes, p: ProjectConfig): Finding {
|
|
1097
|
-
const
|
|
1098
|
-
if (
|
|
1144
|
+
const result = probes.claimedTopics();
|
|
1145
|
+
if (result.kind !== "ok") {
|
|
1146
|
+
// The registry could not be read, so the claim's session file is unknown,
|
|
1147
|
+
// not known-absent. Arm scans the tick-cwd directory either way, but the
|
|
1148
|
+
// finding must stay diagnostic — this is exactly the shape that once made
|
|
1149
|
+
// arming silently impossible, and "no claim" would certify it as fine. A
|
|
1150
|
+
// missing registry is the same unknown to this probe: the bridge may never
|
|
1151
|
+
// have claimed, but nothing proves the claim names no session file.
|
|
1152
|
+
return warnFinding(
|
|
1153
|
+
"arm-session-dir",
|
|
1154
|
+
`[${p.name}] ` +
|
|
1155
|
+
(result.kind === "missing"
|
|
1156
|
+
? `omp-telegram has no claim registry (threads.json in its state dir) — arm scans the tick-cwd session directory, but doctor cannot tell whether the claim names a session file elsewhere`
|
|
1157
|
+
: `omp-telegram's claim registry is unreadable (${result.problem}) — arm scans the tick-cwd session directory, but doctor cannot tell whether the claim names a session file elsewhere`),
|
|
1158
|
+
`check omp-telegram's claim registry (threads.json in its state dir) is readable and the bridge is running, then re-run doctor`,
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
if (result.claims.length === 0) {
|
|
1099
1162
|
return passFinding(
|
|
1100
1163
|
"arm-session-dir",
|
|
1101
1164
|
`[${p.name}] no live omp-telegram claim — arm scans the tick-cwd session directory`,
|
|
1102
1165
|
);
|
|
1103
1166
|
}
|
|
1104
|
-
const
|
|
1105
|
-
|
|
1167
|
+
const claims = result.claims;
|
|
1168
|
+
const match = resolveProjectClaim(claims, p.name);
|
|
1169
|
+
const scanDir = sessionDirForCwd(tickCwdForProject(p));
|
|
1170
|
+
if (match.kind === "ambiguous") {
|
|
1171
|
+
// Several live claims answer to the project, so the claim's session file
|
|
1172
|
+
// cannot be told from a sibling pane's. Arm scans the tick-cwd directory
|
|
1173
|
+
// either way, but certifying that fallback as the scan is the miss this
|
|
1174
|
+
// probe exists to surface: the resumed transcript may live under another
|
|
1175
|
+
// claim while doctor reads the fallback as the whole story (#626).
|
|
1176
|
+
return warnFinding(
|
|
1177
|
+
"arm-session-dir",
|
|
1178
|
+
`[${p.name}] several live claims answer to this project (topics ${match.claimants.map((c) => c.threadId).join(", ")}), so the claim's session file is a coin toss — arm scans the tick-cwd session directory, but the resumed transcript may live elsewhere`,
|
|
1179
|
+
`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 — or start the orchestrator pane's session from the tick cwd ${scanDir}`,
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
const claim = match.kind === "match" ? match.claim : undefined;
|
|
1183
|
+
if (claim === undefined) {
|
|
1184
|
+
return passFinding(
|
|
1185
|
+
"arm-session-dir",
|
|
1186
|
+
`[${p.name}] no live omp-telegram claim answers to this project — arm scans the tick-cwd session directory`,
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
if (claim.sessionFile === undefined) {
|
|
1106
1190
|
return passFinding(
|
|
1107
1191
|
"arm-session-dir",
|
|
1108
1192
|
`[${p.name}] the live claim names no session file — arm scans the tick-cwd session directory`,
|
|
@@ -1110,7 +1194,6 @@ function armSessionDirProbe(probes: Probes, p: ProjectConfig): Finding {
|
|
|
1110
1194
|
}
|
|
1111
1195
|
const root = sessionsRoot();
|
|
1112
1196
|
const claimDir = dirname(claim.sessionFile);
|
|
1113
|
-
const scanDir = sessionDirForCwd(tickCwdForProject(p));
|
|
1114
1197
|
if (claimDir !== root && !claimDir.startsWith(join(root, sep))) {
|
|
1115
1198
|
return failFinding(
|
|
1116
1199
|
"arm-session-dir",
|
|
@@ -1138,43 +1221,208 @@ function armSessionDirProbe(probes: Probes, p: ProjectConfig): Finding {
|
|
|
1138
1221
|
*
|
|
1139
1222
|
* The wizard pins from the claims at setup time, but the bridge re-claims
|
|
1140
1223
|
* topics across restarts and conductor restarts the service itself (#407), so
|
|
1141
|
-
* the pin drifts.
|
|
1142
|
-
*
|
|
1143
|
-
*
|
|
1144
|
-
* the
|
|
1224
|
+
* the pin drifts. A stale pin is not itself a fault: {@link
|
|
1225
|
+
* resolveProjectTopicId} follows the project's one unambiguous live claim in
|
|
1226
|
+
* that shape (#407/#412), so routing is healthy exactly when a claim answers
|
|
1227
|
+
* to the project. The pin is a fault only when no live claim can take its
|
|
1228
|
+
* place — then sends keep the stale pin and degrade to the flat chat, and the
|
|
1229
|
+
* fix is to re-pin. Removing the pin is never the fix: a missing pin disables
|
|
1230
|
+
* substitution entirely and opts the project into flat chat (#626).
|
|
1231
|
+
*
|
|
1232
|
+
* A registry that cannot be read is neither proof of health nor a stale pin:
|
|
1233
|
+
* the bridge may be fine and the pin live, but nothing can be verified, so the
|
|
1234
|
+
* finding warns and names restoring the registry — never "the pin stands".
|
|
1235
|
+
*
|
|
1236
|
+
* On a host with `topicsTidy` on, a pin drifts *structurally*: every pane exit
|
|
1237
|
+
* closes the topic, so no re-pin survives and the durable fix is to turn tidy
|
|
1238
|
+
* off and re-pin.
|
|
1145
1239
|
*/
|
|
1146
1240
|
function topicPinProbe(probes: Probes, p: ProjectConfig): Finding {
|
|
1147
1241
|
const pinned = p.escalation.telegramTopicId;
|
|
1148
1242
|
if (pinned === undefined) {
|
|
1149
1243
|
return passFinding(
|
|
1150
1244
|
"topic-pin",
|
|
1151
|
-
`[${p.name}] no pinned topic id — sends
|
|
1245
|
+
`[${p.name}] no pinned topic id — sends go to the flat chat (live-claim substitution only follows a stale pin)`,
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
const result = probes.claimedTopics();
|
|
1249
|
+
if (result.kind !== "ok") {
|
|
1250
|
+
return warnFinding(
|
|
1251
|
+
"topic-pin",
|
|
1252
|
+
`[${p.name}] pinned topic ${pinned} — ` +
|
|
1253
|
+
(result.kind === "missing"
|
|
1254
|
+
? `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`
|
|
1255
|
+
: `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`),
|
|
1256
|
+
`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)`,
|
|
1152
1257
|
);
|
|
1153
1258
|
}
|
|
1154
|
-
const claims =
|
|
1259
|
+
const claims = result.claims;
|
|
1155
1260
|
if (claims.length === 0) {
|
|
1156
1261
|
return passFinding(
|
|
1157
1262
|
"topic-pin",
|
|
1158
|
-
`[${p.name}] pinned topic ${pinned} — no live claims to compare (bridge
|
|
1263
|
+
`[${p.name}] pinned topic ${pinned} — no live claims to compare (the bridge has claimed no topics yet); the pin stands`,
|
|
1159
1264
|
);
|
|
1160
1265
|
}
|
|
1161
1266
|
if (claims.some((claim) => claim.threadId === pinned)) {
|
|
1162
1267
|
return passFinding("topic-pin", `[${p.name}] pinned topic ${pinned} is a live claim`);
|
|
1163
1268
|
}
|
|
1269
|
+
const match = resolveProjectClaim(claims, p.name);
|
|
1270
|
+
if (match.kind === "match") {
|
|
1271
|
+
return passFinding(
|
|
1272
|
+
"topic-pin",
|
|
1273
|
+
`[${p.name}] pinned topic ${pinned} is not among omp-telegram's live claims, but the ` +
|
|
1274
|
+
`${match.claim.workspaceLabel === p.name ? "herdr space claim" : "claim"} for this project ` +
|
|
1275
|
+
`(topic ${match.claim.threadId}) is live — sends follow it, so topic routing is healthy`,
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
if (match.kind === "ambiguous") {
|
|
1279
|
+
// Several live claims answer to the project, so substitution would be a
|
|
1280
|
+
// coin toss and sends keep the stale pin. This is not the no-answer case:
|
|
1281
|
+
// a claim IS waiting for the project, and naming the claimants tells the
|
|
1282
|
+
// operator exactly which panes to disambiguate (#626).
|
|
1283
|
+
return warnFinding(
|
|
1284
|
+
"topic-pin",
|
|
1285
|
+
`[${p.name}] pinned topic ${pinned} is not among omp-telegram's live claims, and several live claims answer to this project ` +
|
|
1286
|
+
`(topics ${match.claimants.map((c) => c.threadId).join(", ")}) — substitution would be a coin toss, so sends keep the stale pin`,
|
|
1287
|
+
`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`,
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1164
1290
|
if (probes.topicsTidy()) {
|
|
1165
1291
|
return warnFinding(
|
|
1166
1292
|
"topic-pin",
|
|
1167
|
-
`[${p.name}] pinned topic ${pinned} is not among omp-telegram's live claims,
|
|
1168
|
-
`the pin is vestigial and can never be live again
|
|
1169
|
-
|
|
1293
|
+
`[${p.name}] pinned topic ${pinned} is not among omp-telegram's live claims, no live claim answers ` +
|
|
1294
|
+
`to this project, and this host runs topicsTidy — the pin is vestigial and can never be live again ` +
|
|
1295
|
+
`(every pane exit closes its topic); sends keep the stale pin`,
|
|
1296
|
+
`turn topicsTidy off, then re-run setup to re-pin escalation.telegramTopicId to a currently claimed topic`,
|
|
1170
1297
|
);
|
|
1171
1298
|
}
|
|
1172
1299
|
return warnFinding(
|
|
1173
1300
|
"topic-pin",
|
|
1174
|
-
`[${p.name}] pinned topic ${pinned} is not among omp-telegram's live claims (${claims.map((c) => c.threadId).join(", ")})
|
|
1175
|
-
`
|
|
1176
|
-
`update
|
|
1301
|
+
`[${p.name}] pinned topic ${pinned} is not among omp-telegram's live claims (${claims.map((c) => c.threadId).join(", ")}), ` +
|
|
1302
|
+
`and no live claim answers to this project — sends keep the stale pin and degrade to the flat chat`,
|
|
1303
|
+
`update escalation.telegramTopicId to a currently claimed topic, or re-run setup to re-pin`,
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
/**
|
|
1308
|
+
* #612/#636 check — whether omp-telegram plumbing is live, *before* the arm
|
|
1309
|
+
* challenge, on the route the challenge actually rides. Read-only — the state
|
|
1310
|
+
* files and liveness probes, no `getUpdates`, no transcript scan, no network.
|
|
1311
|
+
* Renders the shared verdict as its own finding so a dead recipient or dead
|
|
1312
|
+
* poll is immediate and named, not discovered 300 seconds into an arm
|
|
1313
|
+
* challenge.
|
|
1314
|
+
*
|
|
1315
|
+
* Gated on the project's *real* inbound requirement, never on
|
|
1316
|
+
* `escalation.telegramTopicId` (an outbound send destination, not the inbound
|
|
1317
|
+
* shape) and never on `escalation.telegramChatId`, which `armTicks` ignores —
|
|
1318
|
+
* it sends the challenge to the paired owner from the tick's `accessFile`.
|
|
1319
|
+
* Only a project that can arm — a readable tick config naming an `accessFile`
|
|
1320
|
+
* — rides inbound at all, so only that shape runs the verdict: an armed
|
|
1321
|
+
* project with no chat id must still be verified, and a manually pinned
|
|
1322
|
+
* outbound-only target must not fail for absent claim/poll state. The route
|
|
1323
|
+
* is the forum topic the send would use (`resolveProjectTopicId` — the pin,
|
|
1324
|
+
* or the live claim a stale pin substitutes) when one resolves; with no topic
|
|
1325
|
+
* the challenge is unthreaded and the reply routes through the bridge's DM
|
|
1326
|
+
* owner, or — after `/telegram own clear` — the polling session itself. The
|
|
1327
|
+
* finding always renders, so the stable report shape and exit code are
|
|
1328
|
+
* unchanged.
|
|
1329
|
+
*/
|
|
1330
|
+
function telegramPlumbingProbe(probes: Probes, p: ProjectConfig): Finding {
|
|
1331
|
+
if (!probes.projectArms(p)) {
|
|
1332
|
+
return passFinding(
|
|
1333
|
+
"telegram-plumbing",
|
|
1334
|
+
`[${p.name}] outbound-only paging — no tick accessFile to arm, so no live claim, DM owner or poll ownership is required`,
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
const verdict = telegramPlumbingVerdict(
|
|
1338
|
+
probes.armSendTopic(p),
|
|
1339
|
+
{ dirs: probes.armScanDirs(p) },
|
|
1340
|
+
{
|
|
1341
|
+
channel: probes.channelState(p),
|
|
1342
|
+
registry: probes.claimedTopics(),
|
|
1343
|
+
poll: probes.pollState(),
|
|
1344
|
+
dmOwner: probes.dmOwner(),
|
|
1345
|
+
alive: probes.pidAlive,
|
|
1346
|
+
lockAlive: probes.lockPidAlive,
|
|
1347
|
+
fresh: probes.lockFresh,
|
|
1348
|
+
},
|
|
1177
1349
|
);
|
|
1350
|
+
if (verdict.ok) {
|
|
1351
|
+
const named = verdict.proof.includes("claim")
|
|
1352
|
+
? "verified live claim + live poll"
|
|
1353
|
+
: verdict.proof.includes("dm-owner")
|
|
1354
|
+
? "verified live dm-owner + live poll"
|
|
1355
|
+
: "verified live local poller session";
|
|
1356
|
+
return passFinding(
|
|
1357
|
+
"telegram-plumbing",
|
|
1358
|
+
`[${p.name}] live omp-telegram plumbing: ${named} (${verdict.proof.join(" + ")})`,
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
switch (verdict.reason) {
|
|
1362
|
+
case "channel-down": {
|
|
1363
|
+
const channel = probes.channelState(p);
|
|
1364
|
+
const reason = channel.kind === "down" ? channel.reason : "unexpected channel state";
|
|
1365
|
+
return failFinding(
|
|
1366
|
+
"telegram-plumbing",
|
|
1367
|
+
`[${p.name}] the tick's accessFile does not pair an enabled bridge (${reason}) — an arm challenge could not be sent`,
|
|
1368
|
+
`pair the bot (/telegram pair) and enable the bridge (/telegram on), then re-run doctor`,
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
case "registry-unreadable":
|
|
1372
|
+
return failFinding(
|
|
1373
|
+
"telegram-plumbing",
|
|
1374
|
+
`[${p.name}] omp-telegram's threads.json is absent, unreadable or malformed — the pinned-or-claimed topic's liveness cannot be verified`,
|
|
1375
|
+
`check the bridge state dir for a valid threads.json (the bridge writes it when a forum pane claims a topic)`,
|
|
1376
|
+
);
|
|
1377
|
+
case "no-topic-claim":
|
|
1378
|
+
return failFinding(
|
|
1379
|
+
"telegram-plumbing",
|
|
1380
|
+
`[${p.name}] no live omp-telegram claim carries the topic arming would send into — the pin is stale and no live claim takes its place`,
|
|
1381
|
+
`re-pin escalation.telegramTopicId to a currently claimed topic (or remove it to route the challenge flat), then re-run doctor`,
|
|
1382
|
+
);
|
|
1383
|
+
case "claim-dead":
|
|
1384
|
+
return failFinding(
|
|
1385
|
+
"telegram-plumbing",
|
|
1386
|
+
`[${p.name}] the topic's omp-telegram claim records no live pid — the pane behind it is gone`,
|
|
1387
|
+
`restart the claiming pane so the bridge re-claims it with a live pid`,
|
|
1388
|
+
);
|
|
1389
|
+
case "claim-session-outside":
|
|
1390
|
+
return failFinding(
|
|
1391
|
+
"telegram-plumbing",
|
|
1392
|
+
`[${p.name}] the topic's claim names a session file outside the session directories arm scans — the challenge reply could never be seen`,
|
|
1393
|
+
`resume the claiming pane's session under ${sessionsRoot()}, or from the tick cwd ${sessionDirForCwd(tickCwdForProject(p))}, then re-run doctor`,
|
|
1394
|
+
);
|
|
1395
|
+
case "no-dm-owner":
|
|
1396
|
+
return failFinding(
|
|
1397
|
+
"telegram-plumbing",
|
|
1398
|
+
`[${p.name}] no flat-chat challenge could pass: no dm-owner.json, and bot.lock names no session inside the arm scan surface — the private reply has no recipient arm can see`,
|
|
1399
|
+
`pin the orchestrator session as the DM owner (/telegram own in that pane), or pin escalation.telegramTopicId so the challenge rides a claimed forum topic`,
|
|
1400
|
+
);
|
|
1401
|
+
case "dm-owner-dead":
|
|
1402
|
+
return failFinding(
|
|
1403
|
+
"telegram-plumbing",
|
|
1404
|
+
`[${p.name}] omp-telegram's dm-owner.json records no live pid — the pane that receives flat replies is gone`,
|
|
1405
|
+
`restart the pane that owns dm-owner.json so the bridge re-pins it with a live pid`,
|
|
1406
|
+
);
|
|
1407
|
+
case "dm-owner-unrelated":
|
|
1408
|
+
return failFinding(
|
|
1409
|
+
"telegram-plumbing",
|
|
1410
|
+
`[${p.name}] the dm-owner.json session sits outside the arm scan surface — a flat reply would land in a transcript no challenge can read`,
|
|
1411
|
+
`pin the orchestrator session as the DM owner (/telegram own in this project's pane), or pin escalation.telegramTopicId so the challenge rides a claimed forum topic`,
|
|
1412
|
+
);
|
|
1413
|
+
case "no-poller-state":
|
|
1414
|
+
return failFinding(
|
|
1415
|
+
"telegram-plumbing",
|
|
1416
|
+
`[${p.name}] omp-telegram's bot.lock is absent, unreadable or malformed — nothing owns the poll, so nothing is receiving inbound`,
|
|
1417
|
+
`check the bridge state dir for a bot.lock held by a live poller (the daemon releases it on a getMe or poller failure)`,
|
|
1418
|
+
);
|
|
1419
|
+
case "poller-dead":
|
|
1420
|
+
return failFinding(
|
|
1421
|
+
"telegram-plumbing",
|
|
1422
|
+
`[${p.name}] omp-telegram's bot.lock records no live owner and no fresh heartbeat — Telegram isn't being polled`,
|
|
1423
|
+
`restart the omp-telegram bridge so a poller re-acquires bot.lock`,
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1178
1426
|
}
|
|
1179
1427
|
|
|
1180
1428
|
/**
|
|
@@ -1375,6 +1623,9 @@ export async function runDoctor(projectName: string | undefined, opts: DoctorDep
|
|
|
1375
1623
|
findings.push(
|
|
1376
1624
|
passFinding("topic-pin", projectProblem === undefined ? "no project resolved — nothing to check" : `topic pin uncheckable: ${projectProblem}`),
|
|
1377
1625
|
);
|
|
1626
|
+
findings.push(
|
|
1627
|
+
passFinding("telegram-plumbing", projectProblem === undefined ? "no project resolved — nothing to check" : `telegram plumbing uncheckable: ${projectProblem}`),
|
|
1628
|
+
);
|
|
1378
1629
|
findings.push(
|
|
1379
1630
|
passFinding("omp-settings", projectProblem === undefined ? "no project resolved — nothing to check" : `omp settings overlay uncheckable: ${projectProblem}`),
|
|
1380
1631
|
);
|
|
@@ -1384,6 +1635,7 @@ export async function runDoctor(projectName: string | undefined, opts: DoctorDep
|
|
|
1384
1635
|
findings.push(herdrAgentNameProbe(probes, p));
|
|
1385
1636
|
findings.push(armSessionDirProbe(probes, p));
|
|
1386
1637
|
findings.push(topicPinProbe(probes, p));
|
|
1638
|
+
findings.push(telegramPlumbingProbe(probes, p));
|
|
1387
1639
|
findings.push(ompSettingsProbe(p, sessionRootState));
|
|
1388
1640
|
}
|
|
1389
1641
|
}
|
|
@@ -1450,6 +1702,42 @@ export function defaultProbes(): Probes {
|
|
|
1450
1702
|
herdrEnv: defaultHerdrEnv,
|
|
1451
1703
|
claimedTopics: () => claimedTelegramTopics(),
|
|
1452
1704
|
topicsTidy: () => telegramTopicsTidy(),
|
|
1705
|
+
pollState: () => readTelegramPollState(),
|
|
1706
|
+
dmOwner: () => readTelegramDmOwner(),
|
|
1707
|
+
channelState: (p) => {
|
|
1708
|
+
// The exact tick accessFile armTicks probes — never the state-dir
|
|
1709
|
+
// default, so a project whose tick pairs a different bridge is judged
|
|
1710
|
+
// against its own file.
|
|
1711
|
+
const tick = resolveTickConfig(p.name);
|
|
1712
|
+
if (tick.kind !== "ok" || tick.config.accessFile === undefined) {
|
|
1713
|
+
return { kind: "down", reason: "no tick accessFile" };
|
|
1714
|
+
}
|
|
1715
|
+
return readTelegramChannel(tick.config.accessFile);
|
|
1716
|
+
},
|
|
1717
|
+
projectArms: (p) => {
|
|
1718
|
+
// A tick config only carries inbound when it names an accessFile: the
|
|
1719
|
+
// tick runtime treats a missing one as no fleet bridge (and armTicks
|
|
1720
|
+
// throws without it), so a parseable config with none is no more inbound
|
|
1721
|
+
// than no config at all.
|
|
1722
|
+
const tick = resolveTickConfig(p.name);
|
|
1723
|
+
return tick.kind === "ok" && tick.config.accessFile !== undefined;
|
|
1724
|
+
},
|
|
1725
|
+
armSendTopic: (p) => resolveProjectTopicId(p),
|
|
1726
|
+
armScanDirs: (p) => {
|
|
1727
|
+
// The same session surface armTicks builds from the tick cwd and the
|
|
1728
|
+
// live claim's file: the cwd-derived session directory, plus the claim's
|
|
1729
|
+
// own directory when resume moved the transcript elsewhere (#600).
|
|
1730
|
+
const cwdDir = sessionDirForCwd(tickCwdForProject(p));
|
|
1731
|
+
const claimed = resolveClaimedSessionFile(p);
|
|
1732
|
+
if (claimed === undefined) return [cwdDir];
|
|
1733
|
+
const root = sessionsRoot();
|
|
1734
|
+
const claimDir = dirname(claimed);
|
|
1735
|
+
if (claimDir !== root && !claimDir.startsWith(join(root, sep))) return [cwdDir];
|
|
1736
|
+
return claimDir === cwdDir ? [cwdDir] : [cwdDir, claimDir];
|
|
1737
|
+
},
|
|
1738
|
+
pidAlive,
|
|
1739
|
+
lockPidAlive,
|
|
1740
|
+
lockFresh: (mtimeMs) => Date.now() - mtimeMs < TELEGRAM_LOCK_FRESH_MS,
|
|
1453
1741
|
tickAgentName: (p) => {
|
|
1454
1742
|
const tick = readTickConfig(tickCwdForProject(p));
|
|
1455
1743
|
if (tick.kind !== "ok") return undefined;
|