omp-conductor 0.3.13 → 0.3.16
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 +227 -70
- package/package.json +3 -2
- package/skills/conductor-update/SKILL.md +157 -0
- package/src/brief-upgrade.ts +1 -1
- package/src/briefs/orchestrator.md +3 -2
- package/src/briefs/worker.md +1 -1
- package/src/cli.ts +161 -40
- package/src/confinement.ts +123 -0
- package/src/daemon.ts +55 -2
- package/src/fleet.ts +1271 -0
- package/src/host.ts +90 -0
- package/src/lifecycle.ts +182 -77
- package/src/omp.ts +12 -0
- package/src/orchestrator-tick.ts +64 -3
- package/src/plugin.ts +110 -17
- package/src/tracker/github.ts +25 -1
- package/src/types.ts +10 -0
- package/src/worker.ts +2 -0
- package/systemd/omp-conductor.service.example +54 -0
package/src/daemon.ts
CHANGED
|
@@ -826,7 +826,8 @@ export interface Admission {
|
|
|
826
826
|
*
|
|
827
827
|
* Exported so the admission rules can be pinned without spawning a worker.
|
|
828
828
|
* Every one of them exists because of a live incident, and each guards a
|
|
829
|
-
* different way the same issue gets worked twice
|
|
829
|
+
* different way the same issue gets worked twice — including epic siblings
|
|
830
|
+
* racing onto the same files (#48).
|
|
830
831
|
*
|
|
831
832
|
* Takes the slice of `Deps` it actually reads rather than the whole thing: what
|
|
832
833
|
* admission is allowed to consult is the point of the function, and a `Deps`
|
|
@@ -838,7 +839,34 @@ export async function admitCandidates(
|
|
|
838
839
|
slots: number,
|
|
839
840
|
): Promise<Admission[]> {
|
|
840
841
|
const { project, caps, tracker, store } = d;
|
|
841
|
-
const
|
|
842
|
+
const busyIssues = store.activeRuns(project.name).map((r) => r.issue);
|
|
843
|
+
const busy = new Set(busyIssues);
|
|
844
|
+
|
|
845
|
+
// parent -> blocking issue. Seeded from active runs (including pushed-green),
|
|
846
|
+
// then extended by candidates admitted earlier in this same pass so two
|
|
847
|
+
// siblings never both clear the gate in one tick.
|
|
848
|
+
const occupiedParents = new Map<number, number>();
|
|
849
|
+
const parentCache = new Map<number, number | undefined>();
|
|
850
|
+
|
|
851
|
+
const resolveParent = async (issue: number): Promise<number | undefined> => {
|
|
852
|
+
if (parentCache.has(issue)) return parentCache.get(issue);
|
|
853
|
+
const parent = await tracker.parentOf(issue);
|
|
854
|
+
parentCache.set(issue, parent);
|
|
855
|
+
return parent;
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
// Bounded by concurrent workers, not queue depth. A failed lookup here cannot
|
|
859
|
+
// mark an epic occupied; candidates still fail closed on their own parentOf.
|
|
860
|
+
for (const issue of busyIssues) {
|
|
861
|
+
try {
|
|
862
|
+
const parent = await resolveParent(issue);
|
|
863
|
+
if (parent !== undefined && !occupiedParents.has(parent)) {
|
|
864
|
+
occupiedParents.set(parent, issue);
|
|
865
|
+
}
|
|
866
|
+
} catch (err) {
|
|
867
|
+
log(`#${issue} parent lookup failed while seeding epic occupancy (${errText(err)})`);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
842
870
|
|
|
843
871
|
const admitted: Admission[] = [];
|
|
844
872
|
for (const r of routed) {
|
|
@@ -862,6 +890,27 @@ export async function admitCandidates(
|
|
|
862
890
|
continue;
|
|
863
891
|
}
|
|
864
892
|
|
|
893
|
+
// Soft concurrency per epic: at most one in-flight child of a given parent.
|
|
894
|
+
// No parent means today's concurrent admission. Cheap local filters already
|
|
895
|
+
// ran; this sits before the open-PR API call so a held sibling frees the
|
|
896
|
+
// slot for unrelated work without spending a closers query.
|
|
897
|
+
let parent: number | undefined;
|
|
898
|
+
try {
|
|
899
|
+
parent = await resolveParent(r.issue.number);
|
|
900
|
+
} catch (err) {
|
|
901
|
+
log(`#${r.issue.number} held: parent check failed (${errText(err)}) — retrying next tick`);
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
if (parent !== undefined) {
|
|
905
|
+
const blocker = occupiedParents.get(parent);
|
|
906
|
+
if (blocker !== undefined) {
|
|
907
|
+
log(
|
|
908
|
+
`#${r.issue.number} skipped: sibling #${blocker} in flight under epic #${parent}`,
|
|
909
|
+
);
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
865
914
|
// The busy set is built from run rows, so it can only speak for work this
|
|
866
915
|
// database recorded. Work pushed before this store existed — a migration, a
|
|
867
916
|
// wiped or relocated state dir, a restore onto a new host — looks exactly
|
|
@@ -889,6 +938,7 @@ export async function admitCandidates(
|
|
|
889
938
|
}
|
|
890
939
|
|
|
891
940
|
admitted.push({ r, attempt: prior + 1 });
|
|
941
|
+
if (parent !== undefined) occupiedParents.set(parent, r.issue.number);
|
|
892
942
|
}
|
|
893
943
|
|
|
894
944
|
return admitted;
|
|
@@ -1341,6 +1391,9 @@ export async function runDaemon(o: DaemonOpts = {}): Promise<void> {
|
|
|
1341
1391
|
paused: isPaused(),
|
|
1342
1392
|
activeRuns: store.activeRuns(project.name).length,
|
|
1343
1393
|
project: project.name,
|
|
1394
|
+
// Resident set of *this* process: workers are in-process omp sessions,
|
|
1395
|
+
// so the unit's Memory peak is this number, not a separate worker pid.
|
|
1396
|
+
rssBytes: process.memoryUsage().rss,
|
|
1344
1397
|
});
|
|
1345
1398
|
}
|
|
1346
1399
|
return new Response("not found\n", { status: 404 });
|