@lmzhen/dsh-evolution-merge 0.39.2 → 0.40.0
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 +406 -289
- package/lib/types/approve-command.d.ts +4 -0
- package/lib/types/approve-command.js +5 -4
- package/lib/types/domain/pair-ledger.d.ts +3 -1
- package/lib/types/domain/pair-ledger.js +2 -2
- package/lib/types/domain/row-run.d.ts +4 -2
- package/lib/types/domain/row-run.js +22 -10
- package/lib/types/domain/run-coordinator.d.ts +5 -1
- package/lib/types/domain/run-coordinator.js +35 -2
- package/lib/types/domain/schema4.d.ts +3 -1
- package/lib/types/domain/schema4.js +48 -42
- package/lib/types/domain/stage-aggregate.js +21 -3
- package/lib/types/domain/state-bootstrap.d.ts +3 -1
- package/lib/types/domain/state-bootstrap.js +95 -89
- package/lib/types/index.js +13 -4
- package/lib/types/pair-command.d.ts +3 -0
- package/lib/types/pair-command.js +1 -0
- package/lib/types/pending-actions.d.ts +6 -2
- package/lib/types/pending-actions.js +6 -6
- package/lib/types/resume-command.d.ts +3 -0
- package/lib/types/resume-command.js +4 -4
- package/lib/types/round2-outcome.d.ts +3 -1
- package/lib/types/round2-outcome.js +7 -7
- package/lib/types/run-shell.d.ts +8 -0
- package/lib/types/run-shell.js +41 -19
- package/lib/types/runtime.d.ts +16 -1
- package/lib/types/runtime.js +23 -17
- package/lib/types/scan-command.d.ts +3 -0
- package/lib/types/scan-command.js +1 -0
- package/lib/types/target-claim.js +8 -2
- package/lib/types/undo-operation.d.ts +3 -0
- package/package.json +4 -4
package/lib/types/index.js
CHANGED
|
@@ -178,13 +178,14 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
178
178
|
const loaded = await loadMergeState(io, home);
|
|
179
179
|
return { ok: true, schema: 3, migrated: false, state: { schemaVersion: 3, rows: loaded.state.rows.map(legacyToV3).filter((row) => row !== null) }, quarantine: [], report: null };
|
|
180
180
|
}
|
|
181
|
-
const state = await ensureMergeStateReady(io, home);
|
|
181
|
+
const state = await ensureMergeStateReady(io, home, ctx.get('evolutionMutationAuthority')?.homeFence);
|
|
182
182
|
if (!state.ok)
|
|
183
183
|
return v4.code === 'merge-state-forward-only' || v4.code === 'merge-state-needs-repair' ? { ok: false, code: v4.code, message: v4.message } : state;
|
|
184
184
|
const normalized = await normalizePairLedger(io, home);
|
|
185
185
|
return normalized.ok ? state : { ok: false, code: 'merge-state-needs-repair', message: normalized.message ?? 'v4 normalize failed' };
|
|
186
186
|
})().catch(error => ({ ok: false, code: 'merge-state-migration-blocked', message: error instanceof Error ? error.message : String(error) }));
|
|
187
|
-
const
|
|
187
|
+
const homeFence = ctx.get('evolutionMutationAuthority')?.homeFence;
|
|
188
|
+
const runs = newRunRegistry({ io, home, warn: (message) => { ctx.logger.warn(message); }, ...(homeFence === undefined ? {} : { fence: homeFence }) });
|
|
188
189
|
ctx.effect(() => {
|
|
189
190
|
const host = ctx;
|
|
190
191
|
if (typeof host.on !== 'function')
|
|
@@ -201,16 +202,18 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
201
202
|
return typeof hostApproval?.platformWrite === 'boolean' ? hostApproval.platformWrite : (resolveMergeParams(rawConfig).platformWrite ?? false);
|
|
202
203
|
};
|
|
203
204
|
const getParams = () => ({ ...resolveMergeParams(rawConfig), limitsSnapshot, platformWrite: getPlatformWrite() });
|
|
205
|
+
const mutationAuthority = ctx.get('evolutionMutationAuthority');
|
|
204
206
|
const skills = newSkillLibrary({
|
|
205
207
|
io, ctx, authority: authorityFrom({ origin: 'subagent' }),
|
|
206
208
|
limits: limitsSnapshot.skillLimits,
|
|
209
|
+
...(mutationAuthority?.mutationLock === undefined ? {} : { mutationLock: mutationAuthority.mutationLock }),
|
|
207
210
|
});
|
|
208
211
|
void ready.then(value => {
|
|
209
212
|
if (!value.ok) {
|
|
210
213
|
ctx.logger.warn('merge state is not ready: ' + value.code + ': ' + value.message);
|
|
211
214
|
return;
|
|
212
215
|
}
|
|
213
|
-
const unit = new UnitTransactions({ io, home, backend: skillLibraryUnitBackend(skills), claim: targetClaimUnitPort(io, home, () => Date.now()), now: () => Date.now() });
|
|
216
|
+
const unit = new UnitTransactions({ io, home, backend: skillLibraryUnitBackend(skills), claim: targetClaimUnitPort(io, home, () => Date.now()), now: () => Date.now(), isActive: async (runId) => await runs.hasOwner(runId), ...(mutationAuthority?.homeFence === undefined ? {} : { fence: mutationAuthority.homeFence }) });
|
|
214
217
|
const cards = async () => await readSkillCards(skills);
|
|
215
218
|
void (async () => {
|
|
216
219
|
// NFR-05: load the shared run index BEFORE recovery. Recovery settles through the
|
|
@@ -219,7 +222,12 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
219
222
|
const loaded = await runs.load();
|
|
220
223
|
if (!loaded.ok)
|
|
221
224
|
ctx.logger.warn('merge run index: ' + (loaded.note ?? 'unreadable'));
|
|
222
|
-
const
|
|
225
|
+
const activeRuns = new Set();
|
|
226
|
+
for (const record of runs.runs().filter(record => record.state === 'running')) {
|
|
227
|
+
if (await runs.hasOwner(record.id))
|
|
228
|
+
activeRuns.add(record.id);
|
|
229
|
+
}
|
|
230
|
+
const reports = await recoverCommitUnits({ io, home, unit, now: () => Date.now(), isActive: (runId) => activeRuns.has(runId), runs });
|
|
223
231
|
if (reports.length > 0)
|
|
224
232
|
ctx.logger.info('merge unit recovery: ' + reports.map(report => report.runId + '/' + report.unitId + '=' + report.action).join(', '));
|
|
225
233
|
const v4 = await readV4Readiness(io, home);
|
|
@@ -341,6 +349,7 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
341
349
|
return;
|
|
342
350
|
const runtime = createMergeRuntime({
|
|
343
351
|
io, home, registry: runs, skills, now: () => Date.now(), getParams, getPlatformWrite,
|
|
352
|
+
...(mutationAuthority?.homeFence === undefined ? {} : { fence: mutationAuthority.homeFence }),
|
|
344
353
|
...(defaultModel === undefined ? {} : { model: defaultModel.currentSelection().model }),
|
|
345
354
|
channel, parent: undefined, allow: ['merge_stage'], timeoutMinutes: startupParams.mergeRunTimeoutMinutes,
|
|
346
355
|
hostCapabilities,
|
|
@@ -6,6 +6,9 @@ import type { ScanPorts } from './scan-run.ts';
|
|
|
6
6
|
import type { MergeRegistryPort, WithMergeRunResult } from './run-shell.ts';
|
|
7
7
|
export interface RunPairCommandOptions {
|
|
8
8
|
readonly io: EvolutionIoLike;
|
|
9
|
+
readonly fence?: {
|
|
10
|
+
run<T>(body: () => Promise<T>): Promise<T>;
|
|
11
|
+
};
|
|
9
12
|
readonly home: string;
|
|
10
13
|
readonly registry: MergeRegistryPort;
|
|
11
14
|
readonly ports: ScanPorts;
|
|
@@ -4,6 +4,7 @@ export async function runPairCommand(options) {
|
|
|
4
4
|
io: options.io,
|
|
5
5
|
home: options.home,
|
|
6
6
|
registry: options.registry,
|
|
7
|
+
...(options.fence === undefined ? {} : { fence: options.fence }),
|
|
7
8
|
approval: options.approval,
|
|
8
9
|
platformWrite: options.platformWrite,
|
|
9
10
|
now: options.now,
|
|
@@ -12,6 +12,10 @@ export type PendingActionResult = {
|
|
|
12
12
|
readonly reason: 'not-found' | 'not-pending' | 'invalid' | 'expired';
|
|
13
13
|
readonly message: string;
|
|
14
14
|
};
|
|
15
|
-
export declare function approvePending(io: EvolutionIoLike, home: string, now: () => number, id: string
|
|
16
|
-
|
|
15
|
+
export declare function approvePending(io: EvolutionIoLike, home: string, now: () => number, id: string, fence?: {
|
|
16
|
+
run<T>(body: () => Promise<T>): Promise<T>;
|
|
17
|
+
}): Promise<PendingActionResult>;
|
|
18
|
+
export declare function rejectPending(io: EvolutionIoLike, home: string, now: () => number, id: string, fence?: {
|
|
19
|
+
run<T>(body: () => Promise<T>): Promise<T>;
|
|
20
|
+
}): Promise<PendingActionResult>;
|
|
17
21
|
//# sourceMappingURL=pending-actions.d.ts.map
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { v4ToLegacy } from "./domain/row-v4.js";
|
|
2
2
|
import { pairLedger } from "./domain/pair-ledger.js";
|
|
3
|
-
async function act(io, home, now, id, type) {
|
|
3
|
+
async function act(io, home, now, id, type, fence) {
|
|
4
4
|
void now;
|
|
5
|
-
return await pairLedger(io, home).transact(tx => {
|
|
5
|
+
return await pairLedger(io, home, fence).transact(tx => {
|
|
6
6
|
const row = tx.rows().find(item => item.id === id);
|
|
7
7
|
if (row === undefined)
|
|
8
8
|
return { ok: false, reason: 'not-found', message: 'pending row not found: ' + id };
|
|
@@ -23,10 +23,10 @@ async function act(io, home, now, id, type) {
|
|
|
23
23
|
return { ok: true, row: v4ToLegacy(moved.row) };
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
|
-
export async function approvePending(io, home, now, id) {
|
|
27
|
-
return await act(io, home, now, id, 'approve');
|
|
26
|
+
export async function approvePending(io, home, now, id, fence) {
|
|
27
|
+
return await act(io, home, now, id, 'approve', fence);
|
|
28
28
|
}
|
|
29
|
-
export async function rejectPending(io, home, now, id) {
|
|
30
|
-
return await act(io, home, now, id, 'reject');
|
|
29
|
+
export async function rejectPending(io, home, now, id, fence) {
|
|
30
|
+
return await act(io, home, now, id, 'reject', fence);
|
|
31
31
|
}
|
|
32
32
|
//# sourceMappingURL=pending-actions.js.map
|
|
@@ -8,6 +8,9 @@ import type { ScanPorts, ScanRound2Outcome } from './scan-run.ts';
|
|
|
8
8
|
import type { MergeDecision, MergeRow } from './contracts.ts';
|
|
9
9
|
export interface RunResumeCommandOptions {
|
|
10
10
|
readonly io: EvolutionIoLike;
|
|
11
|
+
readonly fence?: {
|
|
12
|
+
run<T>(body: () => Promise<T>): Promise<T>;
|
|
13
|
+
};
|
|
11
14
|
readonly home: string;
|
|
12
15
|
readonly registry: MergeRegistryPort;
|
|
13
16
|
readonly ports: ScanPorts;
|
|
@@ -23,7 +23,7 @@ export async function runResumeCommand(options) {
|
|
|
23
23
|
return failure('resume', 'run-owns-row', '该 verify 行仍由运行中的 run 持有:' + row.runId, '等待该 run 结束,或运行 /evolution merge cancel ' + row.runId, row.runId);
|
|
24
24
|
}
|
|
25
25
|
return await runScanCommand({
|
|
26
|
-
io: options.io, home: options.home, registry: options.registry,
|
|
26
|
+
io: options.io, home: options.home, registry: options.registry, ...(options.fence === undefined ? {} : { fence: options.fence }),
|
|
27
27
|
approval: options.approval, platformWrite: options.platformWrite, now: options.now,
|
|
28
28
|
...(options.detached === true ? { detached: true } : {}),
|
|
29
29
|
...(options.makePorts === undefined
|
|
@@ -57,7 +57,7 @@ export async function runResumeCommand(options) {
|
|
|
57
57
|
const staged = await options.ports.stage(selected, decision);
|
|
58
58
|
if (!staged.ok)
|
|
59
59
|
return failure('resume', 'stage-failed', staged.message, '检查暂存目录后重试;不影响已记录的决策', selected.runId);
|
|
60
|
-
const fused = await pairLedger(options.io, options.home).transact(tx => {
|
|
60
|
+
const fused = await pairLedger(options.io, options.home, options.fence).transact(tx => {
|
|
61
61
|
const current = tx.rows().find(item => item.id === selected.id);
|
|
62
62
|
if (current === undefined)
|
|
63
63
|
return null;
|
|
@@ -76,7 +76,7 @@ export async function runResumeCommand(options) {
|
|
|
76
76
|
parentRunId, now: options.now,
|
|
77
77
|
...(options.detached === true ? { detached: true } : {}),
|
|
78
78
|
async body(ctx) {
|
|
79
|
-
await attachRunToRow(options.io, options.home, row.id, ctx.runId);
|
|
79
|
+
await attachRunToRow(options.io, options.home, row.id, ctx.runId, options.isOwnerActive, options.fence);
|
|
80
80
|
await ctx.stage('author', { done: 0, total: 1 });
|
|
81
81
|
let outcome;
|
|
82
82
|
try {
|
|
@@ -87,7 +87,7 @@ export async function runResumeCommand(options) {
|
|
|
87
87
|
catch (error) {
|
|
88
88
|
outcome = { kind: 'failed', failure: { stage: 'author', code: 'merge-failed', message: error instanceof Error ? error.message : String(error), retryable: true, retryFrom: 'agent2' } };
|
|
89
89
|
}
|
|
90
|
-
const record = await recordRound2Outcome(options.io, options.home, options.now, row, outcome, true);
|
|
90
|
+
const record = await recordRound2Outcome(options.io, options.home, options.now, row, outcome, true, options.fence);
|
|
91
91
|
return {
|
|
92
92
|
stopReason: record.merged ? 'completed' : 'failed',
|
|
93
93
|
scope: { candidatePairs: 1, queueBefore: { verify: 0, pending: 0, fuse: 1, skipped: 0 } },
|
|
@@ -10,5 +10,7 @@ export interface Round2Record {
|
|
|
10
10
|
readonly merged: boolean;
|
|
11
11
|
readonly finalize?: () => Promise<void>;
|
|
12
12
|
}
|
|
13
|
-
export declare function recordRound2Outcome(io: EvolutionIoLike, home: string, now: () => number, row: MergeRow, outcome: ScanRound2Outcome, deferProjection?: boolean
|
|
13
|
+
export declare function recordRound2Outcome(io: EvolutionIoLike, home: string, now: () => number, row: MergeRow, outcome: ScanRound2Outcome, deferProjection?: boolean, fence?: {
|
|
14
|
+
run<T>(body: () => Promise<T>): Promise<T>;
|
|
15
|
+
}): Promise<Round2Record>;
|
|
14
16
|
//# sourceMappingURL=round2-outcome.d.ts.map
|
|
@@ -3,7 +3,7 @@ import { pairLedger } from "./domain/pair-ledger.js";
|
|
|
3
3
|
import { dropStage } from "./domain/stage-aggregate.js";
|
|
4
4
|
import { casExpiredHook } from "./domain/failure-route.js";
|
|
5
5
|
import { recordPair } from "./domain/run-coordinator.js";
|
|
6
|
-
export async function recordRound2Outcome(io, home, now, row, outcome, deferProjection = false) {
|
|
6
|
+
export async function recordRound2Outcome(io, home, now, row, outcome, deferProjection = false, fence) {
|
|
7
7
|
void now;
|
|
8
8
|
const reportExtra = {
|
|
9
9
|
...(outcome.manifestFiles === undefined ? {} : { manifestFiles: outcome.manifestFiles }),
|
|
@@ -14,7 +14,7 @@ export async function recordRound2Outcome(io, home, now, row, outcome, deferProj
|
|
|
14
14
|
const unitRef = { unitId: outcome.unitId ?? 'unknown', evidenceSha256: outcome.evidenceSha256 ?? 'unknown' };
|
|
15
15
|
const target = outcome.target?.actual ?? outcome.target?.requested ?? row.decision?.target;
|
|
16
16
|
const finalize = async () => {
|
|
17
|
-
const removed = await pairLedger(io, home).transact(tx => {
|
|
17
|
+
const removed = await pairLedger(io, home, fence).transact(tx => {
|
|
18
18
|
const current = tx.rows().find(item => item.id === row.id);
|
|
19
19
|
return current === undefined ? { ok: false, code: 'row-not-found', message: 'row not found' } : tx.remove(row.id, 'committed', unitRef, current.version);
|
|
20
20
|
});
|
|
@@ -51,7 +51,7 @@ export async function recordRound2Outcome(io, home, now, row, outcome, deferProj
|
|
|
51
51
|
const failure = outcome.failure ?? { stage: 'author', code: 'merge-failed', message: 'round2 failed', retryable: true, retryFrom: 'agent2' };
|
|
52
52
|
if (failure.code === 'merge-over-limit' || failure.code === 'limit') {
|
|
53
53
|
const reason = 'merge-over-limit';
|
|
54
|
-
const skipped = await pairLedger(io, home).transact(tx => {
|
|
54
|
+
const skipped = await pairLedger(io, home, fence).transact(tx => {
|
|
55
55
|
const current = tx.rows().find(item => item.id === row.id);
|
|
56
56
|
return current === undefined ? false : tx.transition(row.id, { type: 'skipped', reason, origin: 'system', skipSource: 'system' }, current.version).ok;
|
|
57
57
|
});
|
|
@@ -62,7 +62,7 @@ export async function recordRound2Outcome(io, home, now, row, outcome, deferProj
|
|
|
62
62
|
const cas = casExpiredHook(failure.code);
|
|
63
63
|
if (cas !== null) {
|
|
64
64
|
const terminalFailure = { ...failure, code: cas.reason, retryable: false, retryFrom: 'none' };
|
|
65
|
-
await pairLedger(io, home).transact(tx => {
|
|
65
|
+
await pairLedger(io, home, fence).transact(tx => {
|
|
66
66
|
const current = tx.rows().find(item => item.id === row.id);
|
|
67
67
|
if (current === undefined)
|
|
68
68
|
return false;
|
|
@@ -75,7 +75,7 @@ export async function recordRound2Outcome(io, home, now, row, outcome, deferProj
|
|
|
75
75
|
if (failure.code === 'commit-failed' || failure.code === 'validation-failed') {
|
|
76
76
|
// FR-EXEC-11/14: report-only failure. The pair leaves the active queue (so a later scan
|
|
77
77
|
// can evaluate it again); it is NOT written to skipped.
|
|
78
|
-
const dropped = await pairLedger(io, home).transact(tx => {
|
|
78
|
+
const dropped = await pairLedger(io, home, fence).transact(tx => {
|
|
79
79
|
const current = tx.rows().find(item => item.id === row.id);
|
|
80
80
|
return current === undefined ? false : tx.remove(row.id, 'superseded', undefined, current.version).ok;
|
|
81
81
|
});
|
|
@@ -85,7 +85,7 @@ export async function recordRound2Outcome(io, home, now, row, outcome, deferProj
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
if (failure.code === 'source-gone') {
|
|
88
|
-
const dropped = await pairLedger(io, home).transact(tx => {
|
|
88
|
+
const dropped = await pairLedger(io, home, fence).transact(tx => {
|
|
89
89
|
const current = tx.rows().find(item => item.id === row.id);
|
|
90
90
|
return current === undefined ? false : tx.remove(row.id, 'superseded', undefined, current.version).ok;
|
|
91
91
|
});
|
|
@@ -93,7 +93,7 @@ export async function recordRound2Outcome(io, home, now, row, outcome, deferProj
|
|
|
93
93
|
await dropStage(io, home, row.id, row.stagingRef);
|
|
94
94
|
return { merged: false, pair: { id: row.id, pairKey: row.pairKey, a: row.a, b: row.b, queue: row.queue, outcome: 'failed', archived: [], failure } };
|
|
95
95
|
}
|
|
96
|
-
const projected = await pairLedger(io, home).transact(tx => {
|
|
96
|
+
const projected = await pairLedger(io, home, fence).transact(tx => {
|
|
97
97
|
const current = tx.rows().find(item => item.id === row.id);
|
|
98
98
|
if (current === undefined)
|
|
99
99
|
return null;
|
package/lib/types/run-shell.d.ts
CHANGED
|
@@ -64,6 +64,14 @@ export interface WithMergeRunOptions {
|
|
|
64
64
|
readonly parentRunId?: string;
|
|
65
65
|
readonly detached?: boolean;
|
|
66
66
|
readonly timeoutMinutes?: number;
|
|
67
|
+
/** Drain budget after abort; tests may shrink it. Defaults to 30s. */
|
|
68
|
+
readonly drainMs?: number;
|
|
69
|
+
/** Observability for a detached finish failure; defaults to console.warn. */
|
|
70
|
+
readonly warn?: (message: string) => void;
|
|
71
|
+
/** Optional authority fence for run metadata writes. */
|
|
72
|
+
readonly fence?: {
|
|
73
|
+
run<T>(body: () => Promise<T>): Promise<T>;
|
|
74
|
+
};
|
|
67
75
|
readonly body: (ctx: MergeRunBodyContext) => Promise<MergeRunReportDraft>;
|
|
68
76
|
}
|
|
69
77
|
export type WithMergeRunResult = {
|
package/lib/types/run-shell.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { writeMergeReport } from "./report.js";
|
|
2
|
-
import { writeRunMeta } from "./domain/run-coordinator.js";
|
|
2
|
+
import { readRunMeta, writeRunMeta } from "./domain/run-coordinator.js";
|
|
3
3
|
/** Bounded wait for a detached body before the run is reported as timed out. */
|
|
4
4
|
const DETACHED_DRAIN_MS = 30_000;
|
|
5
5
|
function emptyDraft(reason) {
|
|
@@ -21,21 +21,32 @@ export async function withMergeRun(options) {
|
|
|
21
21
|
...(options.parentRunId === undefined ? {} : { parentRunId: options.parentRunId }),
|
|
22
22
|
});
|
|
23
23
|
const startedAt = handle.startedAt;
|
|
24
|
+
const writeMeta = async (meta) => { await writeRunMeta(options.io, options.home, meta, options.fence); };
|
|
24
25
|
const stage = (name, progress) => options.registry.stage(handle.id, name, progress);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
26
|
+
try {
|
|
27
|
+
await stage('screening', options.total === undefined ? undefined : { done: 0, total: options.total });
|
|
28
|
+
await writeMeta({
|
|
29
|
+
version: 1, runId: handle.id, pid: process.pid,
|
|
30
|
+
...(options.parentRunId === undefined ? {} : { parentRunId: options.parentRunId }),
|
|
31
|
+
...(options.pairKey === undefined ? {} : { pairKey: options.pairKey }),
|
|
32
|
+
...(options.stagingRef === undefined ? {} : { stagingRef: options.stagingRef }),
|
|
33
|
+
...(options.pairKey === undefined ? {} : { pairs: [{ pairKey: options.pairKey, state: 'pending' }] }),
|
|
34
|
+
pairCount: options.pairCount ?? (options.pairKey === undefined ? 0 : 1),
|
|
35
|
+
phase: 'running', approval: options.approval.mergeApproval ? 'pending' : 'approved',
|
|
36
|
+
createdAt: new Date(startedAt).toISOString(), updatedAt: new Date(options.now()).toISOString(),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
// P1-1: begin() succeeded, so a staging/meta failure must still settle the run.
|
|
41
|
+
await options.registry.settle(handle.id, {
|
|
42
|
+
state: 'failed',
|
|
43
|
+
failure: 'merge run failed before body: ' + (error instanceof Error ? error.message : String(error)),
|
|
44
|
+
}).catch(() => { });
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
36
47
|
const writeFailureMeta = async (stopReason) => {
|
|
37
48
|
try {
|
|
38
|
-
await
|
|
49
|
+
await writeMeta({
|
|
39
50
|
version: 1, runId: handle.id, pid: process.pid,
|
|
40
51
|
...(options.parentRunId === undefined ? {} : { parentRunId: options.parentRunId }),
|
|
41
52
|
...(options.pairKey === undefined ? {} : { pairKey: options.pairKey }),
|
|
@@ -80,13 +91,20 @@ export async function withMergeRun(options) {
|
|
|
80
91
|
let drainTimer;
|
|
81
92
|
const drained = await Promise.race([
|
|
82
93
|
bodyOutcome,
|
|
83
|
-
new Promise(resolve => { drainTimer = setTimeout(() => resolve({ kind: 'drain-timeout' }), DETACHED_DRAIN_MS); }),
|
|
94
|
+
new Promise(resolve => { drainTimer = setTimeout(() => resolve({ kind: 'drain-timeout' }), options.drainMs ?? DETACHED_DRAIN_MS); }),
|
|
84
95
|
]);
|
|
85
96
|
if (drainTimer !== undefined)
|
|
86
97
|
clearTimeout(drainTimer);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
98
|
+
if (drained.kind === 'drain-timeout') {
|
|
99
|
+
// P1-2: the body ignored abort. Close the run now so its stage/registry
|
|
100
|
+
// writes stop; the report below records the timeout.
|
|
101
|
+
await options.registry.settle(handle.id, { state: 'failed', failure: 'timeout: body did not stop within 30s drain window' }).catch(error => {
|
|
102
|
+
(options.warn ?? ((message) => { console.warn(message); }))('evolution-merge: drain-timeout settle failed for ' + handle.id + ': ' + (error instanceof Error ? error.message : String(error)));
|
|
103
|
+
});
|
|
104
|
+
outcome = { kind: 'error', error: new Error('timeout: body did not stop within 30s drain window') };
|
|
105
|
+
}
|
|
106
|
+
else
|
|
107
|
+
outcome = drained;
|
|
90
108
|
}
|
|
91
109
|
}
|
|
92
110
|
draft = outcome.kind === 'draft'
|
|
@@ -157,13 +175,17 @@ export async function withMergeRun(options) {
|
|
|
157
175
|
const finalPairs = draft.pairs.length > 0
|
|
158
176
|
? draft.pairs.map(pair => ({ pairKey: pair.pairKey, state: pair.outcome === 'merged' ? 'fused' : pair.outcome === 'pending' ? 'pending' : 'skipped' }))
|
|
159
177
|
: options.pairKey === undefined ? [] : [{ pairKey: options.pairKey, state: 'pending' }];
|
|
160
|
-
await
|
|
178
|
+
const currentMeta = await readRunMeta(options.io, options.home, handle.id);
|
|
179
|
+
await writeMeta({ version: 1, runId: handle.id, pid: process.pid, ...(currentMeta?.revision === undefined ? {} : { revision: currentMeta.revision }), ...(options.parentRunId === undefined ? {} : { parentRunId: options.parentRunId }), ...(options.pairKey === undefined ? {} : { pairKey: options.pairKey }), ...(options.stagingRef === undefined ? {} : { stagingRef: options.stagingRef }), ...(finalPairs.length === 0 ? {} : { pairs: finalPairs }), pairCount: options.pairCount ?? (options.pairKey === undefined ? 0 : 1), phase: awaiting ? 'awaiting-approval' : 'settled', approval: awaiting ? 'pending' : 'approved', stopReason: report.stopReason, reportRef: reportPath, createdAt: new Date(startedAt).toISOString(), updatedAt: new Date(options.now()).toISOString() });
|
|
161
180
|
if (report.stopReason === 'completed')
|
|
162
181
|
return { ok: true, runId: handle.id, reportPath, report };
|
|
163
182
|
return { ok: false, runId: handle.id, message: 'merge ' + report.stopReason, reportPath };
|
|
164
183
|
};
|
|
165
184
|
if (options.detached === true) {
|
|
166
|
-
void finish().catch(() => {
|
|
185
|
+
void finish().catch((error) => {
|
|
186
|
+
const warn = options.warn ?? ((message) => { console.warn(message); });
|
|
187
|
+
warn('evolution-merge: detached run ' + handle.id + ' finish failed: ' + (error instanceof Error ? error.message : String(error)));
|
|
188
|
+
});
|
|
167
189
|
return { ok: true, accepted: true, runId: handle.id };
|
|
168
190
|
}
|
|
169
191
|
return await finish();
|
package/lib/types/runtime.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SkillLibrary, type EvolutionIoLike, type UnitTransaction } from '@lmzhen/dsh-evolution-core';
|
|
2
|
-
import { type MergeCandidate, type MergeDecision } from './contracts.ts';
|
|
2
|
+
import { type MergeCandidate, type MergeDecision, type MergeRow } from './contracts.ts';
|
|
3
3
|
import type { MergeParams } from './params.ts';
|
|
4
4
|
import type { SkillCard } from './skill-card.ts';
|
|
5
5
|
import type { MergeRegistryPort } from './run-shell.ts';
|
|
@@ -7,6 +7,16 @@ import type { MergeCommandService } from './merge-command.ts';
|
|
|
7
7
|
import type { StageToolContext } from './stage-tool.ts';
|
|
8
8
|
import type { ConversationChannelLike } from './channel-port.ts';
|
|
9
9
|
import type { HostCapabilities } from './host-capabilities.ts';
|
|
10
|
+
interface BusyTarget {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly runId?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The approved/fuse row that already holds a target. Pair/approve move a row to
|
|
16
|
+
* approved/fuse BEFORE their run starts, so this row IS the in-flight claim a second
|
|
17
|
+
* same-target run would collide with (the pair itself is guarded by run-meta).
|
|
18
|
+
*/
|
|
19
|
+
export declare function busyTarget(rows: readonly MergeRow[], target: string, excludeRowId: string): BusyTarget | undefined;
|
|
10
20
|
export declare function forcedDecisionFor(a: string, b: string): MergeDecision;
|
|
11
21
|
export interface MergeStageBindingPort {
|
|
12
22
|
bind(context: StageToolContext): void;
|
|
@@ -44,7 +54,12 @@ export interface MergeRuntimeDeps {
|
|
|
44
54
|
readonly stage?: MergeStageBindingPort;
|
|
45
55
|
/** G2 transitional: when set, authoring commits through core UnitTransaction. */
|
|
46
56
|
readonly unit?: UnitTransaction;
|
|
57
|
+
/** Authority home fence for metadata writes. */
|
|
58
|
+
readonly fence?: {
|
|
59
|
+
run<T>(body: () => Promise<T>): Promise<T>;
|
|
60
|
+
};
|
|
47
61
|
}
|
|
48
62
|
export declare function unitFor(deps: MergeRuntimeDeps): UnitTransaction | undefined;
|
|
49
63
|
export declare function createMergeRuntime(deps: MergeRuntimeDeps): MergeCommandService;
|
|
64
|
+
export {};
|
|
50
65
|
//# sourceMappingURL=runtime.d.ts.map
|
package/lib/types/runtime.js
CHANGED
|
@@ -49,8 +49,12 @@ async function hostOnlyFailure(verb, capabilities) {
|
|
|
49
49
|
* approved/fuse BEFORE their run starts, so this row IS the in-flight claim a second
|
|
50
50
|
* same-target run would collide with (the pair itself is guarded by run-meta).
|
|
51
51
|
*/
|
|
52
|
-
function busyTarget(rows, target, excludeRowId) {
|
|
53
|
-
const row = rows.find(item => item.id !== excludeRowId
|
|
52
|
+
export function busyTarget(rows, target, excludeRowId) {
|
|
53
|
+
const row = rows.find(item => item.id !== excludeRowId
|
|
54
|
+
&& (item.state === 'approved' || item.state === 'fuse')
|
|
55
|
+
&& item.decision?.target === target
|
|
56
|
+
// P1-4: a non-retryable failure is auditable but must not hold the target forever.
|
|
57
|
+
&& !(item.failure !== undefined && item.failure.retryable !== true));
|
|
54
58
|
return row === undefined ? undefined : { id: row.id, ...(row.runId === undefined ? {} : { runId: row.runId }) };
|
|
55
59
|
}
|
|
56
60
|
export function forcedDecisionFor(a, b) {
|
|
@@ -59,8 +63,8 @@ export function forcedDecisionFor(a, b) {
|
|
|
59
63
|
const target = raw.length <= 64 ? raw : raw.slice(0, 55) + '-' + createHash('sha256').update(raw).digest('hex').slice(0, 8);
|
|
60
64
|
return { shouldMerge: true, mode: 'create-umbrella', target, sources: [a, b], rationale: 'forced pair merge of ' + sorted.join(' and '), confidence: 0.5, aSummary: 'A: ' + sorted[0] + ' 的现有技能面。', bSummary: 'B: ' + sorted[1] + ' 的现有技能面。' };
|
|
61
65
|
}
|
|
62
|
-
async function removeRuntimeRow(io, home, id, event, unitId) {
|
|
63
|
-
return await pairLedger(io, home).transact(tx => {
|
|
66
|
+
async function removeRuntimeRow(io, home, id, event, unitId, fence) {
|
|
67
|
+
return await pairLedger(io, home, fence).transact(tx => {
|
|
64
68
|
const current = tx.rows().find(item => item.id === id);
|
|
65
69
|
if (current === undefined)
|
|
66
70
|
return false;
|
|
@@ -76,6 +80,7 @@ export function unitFor(deps) {
|
|
|
76
80
|
return new UnitTransactions({ io: deps.io, home: deps.home, backend: skillLibraryUnitBackend(deps.skills), claim: targetClaimUnitPort(deps.io, deps.home, deps.now), now: deps.now });
|
|
77
81
|
}
|
|
78
82
|
export function createMergeRuntime(deps) {
|
|
83
|
+
const ledger = () => pairLedger(deps.io, deps.home, deps.fence);
|
|
79
84
|
const readOnly = createReadOnlyMergeService(deps.io, deps.home);
|
|
80
85
|
const timeout = deps.timeoutMinutes > 0 ? deps.timeoutMinutes : undefined;
|
|
81
86
|
const paramsOf = () => {
|
|
@@ -91,6 +96,7 @@ export function createMergeRuntime(deps) {
|
|
|
91
96
|
platformWrite: platformWriteOf(), now: deps.now,
|
|
92
97
|
...(deps.model === undefined ? {} : { model: deps.model }),
|
|
93
98
|
...(deps.isOwnerActive === undefined ? {} : { isOwnerActive: deps.isOwnerActive }),
|
|
99
|
+
...(deps.fence === undefined ? {} : { fence: deps.fence }),
|
|
94
100
|
...(timeout === undefined ? {} : { timeoutMinutes: timeout }),
|
|
95
101
|
});
|
|
96
102
|
const stage = deps.stage;
|
|
@@ -149,7 +155,7 @@ export function createMergeRuntime(deps) {
|
|
|
149
155
|
const existing = (await loadMergeState(deps.io, deps.home)).state.rows.find(row => row.pairKey === pairKey(command.a, command.b));
|
|
150
156
|
let cleared = false;
|
|
151
157
|
if (existing?.state === 'skipped' || (existing?.state === 'fuse' && existing.failure?.retryable === false)) {
|
|
152
|
-
const dropped = await removeRuntimeRow(deps.io, deps.home, existing.id, 'superseded');
|
|
158
|
+
const dropped = await removeRuntimeRow(deps.io, deps.home, existing.id, 'superseded', undefined, deps.fence);
|
|
153
159
|
if (!dropped)
|
|
154
160
|
return fail('pair', 'row-changed', '清理可替换行时发现并发修改:' + existing.id, '运行 /evolution merge status 后重试');
|
|
155
161
|
cleared = true;
|
|
@@ -310,7 +316,7 @@ export function createMergeRuntime(deps) {
|
|
|
310
316
|
const row = loaded.state.rows.find(item => item.id === command.id);
|
|
311
317
|
if (row === undefined)
|
|
312
318
|
return fail('reject', 'row-not-found', '找不到合并行 ' + command.id, '运行 /evolution merge pending 查看待处理行', command.id);
|
|
313
|
-
const changed = await
|
|
319
|
+
const changed = await ledger().transact(tx => {
|
|
314
320
|
const current = tx.rows().find(item => item.id === row.id);
|
|
315
321
|
if (current === undefined || current.state !== 'pending')
|
|
316
322
|
return false;
|
|
@@ -378,7 +384,7 @@ export function createMergeRuntime(deps) {
|
|
|
378
384
|
return fail('discard', 'row-not-found', '找不到合并行 ' + command.id, '运行 /evolution merge status 查看当前队列', command.id);
|
|
379
385
|
if (row.state !== 'pending' && row.state !== 'approved' && row.state !== 'fuse')
|
|
380
386
|
return fail('discard', 'row-not-discardable', '该行状态是 ' + row.state + ',不可丢弃:' + command.id, '只有 pending/approved/fuse 行可丢弃', row.runId);
|
|
381
|
-
const dropped = await removeRuntimeRow(deps.io, deps.home, row.id, 'superseded');
|
|
387
|
+
const dropped = await removeRuntimeRow(deps.io, deps.home, row.id, 'superseded', undefined, deps.fence);
|
|
382
388
|
if (!dropped)
|
|
383
389
|
return fail('discard', 'row-changed', '丢弃时发现并发修改:' + command.id, '运行 /evolution merge status ' + command.id + ' 后重试');
|
|
384
390
|
return { ok: true, text: '已丢弃合并行:' + command.id };
|
|
@@ -392,7 +398,7 @@ export function createMergeRuntime(deps) {
|
|
|
392
398
|
if (undoRegistry.runs('merge').some(run => run.id === undoRunId && run.supersededAt !== undefined))
|
|
393
399
|
return fail('undo', 'run-superseded', '该 run 已因技能树 restore 失效,不能再 undo:' + undoRunId, '运行 /evolution merge runs 查看失效标记');
|
|
394
400
|
const unit = unitFor(deps);
|
|
395
|
-
const result = await runUndoOperation({ io: deps.io, home: deps.home, runId: undoRunId, port: skillLibraryUndoPort(deps.skills), now: deps.now, target: command.target, ...(unit === undefined ? {} : { finalizeUnit: id => unit.finalize.call(unit, id) }) });
|
|
401
|
+
const result = await runUndoOperation({ io: deps.io, home: deps.home, runId: undoRunId, port: skillLibraryUndoPort(deps.skills), now: deps.now, target: command.target, ...(deps.fence === undefined ? {} : { fence: deps.fence }), ...(unit === undefined ? {} : { finalizeUnit: id => unit.finalize.call(unit, id) }) });
|
|
396
402
|
return result.ok ? { ok: true, text: result.message } : fail('undo', 'undo-failed', result.message, '检查回执与暂存后重试');
|
|
397
403
|
}
|
|
398
404
|
if (command.verb === 'skipped' && command.restore !== undefined) {
|
|
@@ -400,7 +406,7 @@ export function createMergeRuntime(deps) {
|
|
|
400
406
|
const row = loaded.state.rows.find(item => item.id === command.restore);
|
|
401
407
|
if (row === undefined || row.state !== 'skipped')
|
|
402
408
|
return fail('skipped', 'row-not-found', '找不到可恢复的 skipped 行:' + command.restore, '运行 /evolution merge skipped 查看跳过队列', command.restore);
|
|
403
|
-
const dropped = await removeRuntimeRow(deps.io, deps.home, row.id, 'restored');
|
|
409
|
+
const dropped = await removeRuntimeRow(deps.io, deps.home, row.id, 'restored', undefined, deps.fence);
|
|
404
410
|
if (!dropped)
|
|
405
411
|
return fail('skipped', 'row-changed', '恢复时发现并发修改:' + row.id, '运行 /evolution merge status 后重试');
|
|
406
412
|
return { ok: true, text: '已恢复参与筛选:' + row.id };
|
|
@@ -410,7 +416,7 @@ export function createMergeRuntime(deps) {
|
|
|
410
416
|
const cleared = (await loadMergeState(deps.io, deps.home)).state.rows.find(item => item.id === command.clear);
|
|
411
417
|
if (cleared === undefined)
|
|
412
418
|
return fail('resume', 'row-not-found', '找不到要清除的行:' + command.clear, '运行 /evolution merge status 查看当前队列');
|
|
413
|
-
const dropped = await removeRuntimeRow(deps.io, deps.home, cleared.id, 'superseded');
|
|
419
|
+
const dropped = await removeRuntimeRow(deps.io, deps.home, cleared.id, 'superseded', undefined, deps.fence);
|
|
414
420
|
if (!dropped)
|
|
415
421
|
return fail('resume', 'row-changed', '清除时发现并发修改:' + cleared.id, '运行 /evolution merge status 后重试');
|
|
416
422
|
return { ok: true, text: '已清除合并行:' + cleared.id };
|
|
@@ -508,7 +514,7 @@ export function createMergeRuntime(deps) {
|
|
|
508
514
|
}
|
|
509
515
|
const missingSource = [candidate.a, candidate.b].find(name => !cards.some(card => card.name === name && card.managed === true));
|
|
510
516
|
if (missingSource !== undefined) {
|
|
511
|
-
const dropped = await removeRuntimeRow(deps.io, deps.home, candidate.id, 'superseded');
|
|
517
|
+
const dropped = await removeRuntimeRow(deps.io, deps.home, candidate.id, 'superseded', undefined, deps.fence);
|
|
512
518
|
if (!dropped)
|
|
513
519
|
return fail('approve', 'row-changed', 'source-gone,但行已被并发修改,未清理:' + candidate.id, '运行 /evolution merge status ' + candidate.id + ' 后重试');
|
|
514
520
|
return fail('approve', 'source-gone', missingSource + ' 已不在 family-managed 技能库,已从 active 队列清除', '重新 pair 该技能对;剩余行用 /evolution merge discard ' + candidate.id, candidate.runId);
|
|
@@ -517,20 +523,20 @@ export function createMergeRuntime(deps) {
|
|
|
517
523
|
const targetExpected = candidate.decision?.targetRevisionRef ?? candidate.targetRevisionRef;
|
|
518
524
|
const targetMismatch = revisionMismatch(targetExpected, targetRevisionRefOf(cards, candidate.decision.target));
|
|
519
525
|
if (targetMismatch !== null) {
|
|
520
|
-
await
|
|
526
|
+
await ledger().transact(tx => { const current = tx.rows().find(item => item.id === candidate.id); return current === undefined ? false : tx.transition(candidate.id, { type: 'expired', kind: 'target-taken' }, current.version).ok; });
|
|
521
527
|
return fail('approve', 'target-taken', '目标 ' + candidate.decision.target + ' 在决策后已被占用', '重新 pair,或 /evolution merge discard ' + candidate.id, candidate.runId);
|
|
522
528
|
}
|
|
523
529
|
if (candidate.decision.supportDigest !== undefined && candidate.decision.supportDigest !== supportDigestOf(cards, [candidate.decision.target, candidate.a, candidate.b])) {
|
|
524
|
-
await
|
|
530
|
+
await ledger().transact(tx => { const current = tx.rows().find(item => item.id === candidate.id); return current === undefined ? false : tx.transition(candidate.id, { type: 'expired', kind: 'source-changed' }, current.version).ok; });
|
|
525
531
|
return fail('approve', 'source-changed', '源技能内容在决策后变化,拒绝照跑', '重新 pair,或 /evolution merge discard ' + candidate.id, candidate.runId);
|
|
526
532
|
}
|
|
527
533
|
if (revisionMismatch(sourceExpected, sourceRevisionRefOf(cards, candidate.a, candidate.b)) !== null) {
|
|
528
|
-
await
|
|
534
|
+
await ledger().transact(tx => { const current = tx.rows().find(item => item.id === candidate.id); return current === undefined ? false : tx.transition(candidate.id, { type: 'expired', kind: 'source-changed' }, current.version).ok; });
|
|
529
535
|
return fail('approve', 'source-changed', '源技能参考在决策后变化,拒绝照跑', '重新 pair,或 /evolution merge discard ' + candidate.id, candidate.runId);
|
|
530
536
|
}
|
|
531
537
|
const currentRevision = sourceRevisionOf(cards, candidate.a, candidate.b);
|
|
532
538
|
if (candidate.sourceRevision !== undefined && currentRevision !== undefined && currentRevision !== candidate.sourceRevision) {
|
|
533
|
-
const recorded = await
|
|
539
|
+
const recorded = await ledger().transact(tx => {
|
|
534
540
|
const current = tx.rows().find(item => item.id === candidate.id);
|
|
535
541
|
return current === undefined ? false : tx.transition(candidate.id, { type: 'expired', kind: 'source-changed' }, current.version).ok;
|
|
536
542
|
});
|
|
@@ -541,7 +547,7 @@ export function createMergeRuntime(deps) {
|
|
|
541
547
|
const currentTargetRevision = targetRevisionOf(cards, candidate.decision.target);
|
|
542
548
|
if (candidate.targetRevision !== undefined && currentTargetRevision !== undefined
|
|
543
549
|
&& currentTargetRevision !== candidate.targetRevision) {
|
|
544
|
-
const recorded = await
|
|
550
|
+
const recorded = await ledger().transact(tx => {
|
|
545
551
|
const current = tx.rows().find(item => item.id === candidate.id);
|
|
546
552
|
return current === undefined ? false : tx.transition(candidate.id, { type: 'expired', kind: 'target-taken' }, current.version).ok;
|
|
547
553
|
});
|
|
@@ -562,7 +568,7 @@ export function createMergeRuntime(deps) {
|
|
|
562
568
|
}
|
|
563
569
|
}
|
|
564
570
|
if (candidate.decision.mode === 'merge-into-existing' && !cards.some(card => card.name === candidate.decision?.target)) {
|
|
565
|
-
const recorded = await
|
|
571
|
+
const recorded = await ledger().transact(tx => {
|
|
566
572
|
const current = tx.rows().find(item => item.id === candidate.id);
|
|
567
573
|
return current === undefined ? false : tx.transition(candidate.id, { type: 'expired', kind: 'target-taken' }, current.version).ok;
|
|
568
574
|
});
|
|
@@ -7,6 +7,9 @@ import { type ScanPorts } from './scan-run.ts';
|
|
|
7
7
|
import { type MergeRegistryPort, type WithMergeRunResult } from './run-shell.ts';
|
|
8
8
|
export interface RunScanCommandOptions {
|
|
9
9
|
readonly io: EvolutionIoLike;
|
|
10
|
+
readonly fence?: {
|
|
11
|
+
run<T>(body: () => Promise<T>): Promise<T>;
|
|
12
|
+
};
|
|
10
13
|
readonly home: string;
|
|
11
14
|
readonly registry: MergeRegistryPort;
|
|
12
15
|
readonly ports?: ScanPorts;
|
|
@@ -5,6 +5,7 @@ export async function runScanCommand(options) {
|
|
|
5
5
|
io: options.io,
|
|
6
6
|
home: options.home,
|
|
7
7
|
registry: options.registry,
|
|
8
|
+
...(options.fence === undefined ? {} : { fence: options.fence }),
|
|
8
9
|
command: 'scan',
|
|
9
10
|
approval: { platformWrite: options.platformWrite, mergeApproval: options.approval },
|
|
10
11
|
now: options.now,
|
|
@@ -5,6 +5,7 @@ import { dirname, join } from 'node:path';
|
|
|
5
5
|
import { contentHash, isProcessAlive } from '@lmzhen/dsh-evolution-core';
|
|
6
6
|
import { acquireClaim, releaseClaim } from "./domain/claim.js";
|
|
7
7
|
const PROCESS_STARTED_AT = Date.now() - process.uptime() * 1000;
|
|
8
|
+
const TARGET_CLAIM_TTL_MS = 30 * 60_000;
|
|
8
9
|
function claimPath(home, target) {
|
|
9
10
|
return join(home, 'merge', 'claims', contentHash(target) + '.json');
|
|
10
11
|
}
|
|
@@ -24,10 +25,12 @@ async function withGuard(path, fn) {
|
|
|
24
25
|
const guard = path + '.guard';
|
|
25
26
|
await mkdir(dirname(path), { recursive: true });
|
|
26
27
|
for (let attempt = 0; attempt < 500; attempt += 1) {
|
|
28
|
+
let handle;
|
|
27
29
|
try {
|
|
28
|
-
|
|
30
|
+
handle = await open(guard, 'wx');
|
|
29
31
|
await handle.writeFile(String(process.pid));
|
|
30
32
|
await handle.close();
|
|
33
|
+
handle = undefined;
|
|
31
34
|
try {
|
|
32
35
|
return await fn();
|
|
33
36
|
}
|
|
@@ -36,6 +39,9 @@ async function withGuard(path, fn) {
|
|
|
36
39
|
}
|
|
37
40
|
}
|
|
38
41
|
catch (error) {
|
|
42
|
+
await handle?.close().catch(() => { });
|
|
43
|
+
if (error.code !== 'EEXIST')
|
|
44
|
+
await unlink(guard).catch(() => { });
|
|
39
45
|
if (error.code !== 'EEXIST')
|
|
40
46
|
throw error;
|
|
41
47
|
const pid = Number(await readFile(guard, 'utf8').catch(() => ''));
|
|
@@ -83,7 +89,7 @@ export async function acquireTargetClaim(io, home, target, now) {
|
|
|
83
89
|
const reused = current !== undefined && current.pid === process.pid && current.startedAt !== undefined && Math.abs(current.startedAt - PROCESS_STARTED_AT) > 1_000;
|
|
84
90
|
const abandoned = reused || (current !== undefined && current.expiresAt > now
|
|
85
91
|
&& current.pid !== undefined && current.pid !== process.pid && !isProcessAlive(current.pid));
|
|
86
|
-
const claim = acquireClaim(abandoned ? undefined : current, now,
|
|
92
|
+
const claim = acquireClaim(abandoned ? undefined : current, now, TARGET_CLAIM_TTL_MS, token, process.pid, PROCESS_STARTED_AT);
|
|
87
93
|
if (claim === undefined)
|
|
88
94
|
return raw;
|
|
89
95
|
granted = claim;
|
|
@@ -21,5 +21,8 @@ export declare function runUndoOperation(options: {
|
|
|
21
21
|
readonly message?: string;
|
|
22
22
|
}>;
|
|
23
23
|
readonly onPhase?: (phase: string, pairKey: string) => void | Promise<void>;
|
|
24
|
+
readonly fence?: {
|
|
25
|
+
run<T>(body: () => Promise<T>): Promise<T>;
|
|
26
|
+
};
|
|
24
27
|
}): Promise<UndoResult>;
|
|
25
28
|
//# sourceMappingURL=undo-operation.d.ts.map
|