omp-conductor 0.15.7 → 0.15.8
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 +5 -1
- package/package.json +1 -1
- package/src/cli.ts +3 -2
- package/src/escalate.ts +69 -1
- package/src/fleet.ts +5 -4
- package/src/reports.ts +2 -2
- package/src/setup-wizard.ts +2 -24
package/README.md
CHANGED
|
@@ -1497,7 +1497,10 @@ omitted — so an orchestrator that helpfully supplied `chat_id` (and nothing
|
|
|
1497
1497
|
else) answered three topic messages in the main chat instead (#366). The
|
|
1498
1498
|
orchestrator floor now says to name **neither** target or **both**, and the
|
|
1499
1499
|
dispatcher refuses `chat_id` without `thread_id` for a project that configured
|
|
1500
|
-
`escalation.telegramTopicId`. A flat-chat project is unaffected.
|
|
1500
|
+
`escalation.telegramTopicId`. A flat-chat project is unaffected. When a pinned
|
|
1501
|
+
topic id has gone stale — the bridge re-claims pane topics across restarts — the
|
|
1502
|
+
live claim named for the project is used instead, so a restart does not quietly
|
|
1503
|
+
move every page into the main chat (#407).
|
|
1501
1504
|
|
|
1502
1505
|
A locally injected tick has no inbound message to inherit a topic from, so a
|
|
1503
1506
|
bare `telegram_send` there has nothing to preserve. That turn addresses the
|
|
@@ -1924,6 +1927,7 @@ Field notes:
|
|
|
1924
1927
|
| `migrations` | Optional, per repo: `{ "dir": "backend/alembic/versions" }`. Names the repo-relative directory of an Alembic-style ordered migration chain (`revision` / `down_revision` in `*.py`). When set, `conductor_pr_merge` **refuses** a merge that would corrupt the chain at the base tip: reusing a revision id another file already declares, deleting a published migration, or a merge that would leave the combined graph with more than one head (so a stale parent is refused, and a fork-repair merge migration that unifies the heads passes). Absent means the repo opts out of the chain check entirely. Repo-relative only: a leading `/` or `..` is an error. |
|
|
1925
1928
|
| `caps` | Per-project overrides; omit it or pin only the fields you want to change. |
|
|
1926
1929
|
| `escalation.fallbackToIssueComment` | Defaults to `true`. Absent means "yes, still tell me". |
|
|
1930
|
+
| `escalation.telegramTopicId` | Optional forum topic for everything conductor sends: tier-2 pages, reports, digests, arm challenges, `omp-conductor message`. Setup offers the topics omp-telegram has claimed. The bridge re-claims a pane's topic across restarts — including the restarts `upgrade` and `restart` perform — so a pinned id that is no longer claimed is replaced at send time by the live claim whose name matches the project, logged without ids (#407). A pin that is still claimed always wins, so a deliberately separate topic is never hijacked. Absent keeps flat-chat behaviour. |
|
|
1927
1931
|
| `escalation.orchestrator` | Optional; `"embedded"` (default) or `"external"`. `external` means an orchestrator session already runs elsewhere: the daemon starts none, and tier-1 escalations post as issue comments for that session to drain. Any other value is an error. |
|
|
1928
1932
|
| `authority` | Optional; `{ "merge": …, "release": … }`, each `"human"` (default) or `"orchestrator"`. It grants nothing to the daemon — it words the orchestrator's standing orders and the Releases paragraph of the rendered brief, so the config and the prompt cannot disagree about who holds the merge button. Unknown keys and any other value are errors, never folded to the default. |
|
|
1929
1933
|
| `releasePolicy` | Optional; a per-shape map whose values are `"human"` (default) or `"orchestrator"`. Shapes are `version-bump-pr`, `git-tag`, `git-push-tags`, `package-publish`, `github-release`, and `deploy`. The legacy `"none"` denies every shape; legacy `"operator-brief"` grants the artifact-producing shapes, including reviewed version preparation, but keeps deploy human-owned. The in-session tripwire blocks recognised raw release/deploy calls before execution. Every rejection is written to `release-policy-blocks.jsonl`; the heartbeat carries that day's count into the daily digest. This is the mechanical gate; `authority.release` still says who owns the decision. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omp-conductor",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "A 24/7 dispatcher that takes ready GitHub issues to green, mergeable PRs using omp coding sessions, with tiered escalation first to an orchestrator session and then to a human.",
|
package/src/cli.ts
CHANGED
|
@@ -1367,8 +1367,9 @@ try {
|
|
|
1367
1367
|
}
|
|
1368
1368
|
process.stdout.write(
|
|
1369
1369
|
outcome.kind === "sent"
|
|
1370
|
-
?
|
|
1371
|
-
|
|
1370
|
+
? // Not "into the topic": a stale topic degrades to the flat chat with
|
|
1371
|
+
// its own warning on stderr, and this line must not contradict it.
|
|
1372
|
+
`message delivered to ${project.name}'s configured Telegram target (${outcome.category})\n`
|
|
1372
1373
|
: `held notice ${outcome.noticeId} queued for ${project.name} (${outcome.category}; ` +
|
|
1373
1374
|
`${outcome.reason === "availability" ? "outside the availability window" : "a digest-only category"})\n` +
|
|
1374
1375
|
"nothing was sent; the daemon releases it with the next digest or working-hours catch-up\n",
|
package/src/escalate.ts
CHANGED
|
@@ -269,7 +269,7 @@ export function createEscalator(
|
|
|
269
269
|
// the tick re-raises it. A *report* has no such source — it exists
|
|
270
270
|
// once, in the model's head, and nothing regenerates it — which is
|
|
271
271
|
// why that one needed a ledger and this one does not.
|
|
272
|
-
await sendTelegram(token, chatId, text, { topicId: p
|
|
272
|
+
await sendTelegram(token, chatId, text, { topicId: resolveProjectTopicId(p) });
|
|
273
273
|
store.markNotified(key);
|
|
274
274
|
return;
|
|
275
275
|
}
|
|
@@ -350,6 +350,74 @@ export function readTelegramToken(): string | undefined {
|
|
|
350
350
|
return undefined;
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
+
/**
|
|
354
|
+
* The forum topic a project's Telegram actually lives in *now*.
|
|
355
|
+
*
|
|
356
|
+
* `escalation.telegramTopicId` is pinned by the wizard from the topics
|
|
357
|
+
* omp-telegram had claimed at setup time — but the bridge re-claims a pane's
|
|
358
|
+
* topic across restarts, and conductor restarts that service itself during
|
|
359
|
+
* `upgrade` and `restart`. The pinned id therefore goes stale on the fleet's
|
|
360
|
+
* own maintenance, and every topic-addressed send silently degrades to the main
|
|
361
|
+
* chat: tier-2 pages, reports, digests, arm challenges, direct messages (#407).
|
|
362
|
+
*
|
|
363
|
+
* The operator's pin still wins whenever it is live. Only a pin that is
|
|
364
|
+
* *provably* absent from the current claims is replaced, and only by the claim
|
|
365
|
+
* whose name is this project's — so a deliberately separate alerts topic is
|
|
366
|
+
* never hijacked by the pane's own thread. Unreadable bridge state changes
|
|
367
|
+
* nothing, and #318's stale-topic retry remains the last line of defence.
|
|
368
|
+
*
|
|
369
|
+
* The substitution is logged, naming the project and the claim's name. Never an
|
|
370
|
+
* id: a log line is a place these leak from.
|
|
371
|
+
*/
|
|
372
|
+
export function resolveProjectTopicId(project: ProjectConfig): number | undefined {
|
|
373
|
+
const pinned = project.escalation.telegramTopicId;
|
|
374
|
+
if (pinned === undefined) return undefined;
|
|
375
|
+
const claims = claimedTelegramTopics();
|
|
376
|
+
if (claims.length === 0) return pinned;
|
|
377
|
+
if (claims.some((claim) => claim.threadId === pinned)) return pinned;
|
|
378
|
+
const named = claims.find((claim) => claim.name === project.name);
|
|
379
|
+
if (named === undefined) return pinned;
|
|
380
|
+
warn(
|
|
381
|
+
`escalation.telegramTopicId for ${project.name} is no longer a claimed topic; ` +
|
|
382
|
+
`following omp-telegram's current "${named.name}" claim instead`,
|
|
383
|
+
);
|
|
384
|
+
return named.threadId;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* omp-telegram's live topic claims: `{ threads: { "<threadId>": { name } } }`.
|
|
389
|
+
* Borrowed exactly as the token is, and tolerant of every shape it might not
|
|
390
|
+
* be — a bridge that has never run a forum pane has no file at all.
|
|
391
|
+
*
|
|
392
|
+
* `stateDir` is passed by the setup wizard, which has already probed for the
|
|
393
|
+
* bridge; send-time callers let it resolve the same way the token does.
|
|
394
|
+
*/
|
|
395
|
+
export function claimedTelegramTopics(stateDir?: string): Array<{ threadId: number; name: string }> {
|
|
396
|
+
const override = stateDir?.trim() ?? process.env.OMP_TELEGRAM_STATE_DIR?.trim();
|
|
397
|
+
const dir = override ? override : join(homedir(), ".omp", "agent", "telegram");
|
|
398
|
+
let raw: unknown;
|
|
399
|
+
try {
|
|
400
|
+
raw = JSON.parse(readFileSync(join(dir, "threads.json"), "utf8"));
|
|
401
|
+
} catch {
|
|
402
|
+
return [];
|
|
403
|
+
}
|
|
404
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw) || !("threads" in raw)) return [];
|
|
405
|
+
const threads = raw.threads;
|
|
406
|
+
if (typeof threads !== "object" || threads === null || Array.isArray(threads)) return [];
|
|
407
|
+
const out: Array<{ threadId: number; name: string }> = [];
|
|
408
|
+
for (const [id, entry] of Object.entries(threads)) {
|
|
409
|
+
const threadId = Number(id);
|
|
410
|
+
if (!Number.isFinite(threadId) || !Number.isSafeInteger(threadId)) continue;
|
|
411
|
+
let name = id;
|
|
412
|
+
if (typeof entry === "object" && entry !== null && "name" in entry) {
|
|
413
|
+
const candidate = entry.name;
|
|
414
|
+
if (typeof candidate === "string" && candidate.trim() !== "") name = candidate.trim();
|
|
415
|
+
}
|
|
416
|
+
out.push({ threadId, name });
|
|
417
|
+
}
|
|
418
|
+
return out;
|
|
419
|
+
}
|
|
420
|
+
|
|
353
421
|
/**
|
|
354
422
|
* The one Telegram send in this package. Exported so the report outbox (#123)
|
|
355
423
|
* reuses it rather than forking it: the response handling below is load-bearing
|
package/src/fleet.ts
CHANGED
|
@@ -29,7 +29,7 @@ import { homedir } from "node:os";
|
|
|
29
29
|
import { dirname, join } from "node:path";
|
|
30
30
|
import { formatZonedMinute } from "./availability.ts";
|
|
31
31
|
import { findProject, loadConfig, stateDir } from "./config.ts";
|
|
32
|
-
import { sendTelegram } from "./escalate.ts";
|
|
32
|
+
import { resolveProjectTopicId, sendTelegram } from "./escalate.ts";
|
|
33
33
|
import { planUsageLine, readPlanUsage, sharedUsageSource } from "./usage.ts";
|
|
34
34
|
import { readApprovalSurface, readDaemonProfile } from "./approval-surface.ts";
|
|
35
35
|
import { inspectBriefLayout } from "./brief-upgrade.ts";
|
|
@@ -349,12 +349,13 @@ export async function armTicks(projectName?: string, deps: ArmDeps = {}): Promis
|
|
|
349
349
|
`Reply to this chat with exactly:\n${code}\n` +
|
|
350
350
|
`Nothing will be dispatched until that reply is seen in the orchestrator session.`;
|
|
351
351
|
|
|
352
|
-
// Prefer the project's
|
|
353
|
-
// escalations already do (#318)
|
|
352
|
+
// Prefer the project's live forum topic so arm challenges land where
|
|
353
|
+
// escalations already do (#318), following the bridge's current claim when the
|
|
354
|
+
// pinned id has gone stale (#407). Missing project config keeps flat-chat 0.13.
|
|
354
355
|
let topicId: number | undefined;
|
|
355
356
|
if (named !== undefined) {
|
|
356
357
|
try {
|
|
357
|
-
topicId = findProject(loadConfig(), named)
|
|
358
|
+
topicId = resolveProjectTopicId(findProject(loadConfig(), named));
|
|
358
359
|
} catch {
|
|
359
360
|
/* no project config */
|
|
360
361
|
}
|
package/src/reports.ts
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
39
|
import { availabilityDisposition, interruptDisposition } from "./availability.ts";
|
|
40
|
-
import { readTelegramToken, sendTelegram, TelegramSendError } from "./escalate.ts";
|
|
40
|
+
import { readTelegramToken, resolveProjectTopicId, sendTelegram, TelegramSendError } from "./escalate.ts";
|
|
41
41
|
import { localDayKey } from "./digest-schedule.ts";
|
|
42
42
|
import {
|
|
43
43
|
DIGEST_BACKLOG_LIMIT,
|
|
@@ -284,7 +284,7 @@ export function telegramReportSend(p: ProjectConfig): ReportSend {
|
|
|
284
284
|
"definitive",
|
|
285
285
|
);
|
|
286
286
|
}
|
|
287
|
-
return await sendTelegram(token, chatId, text, { topicId: p
|
|
287
|
+
return await sendTelegram(token, chatId, text, { topicId: resolveProjectTopicId(p) });
|
|
288
288
|
};
|
|
289
289
|
}
|
|
290
290
|
|
package/src/setup-wizard.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
resolveCaps,
|
|
22
22
|
saveConfig,
|
|
23
23
|
} from "./config.ts";
|
|
24
|
+
import { claimedTelegramTopics } from "./escalate.ts";
|
|
24
25
|
import { hostRamBytes, recommendedMaxWorkers, workerOvercommit } from "./host.ts";
|
|
25
26
|
import {
|
|
26
27
|
prepareConductor,
|
|
@@ -1208,7 +1209,7 @@ async function askTelegramTopicId(
|
|
|
1208
1209
|
stateDir: string,
|
|
1209
1210
|
prior: number | undefined,
|
|
1210
1211
|
): Promise<number | undefined> {
|
|
1211
|
-
const claimed =
|
|
1212
|
+
const claimed = claimedTelegramTopics(stateDir);
|
|
1212
1213
|
const manual = "Enter thread id manually";
|
|
1213
1214
|
const none = "None — flat chat (0.13 behaviour)";
|
|
1214
1215
|
if (claimed.length > 0) {
|
|
@@ -1246,29 +1247,6 @@ async function askTelegramTopicId(
|
|
|
1246
1247
|
return n;
|
|
1247
1248
|
}
|
|
1248
1249
|
|
|
1249
|
-
function readClaimedTelegramTopics(stateDir: string): Array<{ threadId: number; name: string }> {
|
|
1250
|
-
try {
|
|
1251
|
-
const raw: unknown = JSON.parse(readFileSync(join(stateDir, "threads.json"), "utf8"));
|
|
1252
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return [];
|
|
1253
|
-
if (!("threads" in raw)) return [];
|
|
1254
|
-
const threads = raw.threads;
|
|
1255
|
-
if (typeof threads !== "object" || threads === null || Array.isArray(threads)) return [];
|
|
1256
|
-
const out: Array<{ threadId: number; name: string }> = [];
|
|
1257
|
-
for (const [id, entry] of Object.entries(threads)) {
|
|
1258
|
-
const threadId = Number(id);
|
|
1259
|
-
if (!Number.isFinite(threadId) || !Number.isSafeInteger(threadId)) continue;
|
|
1260
|
-
let name = id;
|
|
1261
|
-
if (typeof entry === "object" && entry !== null && "name" in entry) {
|
|
1262
|
-
const candidate = entry.name;
|
|
1263
|
-
if (typeof candidate === "string" && candidate.trim() !== "") name = candidate.trim();
|
|
1264
|
-
}
|
|
1265
|
-
out.push({ threadId, name });
|
|
1266
|
-
}
|
|
1267
|
-
return out;
|
|
1268
|
-
} catch {
|
|
1269
|
-
return [];
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
1250
|
|
|
1273
1251
|
/** How loud the orchestrator is and when the operator permits interruptions. */
|
|
1274
1252
|
const askReporting: AreaAsker = async (ui, a) => {
|