@lmzhen/dsh-evolution-merge 0.30.2 → 0.30.4
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/lib/index.js +186 -75
- package/lib/types/adapters/library.js +15 -0
- package/lib/types/commit-execute.d.ts +4 -0
- package/lib/types/commit-execute.js +7 -6
- package/lib/types/commit-recovery.d.ts +1 -1
- package/lib/types/commit-recovery.js +53 -7
- package/lib/types/commit-snapshot.d.ts +22 -2
- package/lib/types/commit-snapshot.js +39 -4
- package/lib/types/coordinator.d.ts +2 -0
- package/lib/types/coordinator.js +2 -2
- package/lib/types/domain/commit-journal.d.ts +1 -1
- package/lib/types/domain/commit-journal.js +1 -0
- package/lib/types/index.js +10 -3
- package/lib/types/runtime.js +4 -1
- package/lib/types/scan-run.js +3 -1
- package/lib/types/state.js +1 -1
- package/package.json +3 -3
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
1
|
+
import { join, resolve, sep } from "node:path";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { pairKeyOf, screen, selectCandidates, toEngineCandidate } from "@lmzhen/dsh-evolution-merge-engine";
|
|
4
|
-
import { DEFAULT_SKILL_LIMITS, authorityFrom, contentHash, createSubagentChannel, evolutionHome, evolutionIoAdapter, executeUnitUndo, isPresent, newRunRegistry, newSkillLibrary, parseFrontmatter, planUnitUndoAcross, policyStageLimits, probeAbsent, probeList, probePresent, probeText, probeUnknown, scanContentThreats, supportRefs, transactIo, validateFrontmatter, validateSupportPath } from "@lmzhen/dsh-evolution-core";
|
|
4
|
+
import { DEFAULT_SKILL_LIMITS, authorityFrom, contentHash, createSubagentChannel, evolutionHome, evolutionIoAdapter, executeUnitUndo, isPresent, newRunRegistry, newSkillLibrary, parseFrontmatter, planUnitUndoAcross, policyStageLimits, probeAbsent, probeList, probePresent, probeText, probeUnknown, scanContentThreats, skillsRoot, supportRefs, transactIo, validateFrontmatter, validateSupportPath } from "@lmzhen/dsh-evolution-core";
|
|
5
5
|
import { HOST_ROUTES, json, methodNotAllowed, registerHostRoutes } from "@lmzhen/dsh-evolution-core/http-routes";
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
7
7
|
//#region lib/types/adapters/select.js
|
|
@@ -356,7 +356,7 @@ async function upsertRow(io, home, now, row, expect = "any") {
|
|
|
356
356
|
current
|
|
357
357
|
}
|
|
358
358
|
};
|
|
359
|
-
if (current === void 0 && expect !== "any" && expect !== "absent") return {
|
|
359
|
+
if (current === void 0 && expect !== "any" && expect !== "absent" && expect !== "verify") return {
|
|
360
360
|
rows,
|
|
361
361
|
result: {
|
|
362
362
|
ok: false,
|
|
@@ -588,6 +588,7 @@ function recoveryFor(journal) {
|
|
|
588
588
|
case "committed":
|
|
589
589
|
case "rolled-back":
|
|
590
590
|
case "failed": return "none";
|
|
591
|
+
default: return "refuse";
|
|
591
592
|
}
|
|
592
593
|
}
|
|
593
594
|
//#endregion
|
|
@@ -607,7 +608,16 @@ async function writeJournal(io, home, journal) {
|
|
|
607
608
|
function snapshotPath(home, runId) {
|
|
608
609
|
return join(home, "merge", "snapshots", runId.replace(/[^A-Za-z0-9_-]+/g, "_") + ".json");
|
|
609
610
|
}
|
|
610
|
-
async function writeCommitSnapshot(io, home, runId, plan, now) {
|
|
611
|
+
async function writeCommitSnapshot(io, home, runId, plan, now, port) {
|
|
612
|
+
if (!isSafeSkillName(plan.target) || plan.sources.some((name) => !isSafeSkillName(name))) throw new Error("unsafe skill name in commit plan: " + plan.target);
|
|
613
|
+
const readSupport = port?.readSupportFiles === void 0 ? void 0 : (name) => port.readSupportFiles?.(name) ?? Promise.resolve([]);
|
|
614
|
+
const targetSupport = readSupport === void 0 || !plan.targetExists ? [] : await readSupport(plan.target);
|
|
615
|
+
const sourceSupport = [];
|
|
616
|
+
if (readSupport !== void 0) for (const name of plan.sources) for (const file of await readSupport(name)) sourceSupport.push({
|
|
617
|
+
name,
|
|
618
|
+
path: file.path,
|
|
619
|
+
content: file.content
|
|
620
|
+
});
|
|
611
621
|
const snapshot = {
|
|
612
622
|
runId,
|
|
613
623
|
target: plan.target,
|
|
@@ -618,6 +628,9 @@ async function writeCommitSnapshot(io, home, runId, plan, now) {
|
|
|
618
628
|
name: item.name,
|
|
619
629
|
body: item.body
|
|
620
630
|
})),
|
|
631
|
+
...targetSupport.length === 0 ? {} : { targetSupport },
|
|
632
|
+
...plan.supportFiles.length === 0 ? {} : { targetSupportAfter: plan.supportFiles },
|
|
633
|
+
...sourceSupport.length === 0 ? {} : { sourceSupport },
|
|
621
634
|
at: new Date(now).toISOString()
|
|
622
635
|
};
|
|
623
636
|
await io.writeText(snapshotPath(home, runId), JSON.stringify(snapshot, null, 2));
|
|
@@ -632,58 +645,22 @@ async function readCommitSnapshot(io, home, runId) {
|
|
|
632
645
|
return null;
|
|
633
646
|
}
|
|
634
647
|
}
|
|
635
|
-
function
|
|
636
|
-
return
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
continue;
|
|
652
|
-
}
|
|
653
|
-
const recovery = recoveryFor(journal);
|
|
654
|
-
if (recovery === "none") continue;
|
|
655
|
-
const snapshot = await readCommitSnapshot(io, home, journal.runId);
|
|
656
|
-
let action = "report-only";
|
|
657
|
-
let ok = true;
|
|
658
|
-
if (recovery === "retry-commit") action = "no-host-write";
|
|
659
|
-
else if (snapshot === null) {
|
|
660
|
-
action = "snapshot-missing";
|
|
661
|
-
ok = false;
|
|
662
|
-
} else try {
|
|
663
|
-
if (recovery === "rollback-sources") for (const source of snapshot.sourceBodies) await io.writeText(skillBodyPath(home, source.name), source.body);
|
|
664
|
-
if (snapshot.create && snapshot.targetBody === null) try {
|
|
665
|
-
await io.remove(skillBodyPath(home, snapshot.target));
|
|
666
|
-
} catch {}
|
|
667
|
-
else if (snapshot.targetBody !== null) await io.writeText(skillBodyPath(home, snapshot.target), snapshot.targetBody);
|
|
668
|
-
action = recovery === "rollback-sources" ? "restored-sources-and-target" : "restored-target";
|
|
669
|
-
} catch (error) {
|
|
670
|
-
action = "restore-failed: " + (error instanceof Error ? error.message : String(error));
|
|
671
|
-
ok = false;
|
|
672
|
-
}
|
|
673
|
-
await writeJournal(io, home, {
|
|
674
|
-
...journal,
|
|
675
|
-
phase: ok ? "rolled-back" : "failed",
|
|
676
|
-
updatedAt: new Date(now).toISOString()
|
|
677
|
-
});
|
|
678
|
-
reports.push({
|
|
679
|
-
runId: journal.runId,
|
|
680
|
-
phase: journal.phase,
|
|
681
|
-
action,
|
|
682
|
-
ok
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
await io.writeText(join(home, "merge", "recovery.json"), JSON.stringify(reports, null, 2));
|
|
686
|
-
return reports;
|
|
648
|
+
function isSafeSkillName(name) {
|
|
649
|
+
return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) && name !== "." && name !== "..";
|
|
650
|
+
}
|
|
651
|
+
function skillFilePath(root, name, path) {
|
|
652
|
+
if (!isSafeSkillName(name) || path.startsWith("/") || path.split("/").includes("..")) throw new Error("unsafe skill file: " + name + "/" + path);
|
|
653
|
+
const base = resolve(root, name);
|
|
654
|
+
const file = resolve(base, path);
|
|
655
|
+
if (!file.startsWith(base + sep)) throw new Error("skill file escapes skill: " + name + "/" + path);
|
|
656
|
+
return file;
|
|
657
|
+
}
|
|
658
|
+
function skillBodyPath(root, name) {
|
|
659
|
+
if (!isSafeSkillName(name)) throw new Error("unsafe skill name: " + name);
|
|
660
|
+
const base = resolve(root);
|
|
661
|
+
const path = resolve(base, name, "SKILL.md");
|
|
662
|
+
if (!path.startsWith(base + sep)) throw new Error("skill path escapes root: " + name);
|
|
663
|
+
return path;
|
|
687
664
|
}
|
|
688
665
|
//#endregion
|
|
689
666
|
//#region lib/types/commit-receipt.js
|
|
@@ -761,6 +738,113 @@ async function readCommitReceipt(io, home, runId, target) {
|
|
|
761
738
|
return null;
|
|
762
739
|
}
|
|
763
740
|
//#endregion
|
|
741
|
+
//#region lib/types/commit-recovery.js
|
|
742
|
+
/** Startup replay for non-terminal commit journals. */
|
|
743
|
+
async function recoverCommitJournals(io, home, now, skillRoot, restoreSource, isActive) {
|
|
744
|
+
const dir = join(home, "merge", "journals");
|
|
745
|
+
if (!await io.exists(dir)) return [];
|
|
746
|
+
const files = (await io.list(dir)).filter((name) => name.endsWith(".json"));
|
|
747
|
+
const reports = [];
|
|
748
|
+
for (const file of files) {
|
|
749
|
+
let journal;
|
|
750
|
+
try {
|
|
751
|
+
journal = JSON.parse(await io.readText(join(dir, file)) ?? "");
|
|
752
|
+
} catch {
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
if (isActive?.(journal.runId) === true) {
|
|
756
|
+
reports.push({
|
|
757
|
+
runId: journal.runId,
|
|
758
|
+
phase: journal.phase,
|
|
759
|
+
action: "skipped-active-run",
|
|
760
|
+
ok: true
|
|
761
|
+
});
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
const recovery = recoveryFor(journal);
|
|
765
|
+
if (recovery === "none") continue;
|
|
766
|
+
if (recovery === "refuse") {
|
|
767
|
+
reports.push({
|
|
768
|
+
runId: journal.runId,
|
|
769
|
+
phase: journal.phase,
|
|
770
|
+
action: "unknown-phase-refused",
|
|
771
|
+
ok: false
|
|
772
|
+
});
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
let receipt = null;
|
|
776
|
+
try {
|
|
777
|
+
receipt = await readCommitReceipt(io, home, journal.runId, journal.target);
|
|
778
|
+
} catch {
|
|
779
|
+
reports.push({
|
|
780
|
+
runId: journal.runId,
|
|
781
|
+
phase: journal.phase,
|
|
782
|
+
action: "receipt-read-failed",
|
|
783
|
+
ok: false
|
|
784
|
+
});
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
if (receipt !== null) {
|
|
788
|
+
await writeJournal(io, home, {
|
|
789
|
+
...journal,
|
|
790
|
+
phase: "committed",
|
|
791
|
+
unitId: receipt.unitId,
|
|
792
|
+
updatedAt: new Date(now).toISOString()
|
|
793
|
+
});
|
|
794
|
+
reports.push({
|
|
795
|
+
runId: journal.runId,
|
|
796
|
+
phase: journal.phase,
|
|
797
|
+
action: "receipt-committed",
|
|
798
|
+
ok: true
|
|
799
|
+
});
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
const snapshot = await readCommitSnapshot(io, home, journal.runId);
|
|
803
|
+
let action = "report-only";
|
|
804
|
+
let ok = true;
|
|
805
|
+
if (recovery === "retry-commit") action = "no-host-write";
|
|
806
|
+
else if (snapshot === null) {
|
|
807
|
+
action = "snapshot-missing";
|
|
808
|
+
ok = false;
|
|
809
|
+
} else try {
|
|
810
|
+
if (recovery === "rollback-sources") {
|
|
811
|
+
let restoredArchived = restoreSource !== void 0;
|
|
812
|
+
if (restoreSource !== void 0) {
|
|
813
|
+
for (const source of snapshot.sourceBodies) if (!await restoreSource(source.name)) restoredArchived = false;
|
|
814
|
+
}
|
|
815
|
+
if (!restoredArchived) for (const source of snapshot.sourceBodies) await io.writeText(skillBodyPath(skillRoot, source.name), source.body);
|
|
816
|
+
}
|
|
817
|
+
if (snapshot.create && snapshot.targetBody === null) try {
|
|
818
|
+
await io.remove(skillBodyPath(skillRoot, snapshot.target));
|
|
819
|
+
} catch {}
|
|
820
|
+
else if (snapshot.targetBody !== null) await io.writeText(skillBodyPath(skillRoot, snapshot.target), snapshot.targetBody);
|
|
821
|
+
const oldPaths = new Set((snapshot.targetSupport ?? []).map((file) => file.path));
|
|
822
|
+
for (const file of snapshot.targetSupport ?? []) await io.writeText(skillFilePath(skillRoot, snapshot.target, file.path), file.content);
|
|
823
|
+
for (const file of snapshot.targetSupportAfter ?? []) if (!oldPaths.has(file.path)) try {
|
|
824
|
+
await io.remove(skillFilePath(skillRoot, snapshot.target, file.path));
|
|
825
|
+
} catch {}
|
|
826
|
+
for (const file of snapshot.sourceSupport ?? []) await io.writeText(skillFilePath(skillRoot, file.name, file.path), file.content);
|
|
827
|
+
action = recovery === "rollback-sources" ? "restored-sources-and-target" : "restored-target";
|
|
828
|
+
} catch (error) {
|
|
829
|
+
action = "restore-failed: " + (error instanceof Error ? error.message : String(error));
|
|
830
|
+
ok = false;
|
|
831
|
+
}
|
|
832
|
+
if (ok) await writeJournal(io, home, {
|
|
833
|
+
...journal,
|
|
834
|
+
phase: "rolled-back",
|
|
835
|
+
updatedAt: new Date(now).toISOString()
|
|
836
|
+
});
|
|
837
|
+
reports.push({
|
|
838
|
+
runId: journal.runId,
|
|
839
|
+
phase: journal.phase,
|
|
840
|
+
action,
|
|
841
|
+
ok
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
await io.writeText(join(home, "merge", "recovery.json"), JSON.stringify(reports, null, 2));
|
|
845
|
+
return reports;
|
|
846
|
+
}
|
|
847
|
+
//#endregion
|
|
764
848
|
//#region lib/types/coordinator.js
|
|
765
849
|
/**
|
|
766
850
|
* Pure merge transition table. No io, no run shell, no subagents.
|
|
@@ -866,11 +950,15 @@ function transition(current, event) {
|
|
|
866
950
|
return event.approval === "on" ? next(current, event.at, "pending", {
|
|
867
951
|
...event.decision === void 0 ? {} : { decision: event.decision },
|
|
868
952
|
...event.stagingRef === void 0 ? {} : { stagingRef: event.stagingRef },
|
|
869
|
-
...event.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: event.stagingExpiresAt }
|
|
953
|
+
...event.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: event.stagingExpiresAt },
|
|
954
|
+
...event.sourceRevision === void 0 ? {} : { sourceRevision: event.sourceRevision },
|
|
955
|
+
...event.targetRevision === void 0 ? {} : { targetRevision: event.targetRevision }
|
|
870
956
|
}) : next(current, event.at, "approved", {
|
|
871
957
|
...event.decision === void 0 ? {} : { decision: event.decision },
|
|
872
958
|
...event.stagingRef === void 0 ? {} : { stagingRef: event.stagingRef },
|
|
873
|
-
...event.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: event.stagingExpiresAt }
|
|
959
|
+
...event.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: event.stagingExpiresAt },
|
|
960
|
+
...event.sourceRevision === void 0 ? {} : { sourceRevision: event.sourceRevision },
|
|
961
|
+
...event.targetRevision === void 0 ? {} : { targetRevision: event.targetRevision }
|
|
874
962
|
});
|
|
875
963
|
case "round1-fail": return current.state === "verify" ? next(current, event.at, "verify", {
|
|
876
964
|
failure: event.failure,
|
|
@@ -1678,7 +1766,8 @@ async function runScan(options) {
|
|
|
1678
1766
|
});
|
|
1679
1767
|
if (!created.ok || created.row === null) throw new Error(created.ok ? "candidate row missing" : created.message);
|
|
1680
1768
|
row = created.row;
|
|
1681
|
-
await upsertRow(options.io, options.home, options.now, row);
|
|
1769
|
+
const placed = await upsertRow(options.io, options.home, options.now, row, "verify");
|
|
1770
|
+
if (!placed.ok) throw new Error("pair already in " + placed.current.state);
|
|
1682
1771
|
} catch (error) {
|
|
1683
1772
|
totals.failed += 1;
|
|
1684
1773
|
pairs.push({
|
|
@@ -3049,7 +3138,7 @@ async function commitStagedRun(io, home, runId, port, options) {
|
|
|
3049
3138
|
message: assembled.message
|
|
3050
3139
|
};
|
|
3051
3140
|
try {
|
|
3052
|
-
await writeCommitSnapshot(io, home, runId, assembled.plan, options.now());
|
|
3141
|
+
await writeCommitSnapshot(io, home, runId, assembled.plan, options.now(), port);
|
|
3053
3142
|
} catch (error) {
|
|
3054
3143
|
return {
|
|
3055
3144
|
ok: false,
|
|
@@ -3073,6 +3162,7 @@ async function commitStagedRun(io, home, runId, port, options) {
|
|
|
3073
3162
|
try {
|
|
3074
3163
|
await writeJournal(io, home, {
|
|
3075
3164
|
...journal,
|
|
3165
|
+
phase: "failed",
|
|
3076
3166
|
updatedAt: new Date(options.now()).toISOString()
|
|
3077
3167
|
});
|
|
3078
3168
|
} catch {}
|
|
@@ -3082,6 +3172,14 @@ async function commitStagedRun(io, home, runId, port, options) {
|
|
|
3082
3172
|
plan: assembled.plan
|
|
3083
3173
|
};
|
|
3084
3174
|
}
|
|
3175
|
+
const committed = {
|
|
3176
|
+
...journal,
|
|
3177
|
+
phase: "committed",
|
|
3178
|
+
updatedAt: new Date(options.now()).toISOString()
|
|
3179
|
+
};
|
|
3180
|
+
try {
|
|
3181
|
+
await writeJournal(io, home, committed);
|
|
3182
|
+
} catch {}
|
|
3085
3183
|
if (result.unitId === void 0) return {
|
|
3086
3184
|
ok: true,
|
|
3087
3185
|
plan: assembled.plan,
|
|
@@ -3101,14 +3199,6 @@ async function commitStagedRun(io, home, runId, port, options) {
|
|
|
3101
3199
|
};
|
|
3102
3200
|
try {
|
|
3103
3201
|
await writeCommitReceipt(io, home, receipt);
|
|
3104
|
-
try {
|
|
3105
|
-
await writeJournal(io, home, {
|
|
3106
|
-
...journal,
|
|
3107
|
-
unitId: result.unitId,
|
|
3108
|
-
phase: "committed",
|
|
3109
|
-
updatedAt: new Date(options.now()).toISOString()
|
|
3110
|
-
});
|
|
3111
|
-
} catch {}
|
|
3112
3202
|
return {
|
|
3113
3203
|
ok: true,
|
|
3114
3204
|
plan: assembled.plan,
|
|
@@ -3504,7 +3594,21 @@ function skillLibraryPort(skills) {
|
|
|
3504
3594
|
const names = await archivedNames.call(skills);
|
|
3505
3595
|
return names === null ? true : names.includes(name);
|
|
3506
3596
|
},
|
|
3507
|
-
commitMerge: (plan) => skills.commitMerge(plan)
|
|
3597
|
+
commitMerge: (plan) => skills.commitMerge(plan),
|
|
3598
|
+
async readSupportFiles(name) {
|
|
3599
|
+
if (typeof skills.listSupportFiles !== "function") return [];
|
|
3600
|
+
const probe = await skills.listSupportFiles(name);
|
|
3601
|
+
if (probe.kind === "unknown") throw new Error("support file list unknown: " + name);
|
|
3602
|
+
if (probe.kind === "absent") return [];
|
|
3603
|
+
return await Promise.all(probe.value.map(async (path) => {
|
|
3604
|
+
const content = await skills.readSupportFile(name, path);
|
|
3605
|
+
if (content === null) throw new Error("support file disappeared: " + name + "/" + path);
|
|
3606
|
+
return {
|
|
3607
|
+
path,
|
|
3608
|
+
content
|
|
3609
|
+
};
|
|
3610
|
+
}));
|
|
3611
|
+
}
|
|
3508
3612
|
};
|
|
3509
3613
|
}
|
|
3510
3614
|
//#endregion
|
|
@@ -3561,6 +3665,7 @@ function createMergeRuntime(deps) {
|
|
|
3561
3665
|
b: command.b
|
|
3562
3666
|
}];
|
|
3563
3667
|
const existing = (await loadMergeState(deps.io, deps.home)).state.rows.find((row) => row.pairKey === pairKey(command.a, command.b));
|
|
3668
|
+
if (existing?.state === "skipped") await dropRow(deps.io, deps.home, deps.now, existing.id);
|
|
3564
3669
|
if (existing?.state === "pending") return {
|
|
3565
3670
|
ok: false,
|
|
3566
3671
|
text: "该 pair 已在待批准队列,先 approve/reject/discard: " + existing.id
|
|
@@ -3624,7 +3729,7 @@ function createMergeRuntime(deps) {
|
|
|
3624
3729
|
const managed = new Set(cards.filter((card) => card.managed === true).map((card) => card.name));
|
|
3625
3730
|
const loadedState = await loadMergeState(deps.io, deps.home);
|
|
3626
3731
|
const skipped = skippedPairKeys(loadedState.state.rows, deps.params.mergeSkippedTtlDays, deps.now());
|
|
3627
|
-
const held = new Set(loadedState.state.rows.filter((row) => row.queue === "pending" || !canRetry(row)).map((row) => row.pairKey));
|
|
3732
|
+
const held = new Set(loadedState.state.rows.filter((row) => row.queue === "pending" || row.queue === "fuse" || !canRetry(row)).map((row) => row.pairKey));
|
|
3628
3733
|
const conflictNotes = loadedState.state.rows.filter((row) => row.state === "pending" || row.state === "approved" || row.state === "fuse" || row.state === "skipped").map((row) => "conflict:" + row.state + ":" + row.pairKey + ":" + row.id);
|
|
3629
3734
|
const result = await runScanCommand({
|
|
3630
3735
|
...common,
|
|
@@ -4404,11 +4509,6 @@ function apply(ctx, rawConfig = {}) {
|
|
|
4404
4509
|
loadMergeState(io, home).then((loaded) => cleanupExpiredStaging(io, home, Date.now(), new Set(loaded.state.rows.map((row) => row.runId).filter((id) => id !== void 0)))).catch((error) => {
|
|
4405
4510
|
ctx.logger.warn("merge staging cleanup failed: " + (error instanceof Error ? error.message : String(error)));
|
|
4406
4511
|
});
|
|
4407
|
-
recoverCommitJournals(io, home, Date.now()).then((reports) => {
|
|
4408
|
-
if (reports.length > 0) ctx.logger.info("merge commit recovery: " + reports.map((report) => report.runId + "=" + report.action).join(", "));
|
|
4409
|
-
}).catch((error) => {
|
|
4410
|
-
ctx.logger.warn("merge commit recovery failed: " + (error instanceof Error ? error.message : String(error)));
|
|
4411
|
-
});
|
|
4412
4512
|
const runs = newRunRegistry({
|
|
4413
4513
|
io,
|
|
4414
4514
|
home,
|
|
@@ -4434,6 +4534,17 @@ function apply(ctx, rawConfig = {}) {
|
|
|
4434
4534
|
...policyStageLimits(policySnapshot?.get?.())
|
|
4435
4535
|
}
|
|
4436
4536
|
});
|
|
4537
|
+
const mergeSkillsRoot = skills.root ?? skillsRoot();
|
|
4538
|
+
recoverCommitJournals(io, home, Date.now(), mergeSkillsRoot, async (name) => {
|
|
4539
|
+
return (await skills.restoreFromArchive(name)).ok;
|
|
4540
|
+
}, (runId) => {
|
|
4541
|
+
const run = runs.find(runId);
|
|
4542
|
+
return run !== void 0 && run.state === "running";
|
|
4543
|
+
}).then((reports) => {
|
|
4544
|
+
if (reports.length > 0) ctx.logger.info("merge commit recovery: " + reports.map((report) => report.runId + "=" + report.action).join(", "));
|
|
4545
|
+
}).catch((error) => {
|
|
4546
|
+
ctx.logger.warn("merge commit recovery failed: " + (error instanceof Error ? error.message : String(error)));
|
|
4547
|
+
});
|
|
4437
4548
|
const channel = subagentChannelAdapter(createSubagentChannel(ctx));
|
|
4438
4549
|
const stage = new StageToolRegistry();
|
|
4439
4550
|
const engineConfig = engineConfigFromEnv(params.mergeModelsRoot);
|
|
@@ -9,6 +9,21 @@ export function skillLibraryPort(skills) {
|
|
|
9
9
|
return names === null ? true : names.includes(name);
|
|
10
10
|
},
|
|
11
11
|
commitMerge: plan => skills.commitMerge(plan),
|
|
12
|
+
async readSupportFiles(name) {
|
|
13
|
+
if (typeof skills.listSupportFiles !== 'function')
|
|
14
|
+
return [];
|
|
15
|
+
const probe = await skills.listSupportFiles(name);
|
|
16
|
+
if (probe.kind === 'unknown')
|
|
17
|
+
throw new Error('support file list unknown: ' + name);
|
|
18
|
+
if (probe.kind === 'absent')
|
|
19
|
+
return [];
|
|
20
|
+
return await Promise.all(probe.value.map(async (path) => {
|
|
21
|
+
const content = await skills.readSupportFile(name, path);
|
|
22
|
+
if (content === null)
|
|
23
|
+
throw new Error('support file disappeared: ' + name + '/' + path);
|
|
24
|
+
return { path, content };
|
|
25
|
+
}));
|
|
26
|
+
},
|
|
12
27
|
};
|
|
13
28
|
}
|
|
14
29
|
//# sourceMappingURL=library.js.map
|
|
@@ -8,6 +8,10 @@ import { type CommitAssemblyPort } from './commit-assembly.ts';
|
|
|
8
8
|
import { type CommitReceipt } from './commit-receipt.ts';
|
|
9
9
|
export interface CommitPort extends CommitAssemblyPort {
|
|
10
10
|
commitMerge(plan: CommitMergePlan): Promise<CommitMergeResult>;
|
|
11
|
+
readSupportFiles?(name: string): Promise<readonly {
|
|
12
|
+
readonly path: string;
|
|
13
|
+
readonly content: string;
|
|
14
|
+
}[]>;
|
|
11
15
|
}
|
|
12
16
|
export interface CommitStagedOptions {
|
|
13
17
|
readonly origin: CommitMergePlan['origin'];
|
|
@@ -8,7 +8,7 @@ export async function commitStagedRun(io, home, runId, port, options) {
|
|
|
8
8
|
if (!assembled.ok)
|
|
9
9
|
return { ok: false, message: assembled.message };
|
|
10
10
|
try {
|
|
11
|
-
await writeCommitSnapshot(io, home, runId, assembled.plan, options.now());
|
|
11
|
+
await writeCommitSnapshot(io, home, runId, assembled.plan, options.now(), port);
|
|
12
12
|
}
|
|
13
13
|
catch (error) {
|
|
14
14
|
return { ok: false, message: 'commit snapshot failed; refusing to write: ' + (error instanceof Error ? error.message : String(error)), plan: assembled.plan };
|
|
@@ -21,11 +21,16 @@ export async function commitStagedRun(io, home, runId, port, options) {
|
|
|
21
21
|
const result = await port.commitMerge(assembled.plan);
|
|
22
22
|
if (!result.ok) {
|
|
23
23
|
try {
|
|
24
|
-
await writeJournal(io, home, { ...journal, updatedAt: new Date(options.now()).toISOString() });
|
|
24
|
+
await writeJournal(io, home, { ...journal, phase: 'failed', updatedAt: new Date(options.now()).toISOString() });
|
|
25
25
|
}
|
|
26
26
|
catch { }
|
|
27
27
|
return { ok: false, message: result.message, plan: assembled.plan };
|
|
28
28
|
}
|
|
29
|
+
const committed = { ...journal, phase: 'committed', updatedAt: new Date(options.now()).toISOString() };
|
|
30
|
+
try {
|
|
31
|
+
await writeJournal(io, home, committed);
|
|
32
|
+
}
|
|
33
|
+
catch { }
|
|
29
34
|
if (result.unitId === undefined)
|
|
30
35
|
return { ok: true, plan: assembled.plan, result, warning: 'commit landed without a unitId; receipt not written' };
|
|
31
36
|
const receipt = {
|
|
@@ -41,10 +46,6 @@ export async function commitStagedRun(io, home, runId, port, options) {
|
|
|
41
46
|
};
|
|
42
47
|
try {
|
|
43
48
|
await writeCommitReceipt(io, home, receipt);
|
|
44
|
-
try {
|
|
45
|
-
await writeJournal(io, home, { ...journal, unitId: result.unitId, phase: 'committed', updatedAt: new Date(options.now()).toISOString() });
|
|
46
|
-
}
|
|
47
|
-
catch { }
|
|
48
49
|
return { ok: true, plan: assembled.plan, result, receipt };
|
|
49
50
|
}
|
|
50
51
|
catch (error) {
|
|
@@ -6,5 +6,5 @@ export interface CommitRecoveryReport {
|
|
|
6
6
|
readonly action: string;
|
|
7
7
|
readonly ok: boolean;
|
|
8
8
|
}
|
|
9
|
-
export declare function recoverCommitJournals(io: EvolutionIoLike, home: string, now: number): Promise<readonly CommitRecoveryReport[]>;
|
|
9
|
+
export declare function recoverCommitJournals(io: EvolutionIoLike, home: string, now: number, skillRoot: string, restoreSource?: (name: string) => Promise<boolean>, isActive?: (runId: string) => boolean): Promise<readonly CommitRecoveryReport[]>;
|
|
10
10
|
//# sourceMappingURL=commit-recovery.d.ts.map
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { recoveryFor } from "./domain/commit-journal.js";
|
|
4
4
|
import { writeJournal } from "./domain/commit-journal-store.js";
|
|
5
|
-
import { readCommitSnapshot, skillBodyPath } from "./commit-snapshot.js";
|
|
6
|
-
|
|
5
|
+
import { readCommitSnapshot, skillBodyPath, skillFilePath } from "./commit-snapshot.js";
|
|
6
|
+
import { readCommitReceipt } from "./commit-receipt.js";
|
|
7
|
+
export async function recoverCommitJournals(io, home, now, skillRoot, restoreSource, isActive) {
|
|
7
8
|
const dir = join(home, 'merge', 'journals');
|
|
8
9
|
if (!(await io.exists(dir)))
|
|
9
10
|
return [];
|
|
@@ -17,9 +18,30 @@ export async function recoverCommitJournals(io, home, now) {
|
|
|
17
18
|
catch {
|
|
18
19
|
continue;
|
|
19
20
|
}
|
|
21
|
+
if (isActive?.(journal.runId) === true) {
|
|
22
|
+
reports.push({ runId: journal.runId, phase: journal.phase, action: 'skipped-active-run', ok: true });
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
20
25
|
const recovery = recoveryFor(journal);
|
|
21
26
|
if (recovery === 'none')
|
|
22
27
|
continue;
|
|
28
|
+
if (recovery === 'refuse') {
|
|
29
|
+
reports.push({ runId: journal.runId, phase: journal.phase, action: 'unknown-phase-refused', ok: false });
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
let receipt = null;
|
|
33
|
+
try {
|
|
34
|
+
receipt = await readCommitReceipt(io, home, journal.runId, journal.target);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
reports.push({ runId: journal.runId, phase: journal.phase, action: 'receipt-read-failed', ok: false });
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (receipt !== null) {
|
|
41
|
+
await writeJournal(io, home, { ...journal, phase: 'committed', unitId: receipt.unitId, updatedAt: new Date(now).toISOString() });
|
|
42
|
+
reports.push({ runId: journal.runId, phase: journal.phase, action: 'receipt-committed', ok: true });
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
23
45
|
const snapshot = await readCommitSnapshot(io, home, journal.runId);
|
|
24
46
|
let action = 'report-only';
|
|
25
47
|
let ok = true;
|
|
@@ -33,18 +55,41 @@ export async function recoverCommitJournals(io, home, now) {
|
|
|
33
55
|
else {
|
|
34
56
|
try {
|
|
35
57
|
if (recovery === 'rollback-sources') {
|
|
36
|
-
|
|
37
|
-
|
|
58
|
+
let restoredArchived = restoreSource !== undefined;
|
|
59
|
+
if (restoreSource !== undefined) {
|
|
60
|
+
for (const source of snapshot.sourceBodies) {
|
|
61
|
+
if (!(await restoreSource(source.name)))
|
|
62
|
+
restoredArchived = false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (!restoredArchived) {
|
|
66
|
+
for (const source of snapshot.sourceBodies)
|
|
67
|
+
await io.writeText(skillBodyPath(skillRoot, source.name), source.body);
|
|
68
|
+
}
|
|
38
69
|
}
|
|
39
70
|
if (snapshot.create && snapshot.targetBody === null) {
|
|
40
71
|
try {
|
|
41
|
-
await io.remove(skillBodyPath(
|
|
72
|
+
await io.remove(skillBodyPath(skillRoot, snapshot.target));
|
|
42
73
|
}
|
|
43
74
|
catch { /* already gone */ }
|
|
44
75
|
}
|
|
45
76
|
else if (snapshot.targetBody !== null) {
|
|
46
|
-
await io.writeText(skillBodyPath(
|
|
77
|
+
await io.writeText(skillBodyPath(skillRoot, snapshot.target), snapshot.targetBody);
|
|
78
|
+
}
|
|
79
|
+
const oldPaths = new Set((snapshot.targetSupport ?? []).map(file => file.path));
|
|
80
|
+
for (const file of snapshot.targetSupport ?? []) {
|
|
81
|
+
await io.writeText(skillFilePath(skillRoot, snapshot.target, file.path), file.content);
|
|
82
|
+
}
|
|
83
|
+
for (const file of snapshot.targetSupportAfter ?? []) {
|
|
84
|
+
if (!oldPaths.has(file.path)) {
|
|
85
|
+
try {
|
|
86
|
+
await io.remove(skillFilePath(skillRoot, snapshot.target, file.path));
|
|
87
|
+
}
|
|
88
|
+
catch { /* already gone */ }
|
|
89
|
+
}
|
|
47
90
|
}
|
|
91
|
+
for (const file of snapshot.sourceSupport ?? [])
|
|
92
|
+
await io.writeText(skillFilePath(skillRoot, file.name, file.path), file.content);
|
|
48
93
|
action = recovery === 'rollback-sources' ? 'restored-sources-and-target' : 'restored-target';
|
|
49
94
|
}
|
|
50
95
|
catch (error) {
|
|
@@ -52,7 +97,8 @@ export async function recoverCommitJournals(io, home, now) {
|
|
|
52
97
|
ok = false;
|
|
53
98
|
}
|
|
54
99
|
}
|
|
55
|
-
|
|
100
|
+
if (ok)
|
|
101
|
+
await writeJournal(io, home, { ...journal, phase: 'rolled-back', updatedAt: new Date(now).toISOString() });
|
|
56
102
|
reports.push({ runId: journal.runId, phase: journal.phase, action, ok });
|
|
57
103
|
}
|
|
58
104
|
await io.writeText(join(home, 'merge', 'recovery.json'), JSON.stringify(reports, null, 2));
|
|
@@ -9,10 +9,30 @@ export interface CommitSnapshot {
|
|
|
9
9
|
readonly name: string;
|
|
10
10
|
readonly body: string;
|
|
11
11
|
}[];
|
|
12
|
+
readonly targetSupport?: readonly {
|
|
13
|
+
readonly path: string;
|
|
14
|
+
readonly content: string;
|
|
15
|
+
}[];
|
|
16
|
+
readonly targetSupportAfter?: readonly {
|
|
17
|
+
readonly path: string;
|
|
18
|
+
readonly content: string;
|
|
19
|
+
}[];
|
|
20
|
+
readonly sourceSupport?: readonly {
|
|
21
|
+
readonly name: string;
|
|
22
|
+
readonly path: string;
|
|
23
|
+
readonly content: string;
|
|
24
|
+
}[];
|
|
12
25
|
readonly at: string;
|
|
13
26
|
}
|
|
14
27
|
export declare function snapshotPath(home: string, runId: string): string;
|
|
15
|
-
export declare function writeCommitSnapshot(io: EvolutionIoLike, home: string, runId: string, plan: CommitMergePlan, now: number
|
|
28
|
+
export declare function writeCommitSnapshot(io: EvolutionIoLike, home: string, runId: string, plan: CommitMergePlan, now: number, port?: {
|
|
29
|
+
readSupportFiles?(name: string): Promise<readonly {
|
|
30
|
+
readonly path: string;
|
|
31
|
+
readonly content: string;
|
|
32
|
+
}[]>;
|
|
33
|
+
}): Promise<CommitSnapshot>;
|
|
16
34
|
export declare function readCommitSnapshot(io: EvolutionIoLike, home: string, runId: string): Promise<CommitSnapshot | null>;
|
|
17
|
-
export declare function
|
|
35
|
+
export declare function isSafeSkillName(name: string): boolean;
|
|
36
|
+
export declare function skillFilePath(root: string, name: string, path: string): string;
|
|
37
|
+
export declare function skillBodyPath(root: string, name: string): string;
|
|
18
38
|
//# sourceMappingURL=commit-snapshot.d.ts.map
|
|
@@ -1,13 +1,30 @@
|
|
|
1
1
|
/** Pre-commit snapshot: enough to restore the target and archived sources after a crash. */
|
|
2
|
-
import { join } from 'node:path';
|
|
2
|
+
import { join, resolve, sep } from 'node:path';
|
|
3
3
|
export function snapshotPath(home, runId) {
|
|
4
4
|
return join(home, 'merge', 'snapshots', runId.replace(/[^A-Za-z0-9_-]+/g, '_') + '.json');
|
|
5
5
|
}
|
|
6
|
-
export async function writeCommitSnapshot(io, home, runId, plan, now) {
|
|
6
|
+
export async function writeCommitSnapshot(io, home, runId, plan, now, port) {
|
|
7
|
+
if (!isSafeSkillName(plan.target) || plan.sources.some(name => !isSafeSkillName(name))) {
|
|
8
|
+
throw new Error('unsafe skill name in commit plan: ' + plan.target);
|
|
9
|
+
}
|
|
10
|
+
const readSupport = port?.readSupportFiles === undefined
|
|
11
|
+
? undefined
|
|
12
|
+
: (name) => port.readSupportFiles?.(name) ?? Promise.resolve([]);
|
|
13
|
+
const targetSupport = readSupport === undefined || !plan.targetExists ? [] : await readSupport(plan.target);
|
|
14
|
+
const sourceSupport = [];
|
|
15
|
+
if (readSupport !== undefined) {
|
|
16
|
+
for (const name of plan.sources) {
|
|
17
|
+
for (const file of await readSupport(name))
|
|
18
|
+
sourceSupport.push({ name, path: file.path, content: file.content });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
7
21
|
const snapshot = {
|
|
8
22
|
runId, target: plan.target, create: !plan.targetExists, sources: plan.sources,
|
|
9
23
|
targetBody: plan.expectedTargetBody,
|
|
10
24
|
sourceBodies: plan.expectedSourceBodies.map(item => ({ name: item.name, body: item.body })),
|
|
25
|
+
...(targetSupport.length === 0 ? {} : { targetSupport }),
|
|
26
|
+
...(plan.supportFiles.length === 0 ? {} : { targetSupportAfter: plan.supportFiles }),
|
|
27
|
+
...(sourceSupport.length === 0 ? {} : { sourceSupport }),
|
|
11
28
|
at: new Date(now).toISOString(),
|
|
12
29
|
};
|
|
13
30
|
await io.writeText(snapshotPath(home, runId), JSON.stringify(snapshot, null, 2));
|
|
@@ -24,7 +41,25 @@ export async function readCommitSnapshot(io, home, runId) {
|
|
|
24
41
|
return null;
|
|
25
42
|
}
|
|
26
43
|
}
|
|
27
|
-
export function
|
|
28
|
-
return
|
|
44
|
+
export function isSafeSkillName(name) {
|
|
45
|
+
return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) && name !== '.' && name !== '..';
|
|
46
|
+
}
|
|
47
|
+
export function skillFilePath(root, name, path) {
|
|
48
|
+
if (!isSafeSkillName(name) || path.startsWith('/') || path.split('/').includes('..'))
|
|
49
|
+
throw new Error('unsafe skill file: ' + name + '/' + path);
|
|
50
|
+
const base = resolve(root, name);
|
|
51
|
+
const file = resolve(base, path);
|
|
52
|
+
if (!file.startsWith(base + sep))
|
|
53
|
+
throw new Error('skill file escapes skill: ' + name + '/' + path);
|
|
54
|
+
return file;
|
|
55
|
+
}
|
|
56
|
+
export function skillBodyPath(root, name) {
|
|
57
|
+
if (!isSafeSkillName(name))
|
|
58
|
+
throw new Error('unsafe skill name: ' + name);
|
|
59
|
+
const base = resolve(root);
|
|
60
|
+
const path = resolve(base, name, 'SKILL.md');
|
|
61
|
+
if (!path.startsWith(base + sep))
|
|
62
|
+
throw new Error('skill path escapes root: ' + name);
|
|
63
|
+
return path;
|
|
29
64
|
}
|
|
30
65
|
//# sourceMappingURL=commit-snapshot.js.map
|
|
@@ -20,6 +20,8 @@ export type MergeEvent = {
|
|
|
20
20
|
readonly decision?: MergeDecision;
|
|
21
21
|
readonly stagingRef?: string;
|
|
22
22
|
readonly stagingExpiresAt?: number;
|
|
23
|
+
readonly sourceRevision?: string;
|
|
24
|
+
readonly targetRevision?: string;
|
|
23
25
|
readonly at: string;
|
|
24
26
|
} | {
|
|
25
27
|
readonly type: 'round1-fail';
|
package/lib/types/coordinator.js
CHANGED
|
@@ -98,8 +98,8 @@ export function transition(current, event) {
|
|
|
98
98
|
if (current.state !== 'verify')
|
|
99
99
|
return invalid(event, current.state);
|
|
100
100
|
return event.approval === 'on'
|
|
101
|
-
? next(current, event.at, 'pending', { ...(event.decision === undefined ? {} : { decision: event.decision }), ...(event.stagingRef === undefined ? {} : { stagingRef: event.stagingRef }), ...(event.stagingExpiresAt === undefined ? {} : { stagingExpiresAt: event.stagingExpiresAt }) })
|
|
102
|
-
: next(current, event.at, 'approved', { ...(event.decision === undefined ? {} : { decision: event.decision }), ...(event.stagingRef === undefined ? {} : { stagingRef: event.stagingRef }), ...(event.stagingExpiresAt === undefined ? {} : { stagingExpiresAt: event.stagingExpiresAt }) });
|
|
101
|
+
? next(current, event.at, 'pending', { ...(event.decision === undefined ? {} : { decision: event.decision }), ...(event.stagingRef === undefined ? {} : { stagingRef: event.stagingRef }), ...(event.stagingExpiresAt === undefined ? {} : { stagingExpiresAt: event.stagingExpiresAt }), ...(event.sourceRevision === undefined ? {} : { sourceRevision: event.sourceRevision }), ...(event.targetRevision === undefined ? {} : { targetRevision: event.targetRevision }) })
|
|
102
|
+
: next(current, event.at, 'approved', { ...(event.decision === undefined ? {} : { decision: event.decision }), ...(event.stagingRef === undefined ? {} : { stagingRef: event.stagingRef }), ...(event.stagingExpiresAt === undefined ? {} : { stagingExpiresAt: event.stagingExpiresAt }), ...(event.sourceRevision === undefined ? {} : { sourceRevision: event.sourceRevision }), ...(event.targetRevision === undefined ? {} : { targetRevision: event.targetRevision }) });
|
|
103
103
|
case 'round1-fail':
|
|
104
104
|
return current.state === 'verify'
|
|
105
105
|
? next(current, event.at, 'verify', { failure: event.failure, attempts: (current.attempts ?? 0) + 1 })
|
|
@@ -10,7 +10,7 @@ export interface CommitJournal {
|
|
|
10
10
|
readonly updatedAt: string;
|
|
11
11
|
}
|
|
12
12
|
export declare function advanceJournal(journal: CommitJournal, phase: JournalPhase, at: string): CommitJournal;
|
|
13
|
-
export type JournalRecovery = 'none' | 'retry-commit' | 'rollback-target' | 'rollback-sources';
|
|
13
|
+
export type JournalRecovery = 'none' | 'retry-commit' | 'rollback-target' | 'rollback-sources' | 'refuse';
|
|
14
14
|
/** What a restart may do with the last durable phase. */
|
|
15
15
|
export declare function recoveryFor(journal: CommitJournal): JournalRecovery;
|
|
16
16
|
//# sourceMappingURL=commit-journal.d.ts.map
|
package/lib/types/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { screen as engineScreen } from '@lmzhen/dsh-evolution-merge-engine';
|
|
|
4
4
|
import { selectWithErrors } from "./adapters/select.js";
|
|
5
5
|
import { createEngineScreening } from "./adapters/engine-screen.js";
|
|
6
6
|
import { createEngineRunner } from "./adapters/model-runner.js";
|
|
7
|
-
import { authorityFrom, createSubagentChannel, DEFAULT_SKILL_LIMITS, evolutionHome, evolutionIoAdapter, newRunRegistry, newSkillLibrary, policyStageLimits } from '@lmzhen/dsh-evolution-core';
|
|
7
|
+
import { authorityFrom, createSubagentChannel, DEFAULT_SKILL_LIMITS, evolutionHome, evolutionIoAdapter, newRunRegistry, newSkillLibrary, policyStageLimits, skillsRoot } from '@lmzhen/dsh-evolution-core';
|
|
8
8
|
import { registerHostRoutes } from '@lmzhen/dsh-evolution-core/http-routes';
|
|
9
9
|
import { loadMergeState, upsertRow } from "./state.js";
|
|
10
10
|
import { cleanupExpiredStaging } from "./staging.js";
|
|
@@ -122,8 +122,6 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
122
122
|
const io = evolutionIoAdapter(() => registry.provider());
|
|
123
123
|
const home = evolutionHome();
|
|
124
124
|
void loadMergeState(io, home).then(loaded => cleanupExpiredStaging(io, home, Date.now(), new Set(loaded.state.rows.map(row => row.runId).filter((id) => id !== undefined)))).catch((error) => { ctx.logger.warn('merge staging cleanup failed: ' + (error instanceof Error ? error.message : String(error))); });
|
|
125
|
-
void recoverCommitJournals(io, home, Date.now()).then((reports) => { if (reports.length > 0)
|
|
126
|
-
ctx.logger.info('merge commit recovery: ' + reports.map(report => report.runId + '=' + report.action).join(', ')); }).catch((error) => { ctx.logger.warn('merge commit recovery failed: ' + (error instanceof Error ? error.message : String(error))); });
|
|
127
125
|
const runs = newRunRegistry({ io, home, warn: (message) => { ctx.logger.warn(message); } });
|
|
128
126
|
void runs.load();
|
|
129
127
|
ctx.effect(() => {
|
|
@@ -140,6 +138,15 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
140
138
|
io, ctx, authority: authorityFrom({ origin: 'subagent' }),
|
|
141
139
|
limits: { ...DEFAULT_SKILL_LIMITS, ...policyStageLimits(policySnapshot?.get?.()) },
|
|
142
140
|
});
|
|
141
|
+
const mergeSkillsRoot = skills.root ?? skillsRoot();
|
|
142
|
+
void recoverCommitJournals(io, home, Date.now(), mergeSkillsRoot, async (name) => {
|
|
143
|
+
const result = await skills.restoreFromArchive(name);
|
|
144
|
+
return result.ok;
|
|
145
|
+
}, (runId) => {
|
|
146
|
+
const run = runs.find(runId);
|
|
147
|
+
return run !== undefined && run.state === 'running';
|
|
148
|
+
}).then((reports) => { if (reports.length > 0)
|
|
149
|
+
ctx.logger.info('merge commit recovery: ' + reports.map(report => report.runId + '=' + report.action).join(', ')); }).catch((error) => { ctx.logger.warn('merge commit recovery failed: ' + (error instanceof Error ? error.message : String(error))); });
|
|
143
150
|
const channel = subagentChannelAdapter(createSubagentChannel(ctx));
|
|
144
151
|
const stage = new StageToolRegistry();
|
|
145
152
|
const engineConfig = engineConfigFromEnv(params.mergeModelsRoot);
|
package/lib/types/runtime.js
CHANGED
|
@@ -55,6 +55,9 @@ export function createMergeRuntime(deps) {
|
|
|
55
55
|
}
|
|
56
56
|
const pair = [{ a: command.a, b: command.b }];
|
|
57
57
|
const existing = (await loadMergeState(deps.io, deps.home)).state.rows.find(row => row.pairKey === pairKey(command.a, command.b));
|
|
58
|
+
if (existing?.state === 'skipped') {
|
|
59
|
+
await dropRow(deps.io, deps.home, deps.now, existing.id);
|
|
60
|
+
}
|
|
58
61
|
if (existing?.state === 'pending') {
|
|
59
62
|
return { ok: false, text: '该 pair 已在待批准队列,先 approve/reject/discard: ' + existing.id };
|
|
60
63
|
}
|
|
@@ -95,7 +98,7 @@ export function createMergeRuntime(deps) {
|
|
|
95
98
|
const managed = new Set(cards.filter(card => card.managed === true).map(card => card.name));
|
|
96
99
|
const loadedState = await loadMergeState(deps.io, deps.home);
|
|
97
100
|
const skipped = skippedPairKeys(loadedState.state.rows, deps.params.mergeSkippedTtlDays, deps.now());
|
|
98
|
-
const held = new Set(loadedState.state.rows.filter(row => row.queue === 'pending' || !canRetry(row)).map(row => row.pairKey));
|
|
101
|
+
const held = new Set(loadedState.state.rows.filter(row => row.queue === 'pending' || row.queue === 'fuse' || !canRetry(row)).map(row => row.pairKey));
|
|
99
102
|
const conflictNotes = loadedState.state.rows
|
|
100
103
|
.filter(row => row.state === 'pending' || row.state === 'approved' || row.state === 'fuse' || row.state === 'skipped')
|
|
101
104
|
.map(row => 'conflict:' + row.state + ':' + row.pairKey + ':' + row.id);
|
package/lib/types/scan-run.js
CHANGED
|
@@ -65,7 +65,9 @@ export async function runScan(options) {
|
|
|
65
65
|
if (!created.ok || created.row === null)
|
|
66
66
|
throw new Error(created.ok ? 'candidate row missing' : created.message);
|
|
67
67
|
row = created.row;
|
|
68
|
-
await upsertRow(options.io, options.home, options.now, row);
|
|
68
|
+
const placed = await upsertRow(options.io, options.home, options.now, row, 'verify');
|
|
69
|
+
if (!placed.ok)
|
|
70
|
+
throw new Error('pair already in ' + placed.current.state);
|
|
69
71
|
}
|
|
70
72
|
catch (error) {
|
|
71
73
|
totals.failed += 1;
|
package/lib/types/state.js
CHANGED
|
@@ -62,7 +62,7 @@ export async function upsertRow(io, home, now, row, expect = 'any') {
|
|
|
62
62
|
if (current !== undefined && expect !== 'any' && (expect === 'absent' || queueOf(current.state) !== expect)) {
|
|
63
63
|
return { rows, result: { ok: false, current } };
|
|
64
64
|
}
|
|
65
|
-
if (current === undefined && expect !== 'any' && expect !== 'absent') {
|
|
65
|
+
if (current === undefined && expect !== 'any' && expect !== 'absent' && expect !== 'verify') {
|
|
66
66
|
return { rows, result: { ok: false, current: row } };
|
|
67
67
|
}
|
|
68
68
|
const normalized = { ...row, queue: queueOf(row.state) };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmzhen/dsh-evolution-merge",
|
|
3
3
|
"description": "Skill auto-merge domain: screening, two-round authoring, queues and commit orchestration (community build)",
|
|
4
|
-
"version": "0.30.
|
|
4
|
+
"version": "0.30.4",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@deepseek-ai/schemastery": "^3.18.5",
|
|
30
|
-
"@lmzhen/dsh-evolution-core": "^0.30.
|
|
31
|
-
"@lmzhen/dsh-evolution-merge-engine": "^0.30.
|
|
30
|
+
"@lmzhen/dsh-evolution-core": "^0.30.4",
|
|
31
|
+
"@lmzhen/dsh-evolution-merge-engine": "^0.30.4"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"@deepseek-ai/cordis": "^4.0.5"
|