omp-conductor 0.15.12 → 0.15.13
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/REFERENCE.md +9 -4
- package/package.json +1 -1
- package/src/commands/restart.ts +81 -54
- package/src/commands/stop.ts +45 -22
- package/src/daemon.ts +253 -54
- package/src/doctor.ts +241 -4
- package/src/escalate.ts +8 -0
- package/src/fleet.ts +49 -2
- package/src/lifecycle.ts +113 -2
- package/src/omp.ts +24 -0
- package/src/orchestrator-down.ts +231 -0
- package/src/orchestrator-tick.ts +7 -0
- package/src/orchestrator.ts +14 -0
- package/src/release-policy.ts +163 -18
- package/src/setup-host.ts +386 -17
- package/src/setup-install.ts +40 -2
- package/src/setup-wizard.ts +278 -113
- package/src/stop-provenance.ts +66 -0
- package/src/store.ts +181 -0
- package/src/types.ts +111 -0
- package/src/upgrade.ts +26 -6
- package/src/verbs/protocol.ts +16 -3
- package/src/verbs/server.ts +27 -1
- package/src/wizard-ui.ts +261 -46
- package/src/worker.ts +21 -0
package/REFERENCE.md
CHANGED
|
@@ -2633,10 +2633,15 @@ Known and deliberate in this version:
|
|
|
2633
2633
|
restart policy stays a visible detector rather than a silent spin. Prefer
|
|
2634
2634
|
`omp-conductor stop` / `systemctl stop` over raw `kill`: a raw kill is not an
|
|
2635
2635
|
intentional stop, and the daemon comes straight back.
|
|
2636
|
-
- **A failed orchestrator
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
is
|
|
2636
|
+
- **A failed orchestrator doesn't stay quiet.** Start failure and an
|
|
2637
|
+
unexpected session death each open one durable orchestrator-down incident
|
|
2638
|
+
that pages tier 2 once ("down since <t>, tier-1 escalations diverting to
|
|
2639
|
+
issue comments") and is shown as a degrade row by `omp-conductor status` —
|
|
2640
|
+
mode, since-when, and how many tier-1 escalations were diverted while it was
|
|
2641
|
+
down. The incident survives a daemon restart while still down (it is
|
|
2642
|
+
re-derived, not forgotten), and one closing notice lands when the orchestrator
|
|
2643
|
+
recovers. What remains a separate watchdog is the `.conductor-stalled` wedge
|
|
2644
|
+
path, for a session that stays alive but stops draining its queue.
|
|
2640
2645
|
- **Workers are not terminal panes, so you cannot watch them there.** Each
|
|
2641
2646
|
worker is an omp session the daemon starts as a child process. The resident
|
|
2642
2647
|
daemon tracks workers in a background pool so the five-minute loop keeps
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omp-conductor",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.13",
|
|
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/commands/restart.ts
CHANGED
|
@@ -7,70 +7,97 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { CommandContext } from "./context.ts";
|
|
10
|
+
import { buildStopProvenance } from "../stop-provenance.ts";
|
|
10
11
|
import { setPaused } from "../daemon.ts";
|
|
11
|
-
import { restartDaemon, type RestartResult } from "../lifecycle.ts";
|
|
12
|
+
import { restartDaemon, type RestartResult, type StopDelivery } from "../lifecycle.ts";
|
|
13
|
+
import { openStore, dbPath } from "../store.ts";
|
|
12
14
|
import { DEFAULT_DEPS, drainAndRestart, type UpgradeDeps } from "../upgrade.ts";
|
|
13
15
|
|
|
14
16
|
export async function restartCommand(ctx: CommandContext): Promise<void> {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
project: ctx.projectFlag,
|
|
17
|
+
const store = openStore(dbPath());
|
|
18
|
+
try {
|
|
19
|
+
// Provenance (#378): one record naming the request, every configured
|
|
20
|
+
// project with its live-run count, and — via the lifecycle chokepoint — the
|
|
21
|
+
// delivery method used. The shared daemon serves all projects, so a restart
|
|
22
|
+
// is never silent even when the operator narrows nothing.
|
|
23
|
+
const provenance = buildStopProvenance({
|
|
24
|
+
controlPath: "cli restart",
|
|
25
|
+
reason: "operator restart",
|
|
26
|
+
scope: "global",
|
|
26
27
|
});
|
|
27
|
-
|
|
28
|
-
process.stdout.write(
|
|
29
|
-
`stopped — pid ${previous.pid}${via === "systemctl" ? " (via systemctl)" : ""}\n`,
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
process.stdout.write(
|
|
33
|
-
`started — pid ${record.pid}, /healthz on :${record.port}` +
|
|
34
|
-
`${record.project === undefined ? "" : `, project ${record.project}`}` +
|
|
35
|
-
`${via === "systemctl" ? " (via systemctl)" : ""}\nlog ${record.logFile}\n`,
|
|
36
|
-
);
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
28
|
+
const record = (signal: StopDelivery) => store.recordDaemonStop(signal.provenance);
|
|
39
29
|
|
|
40
|
-
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const deps: UpgradeDeps = {
|
|
50
|
-
...DEFAULT_DEPS,
|
|
51
|
-
setPaused: (v, project) =>
|
|
52
|
-
setPaused(v, { source: "restart", reason: "restart, draining" }, project),
|
|
53
|
-
restartDaemon: async () => {
|
|
54
|
-
result = await restartDaemon({
|
|
30
|
+
if (ctx.argv.includes("--now")) {
|
|
31
|
+
// Inherit the running daemon's port and project: a restart that quietly
|
|
32
|
+
// moved to the default port would leave every existing health check
|
|
33
|
+
// pointing at nothing. When the unit owns the live pid, restartDaemon
|
|
34
|
+
// goes through systemctl so the replacement stays supervised; a failed
|
|
35
|
+
// installed unit is reset and started through systemd the same way and
|
|
36
|
+
// is never replaced by an unmanaged daemon. Success is only reported
|
|
37
|
+
// after the manager is proven to own the reported pid.
|
|
38
|
+
const { previous, record: newRecord, via } = await restartDaemon({
|
|
55
39
|
port: ctx.portFlag(),
|
|
56
40
|
project: ctx.projectFlag,
|
|
41
|
+
provenance,
|
|
42
|
+
record,
|
|
57
43
|
});
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
44
|
+
if (previous !== undefined) {
|
|
45
|
+
process.stdout.write(
|
|
46
|
+
`stopped — pid ${previous.pid}${via === "systemctl" ? " (via systemctl)" : ""}\n`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
process.stdout.write(
|
|
50
|
+
`started — pid ${newRecord.pid}, /healthz on :${newRecord.port}` +
|
|
51
|
+
`${newRecord.project === undefined ? "" : `, project ${newRecord.project}`}` +
|
|
52
|
+
`${via === "systemctl" ? " (via systemctl)" : ""}\nlog ${newRecord.logFile}\n`,
|
|
53
|
+
);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Default: drain the fleet before restarting — pause new claims, wait for
|
|
58
|
+
// live workers to reach 0/N (bounded by --timeout), restart, then restore
|
|
59
|
+
// the prior dispatch state. A timed-out drain restarts nothing and leaves
|
|
60
|
+
// dispatch paused, so it is safe to run while the daemon is wedged.
|
|
61
|
+
const timeoutRaw = ctx.flag("timeout");
|
|
62
|
+
const timeoutSeconds =
|
|
63
|
+
timeoutRaw === undefined ? 1800 : Number.parseInt(timeoutRaw, 10);
|
|
64
|
+
const timeoutMs = (Number.isNaN(timeoutSeconds) ? 1800 : timeoutSeconds) * 1000;
|
|
65
|
+
let result!: RestartResult;
|
|
66
|
+
const deps: UpgradeDeps = {
|
|
67
|
+
...DEFAULT_DEPS,
|
|
68
|
+
setPaused: (v, project) =>
|
|
69
|
+
setPaused(v, { source: "restart", reason: "restart, draining" }, project),
|
|
70
|
+
restartDaemon: async () => {
|
|
71
|
+
result = await restartDaemon({
|
|
72
|
+
port: ctx.portFlag(),
|
|
73
|
+
project: ctx.projectFlag,
|
|
74
|
+
provenance,
|
|
75
|
+
record,
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
// Record the request at ENTRY, before the drain: a caller that disconnects
|
|
80
|
+
// or hits --timeout while workers drain never reaches the lifecycle
|
|
81
|
+
// chokepoint, and the durable provenance must survive that — it is exactly
|
|
82
|
+
// the stop-with-no-witness incident #378 exists for (#378).
|
|
83
|
+
store.recordDaemonStop(provenance);
|
|
84
|
+
try {
|
|
85
|
+
await drainAndRestart(deps, { project: ctx.projectFlag, timeoutMs });
|
|
86
|
+
} catch (err) {
|
|
87
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
if (result.previous !== undefined) {
|
|
91
|
+
process.stdout.write(
|
|
92
|
+
`stopped — pid ${result.previous.pid}${result.via === "systemctl" ? " (via systemctl)" : ""}\n`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
67
95
|
process.stdout.write(
|
|
68
|
-
`
|
|
96
|
+
`started — pid ${result.record.pid}, /healthz on :${result.record.port}` +
|
|
97
|
+
`${result.record.project === undefined ? "" : `, project ${result.record.project}`}` +
|
|
98
|
+
`${result.via === "systemctl" ? " (via systemctl)" : ""}\nlog ${result.record.logFile}\n`,
|
|
69
99
|
);
|
|
100
|
+
} finally {
|
|
101
|
+
store.close();
|
|
70
102
|
}
|
|
71
|
-
process.stdout.write(
|
|
72
|
-
`started — pid ${result.record.pid}, /healthz on :${result.record.port}` +
|
|
73
|
-
`${result.record.project === undefined ? "" : `, project ${result.record.project}`}` +
|
|
74
|
-
`${result.via === "systemctl" ? " (via systemctl)" : ""}\nlog ${result.record.logFile}\n`,
|
|
75
|
-
);
|
|
76
103
|
}
|
package/src/commands/stop.ts
CHANGED
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { CommandContext } from "./context.ts";
|
|
10
|
+
import { buildStopProvenance } from "../stop-provenance.ts";
|
|
10
11
|
import { hold, pinPaneHalt, stopConductorPane } from "../fleet.ts";
|
|
11
12
|
import { stopDaemon } from "../lifecycle.ts";
|
|
13
|
+
import { openStore, dbPath } from "../store.ts";
|
|
12
14
|
|
|
13
15
|
export async function stopCommand(ctx: CommandContext): Promise<void> {
|
|
14
16
|
const withPane = ctx.argv.includes("--pane");
|
|
@@ -24,28 +26,49 @@ const targets = ctx.targetProjects().map((project) => {
|
|
|
24
26
|
pin: withPane ? pinPaneHalt(project.name).path : undefined,
|
|
25
27
|
};
|
|
26
28
|
});
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
29
|
+
const store = openStore(dbPath());
|
|
30
|
+
try {
|
|
31
|
+
// Provenance (#378): the shared daemon serves every configured project, so
|
|
32
|
+
// the record names the request's own project (or the global scope) AND every
|
|
33
|
+
// sibling with its live-run count — no silent cross-project stop.
|
|
34
|
+
const provenance = buildStopProvenance({
|
|
35
|
+
controlPath: "cli stop",
|
|
36
|
+
reason: "operator stop",
|
|
37
|
+
scope: targets.length > 1 ? "global" : "project",
|
|
38
|
+
project: targets.length === 1 ? targets[0]!.project.name : undefined,
|
|
39
|
+
});
|
|
40
|
+
const stop = await stopDaemon({
|
|
41
|
+
provenance,
|
|
42
|
+
// The durable row is written by the lifecycle at the exact chokepoint —
|
|
43
|
+
// immediately before systemctl/SIGTERM, with the delivery method that is
|
|
44
|
+
// about to be used — which is what makes "provenance precedes signalling"
|
|
45
|
+
// provable rather than claimed.
|
|
46
|
+
record: (signal) => store.recordDaemonStop(signal.provenance),
|
|
47
|
+
});
|
|
48
|
+
const stopLine =
|
|
49
|
+
stop.kind === "not-running"
|
|
50
|
+
? "daemon was not running"
|
|
51
|
+
: `daemon stopped — pid ${stop.pid}${stop.via === "systemctl" ? " (via systemctl)" : ""}`;
|
|
52
|
+
for (const target of targets) {
|
|
53
|
+
if (target.pin !== undefined) {
|
|
54
|
+
const pane = await stopConductorPane(target.project.name);
|
|
55
|
+
process.stdout.write(
|
|
56
|
+
`stopped — claiming paused; ticks disarmed at ${target.hold.disarmed.path}\n` +
|
|
57
|
+
`${stopLine}\n` +
|
|
58
|
+
`pane recovery pinned at ${target.pin}\n` +
|
|
59
|
+
`pane stop: ${pane.stopped} — ${pane.detail}\n` +
|
|
60
|
+
` (conductor agent "${pane.agentName}" only — herdr-fleet.service was NOT stopped;\n` +
|
|
61
|
+
` resume clears the pin when you want recovery again)\n`,
|
|
62
|
+
);
|
|
63
|
+
} else {
|
|
64
|
+
process.stdout.write(
|
|
65
|
+
`stopped — claiming paused; ticks disarmed at ${target.hold.disarmed.path}\n` +
|
|
66
|
+
`${stopLine}\n` +
|
|
67
|
+
`pane left running (pass --pane to stop the conductor agent and pin recovery)\n`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
49
70
|
}
|
|
71
|
+
} finally {
|
|
72
|
+
store.close();
|
|
50
73
|
}
|
|
51
74
|
}
|