omp-conductor 0.15.9 → 0.15.10
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 +4 -3
- package/package.json +1 -1
- package/src/escalate.ts +58 -17
- package/src/setup-wizard.ts +6 -1
package/README.md
CHANGED
|
@@ -1499,8 +1499,9 @@ 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
1500
|
`escalation.telegramTopicId`. A flat-chat project is unaffected. When a pinned
|
|
1501
1501
|
topic id has gone stale — the bridge re-claims pane topics across restarts — the
|
|
1502
|
-
live claim
|
|
1503
|
-
move every page into
|
|
1502
|
+
live claim for the project's herdr space is used instead, falling back to a
|
|
1503
|
+
claim titled for the project, so a restart does not quietly move every page into
|
|
1504
|
+
the main chat (#407, #412).
|
|
1504
1505
|
|
|
1505
1506
|
A locally injected tick has no inbound message to inherit a topic from, so a
|
|
1506
1507
|
bare `telegram_send` there has nothing to preserve. That turn addresses the
|
|
@@ -1927,7 +1928,7 @@ Field notes:
|
|
|
1927
1928
|
| `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. |
|
|
1928
1929
|
| `caps` | Per-project overrides; omit it or pin only the fields you want to change. |
|
|
1929
1930
|
| `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
|
|
1931
|
+
| `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, naming each one's herdr space. 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 **herdr space** is this project, falling back to one titled for the project, logged without ids (#407, #412). The space is read first because the bridge titles a topic `ownAgentName ?? basename(cwd)`, and a multi-project host whose panes sit under one state directory gives every claim the same title. An identity two claims share is treated as no match at all rather than a guess. A pin that is still claimed always wins, so a deliberately separate topic is never hijacked. Absent keeps flat-chat behaviour. |
|
|
1931
1932
|
| `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. |
|
|
1932
1933
|
| `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. |
|
|
1933
1934
|
| `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.10",
|
|
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/escalate.ts
CHANGED
|
@@ -350,6 +350,15 @@ export function readTelegramToken(): string | undefined {
|
|
|
350
350
|
return undefined;
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
+
/** One live omp-telegram topic claim, as much of it as conductor reads. */
|
|
354
|
+
export interface ClaimedTopic {
|
|
355
|
+
threadId: number;
|
|
356
|
+
/** The bridge's own title for the claim. */
|
|
357
|
+
name: string;
|
|
358
|
+
/** The herdr space the claiming pane sits in, when the bridge captured one. */
|
|
359
|
+
workspaceLabel?: string;
|
|
360
|
+
}
|
|
361
|
+
|
|
353
362
|
/**
|
|
354
363
|
* The forum topic a project's Telegram actually lives in *now*.
|
|
355
364
|
*
|
|
@@ -362,12 +371,19 @@ export function readTelegramToken(): string | undefined {
|
|
|
362
371
|
*
|
|
363
372
|
* The operator's pin still wins whenever it is live. Only a pin that is
|
|
364
373
|
* *provably* absent from the current claims is replaced, and only by the claim
|
|
365
|
-
*
|
|
366
|
-
* never hijacked by the pane's own thread. Unreadable bridge state changes
|
|
374
|
+
* this project can be identified with — so a deliberately separate alerts topic
|
|
375
|
+
* is never hijacked by the pane's own thread. Unreadable bridge state changes
|
|
367
376
|
* nothing, and #318's stale-topic retry remains the last line of defence.
|
|
368
377
|
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
378
|
+
* Identity is read from the herdr space first, and only then from the claim's
|
|
379
|
+
* title. The bridge titles a topic `ownAgentName ?? basename(cwd)`, and both
|
|
380
|
+
* halves of that fall down on a real multi-project host: the agent-name lookup
|
|
381
|
+
* is best-effort, and a fleet whose panes live under one state directory gives
|
|
382
|
+
* every pane the same basename. `workspaceLabel` is the herdr space, which is
|
|
383
|
+
* one-to-one with the project, so it is the identity that survives a re-claim.
|
|
384
|
+
*
|
|
385
|
+
* The substitution is logged, naming the project and which identity answered.
|
|
386
|
+
* Never an id: a log line is a place these leak from.
|
|
371
387
|
*/
|
|
372
388
|
export function resolveProjectTopicId(project: ProjectConfig): number | undefined {
|
|
373
389
|
const pinned = project.escalation.telegramTopicId;
|
|
@@ -375,24 +391,42 @@ export function resolveProjectTopicId(project: ProjectConfig): number | undefine
|
|
|
375
391
|
const claims = claimedTelegramTopics();
|
|
376
392
|
if (claims.length === 0) return pinned;
|
|
377
393
|
if (claims.some((claim) => claim.threadId === pinned)) return pinned;
|
|
378
|
-
const
|
|
379
|
-
|
|
394
|
+
const bySpace = soleClaim(claims, (claim) => claim.workspaceLabel === project.name);
|
|
395
|
+
const match = bySpace ?? soleClaim(claims, (claim) => claim.name === project.name);
|
|
396
|
+
if (match === undefined) return pinned;
|
|
380
397
|
warn(
|
|
381
398
|
`escalation.telegramTopicId for ${project.name} is no longer a claimed topic; ` +
|
|
382
|
-
`following omp-telegram's current "${
|
|
399
|
+
`following omp-telegram's current "${project.name}" ` +
|
|
400
|
+
`${bySpace === undefined ? "claim" : "herdr space claim"} instead`,
|
|
383
401
|
);
|
|
384
|
-
return
|
|
402
|
+
return match.threadId;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* The one claim answering to `predicate`, or nothing when none or several do.
|
|
407
|
+
*
|
|
408
|
+
* Ambiguity is not a coin toss: two claims wearing the same identity means the
|
|
409
|
+
* bridge cannot say which pane belongs to this project, and paging the wrong
|
|
410
|
+
* project's topic is worse than the flat-chat degrade #318 already handles.
|
|
411
|
+
*/
|
|
412
|
+
function soleClaim(
|
|
413
|
+
claims: readonly ClaimedTopic[],
|
|
414
|
+
predicate: (claim: ClaimedTopic) => boolean,
|
|
415
|
+
): ClaimedTopic | undefined {
|
|
416
|
+
const hits = claims.filter(predicate);
|
|
417
|
+
return hits.length === 1 ? hits[0] : undefined;
|
|
385
418
|
}
|
|
386
419
|
|
|
387
420
|
/**
|
|
388
|
-
* omp-telegram's live topic claims
|
|
389
|
-
*
|
|
390
|
-
*
|
|
421
|
+
* omp-telegram's live topic claims, from
|
|
422
|
+
* `{ threads: { "<threadId>": { name, workspaceLabel } } }`. Borrowed exactly as
|
|
423
|
+
* the token is, and tolerant of every shape it might not be — a bridge that has
|
|
424
|
+
* never run a forum pane has no file at all.
|
|
391
425
|
*
|
|
392
426
|
* `stateDir` is passed by the setup wizard, which has already probed for the
|
|
393
427
|
* bridge; send-time callers let it resolve the same way the token does.
|
|
394
428
|
*/
|
|
395
|
-
export function claimedTelegramTopics(stateDir?: string):
|
|
429
|
+
export function claimedTelegramTopics(stateDir?: string): ClaimedTopic[] {
|
|
396
430
|
const override = stateDir?.trim() ?? process.env.OMP_TELEGRAM_STATE_DIR?.trim();
|
|
397
431
|
const dir = override ? override : join(homedir(), ".omp", "agent", "telegram");
|
|
398
432
|
let raw: unknown;
|
|
@@ -404,16 +438,23 @@ export function claimedTelegramTopics(stateDir?: string): Array<{ threadId: numb
|
|
|
404
438
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw) || !("threads" in raw)) return [];
|
|
405
439
|
const threads = raw.threads;
|
|
406
440
|
if (typeof threads !== "object" || threads === null || Array.isArray(threads)) return [];
|
|
407
|
-
const out:
|
|
441
|
+
const out: ClaimedTopic[] = [];
|
|
408
442
|
for (const [id, entry] of Object.entries(threads)) {
|
|
409
443
|
const threadId = Number(id);
|
|
410
444
|
if (!Number.isFinite(threadId) || !Number.isSafeInteger(threadId)) continue;
|
|
411
445
|
let name = id;
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
if (
|
|
446
|
+
let workspaceLabel: string | undefined;
|
|
447
|
+
if (typeof entry === "object" && entry !== null) {
|
|
448
|
+
if ("name" in entry) {
|
|
449
|
+
const candidate = entry.name;
|
|
450
|
+
if (typeof candidate === "string" && candidate.trim() !== "") name = candidate.trim();
|
|
451
|
+
}
|
|
452
|
+
if ("workspaceLabel" in entry) {
|
|
453
|
+
const candidate = entry.workspaceLabel;
|
|
454
|
+
if (typeof candidate === "string" && candidate.trim() !== "") workspaceLabel = candidate.trim();
|
|
455
|
+
}
|
|
415
456
|
}
|
|
416
|
-
out.push({ threadId, name });
|
|
457
|
+
out.push(workspaceLabel === undefined ? { threadId, name } : { threadId, name, workspaceLabel });
|
|
417
458
|
}
|
|
418
459
|
return out;
|
|
419
460
|
}
|
package/src/setup-wizard.ts
CHANGED
|
@@ -1216,7 +1216,12 @@ async function askTelegramTopicId(
|
|
|
1216
1216
|
const options = [
|
|
1217
1217
|
...claimed.map((t) => ({
|
|
1218
1218
|
label: `${t.name} — ${t.threadId}`,
|
|
1219
|
-
|
|
1219
|
+
// Two panes under one state directory claim the same title, so the
|
|
1220
|
+
// herdr space is the only thing that tells the operator which is which.
|
|
1221
|
+
description:
|
|
1222
|
+
t.workspaceLabel === undefined || t.workspaceLabel === t.name
|
|
1223
|
+
? `message_thread_id ${t.threadId}`
|
|
1224
|
+
: `herdr space ${t.workspaceLabel} · message_thread_id ${t.threadId}`,
|
|
1220
1225
|
})),
|
|
1221
1226
|
{ label: manual, description: "type a numeric forum topic id" },
|
|
1222
1227
|
{ label: none, description: "send to the chat root, not a topic" },
|