@lmzhen/dsh-evolution-merge 0.33.0 → 0.35.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 CHANGED
@@ -3700,9 +3700,11 @@ function runView(run, readAt, includeResultRef = false) {
3700
3700
  const record = run.record;
3701
3701
  const state = run.meta?.phase === "undone" ? "undone" : run.meta?.stopReason === "interrupted" ? "interrupted" : run.meta?.approval === "pending" ? "awaiting-approval" : record.state;
3702
3702
  const stopReason = run.meta?.stopReason ?? (record.state === "cancelled" ? "cancelled" : record.state === "failed" ? "failed" : record.state === "succeeded" ? "completed" : void 0);
3703
- const progress = record.progress === void 0 ? null : {
3704
- done: state === "succeeded" || state === "undone" ? Math.max(record.progress.done, run.pairs.length) : record.progress.done,
3705
- total: record.progress.total
3703
+ const reportPairs = run.report?.processedPairs?.length ?? run.report?.pairs.length;
3704
+ const terminal = state === "succeeded" || state === "undone";
3705
+ const progress = record.progress === void 0 && reportPairs === void 0 ? null : {
3706
+ done: terminal ? Math.max(record.progress?.done ?? 0, reportPairs ?? 0, run.pairs.length) : record.progress?.done ?? reportPairs ?? 0,
3707
+ total: record.progress?.total ?? reportPairs ?? 0
3706
3708
  };
3707
3709
  return {
3708
3710
  wireVersion: 1,
@@ -5039,14 +5041,17 @@ function toHistoryView(items, readAt) {
5039
5041
  return {
5040
5042
  wireVersion: 1,
5041
5043
  readAt,
5042
- items: items.map((item) => ({
5043
- name: item.name,
5044
- kind: item.kind,
5045
- reason: item.reason,
5046
- lastVersion: item.lastVersion,
5047
- archivedAt: Date.parse(item.archivedAt),
5048
- ...item.sourceRunId === void 0 ? {} : { sourceRunId: item.sourceRunId }
5049
- }))
5044
+ items: items.map((item) => {
5045
+ const parsed = item.archivedAt === null ? null : typeof item.archivedAt === "number" ? item.archivedAt : Date.parse(item.archivedAt);
5046
+ return {
5047
+ name: item.name,
5048
+ kind: item.kind,
5049
+ reason: item.reason,
5050
+ lastVersion: item.lastVersion,
5051
+ archivedAt: parsed === null || Number.isFinite(parsed) ? parsed : null,
5052
+ ...item.sourceRunId === void 0 ? {} : { sourceRunId: item.sourceRunId }
5053
+ };
5054
+ })
5050
5055
  };
5051
5056
  }
5052
5057
  //#endregion
@@ -5057,7 +5062,8 @@ const MERGE_ROUTES = {
5057
5062
  view: MERGE_PATHS.view,
5058
5063
  rows: MERGE_PATHS.rows,
5059
5064
  body: MERGE_PATHS.body ?? MERGE_BASE + "body",
5060
- history: MERGE_PATHS.history ?? MERGE_BASE + "history"
5065
+ history: MERGE_PATHS.history ?? MERGE_BASE + "history",
5066
+ decision: MERGE_BASE + "decision"
5061
5067
  };
5062
5068
  function makeMergeRoutes(deps, _audit) {
5063
5069
  const absent = () => json(404, {
@@ -5172,7 +5178,6 @@ function makeMergeRoutes(deps, _audit) {
5172
5178
  const id = query.get("id");
5173
5179
  const name = query.get("name");
5174
5180
  const side = query.get("side");
5175
- if (id !== null && name !== null) return bad("id and name are mutually exclusive");
5176
5181
  if ((id === null || id === "") && (name === null || name === "")) return json(404, {
5177
5182
  wireVersion: 1,
5178
5183
  code: "merge-body-not-found",
@@ -5185,21 +5190,21 @@ function makeMergeRoutes(deps, _audit) {
5185
5190
  code: "merge-state-blocked",
5186
5191
  readiness: snapshot.blocked.readiness
5187
5192
  });
5188
- const refs = id !== null && id !== "" ? snapshot.bodyRefs.filter((ref) => ref.rowId === id) : snapshot.bodyRefs.filter((ref) => ref.name === name);
5193
+ const sideFilter = side === "a" ? "live" : side === "b" ? "archive" : side === "live" || side === "archive" ? side : void 0;
5194
+ const refs = snapshot.bodyRefs.filter((ref) => (id === null || id === "" || ref.rowId === id) && (name === null || name === "" || ref.name === name) && (sideFilter === void 0 || ref.side === sideFilter));
5189
5195
  if (refs.length === 0) return json(404, {
5190
5196
  wireVersion: 1,
5191
5197
  code: "merge-body-not-found",
5192
5198
  message: "merge body not found"
5193
5199
  });
5194
- if (name !== null && refs.length > 1) return bad("name matches multiple merge rows");
5200
+ if (refs.length > 1) return bad("merge body is ambiguous; pass id, name and side");
5195
5201
  const row = snapshot.rows.find((item) => item.row.id === refs[0].rowId);
5196
5202
  if (row?.stage?.state === "lost" || row?.stage?.state === "expired") return json(404, {
5197
5203
  wireVersion: 1,
5198
5204
  code: "merge-body-not-found",
5199
5205
  message: "merge stage is lost"
5200
5206
  });
5201
- const sideFilter = side === "a" ? "live" : side === "b" ? "archive" : side === "live" || side === "archive" ? side : void 0;
5202
- const ref = (sideFilter === void 0 ? refs[0] : refs.find((item) => item.side === sideFilter)) ?? refs[0];
5207
+ const ref = refs[0];
5203
5208
  return json(200, buildBodyView(snapshot, ref, (source.bodyRef === void 0 ? null : await source.bodyRef(ref)) ?? ""));
5204
5209
  }
5205
5210
  const readiness = await notReady(source);
@@ -5235,6 +5240,35 @@ function makeMergeRoutes(deps, _audit) {
5235
5240
  if (readiness !== null) return readiness;
5236
5241
  return json(200, toHistoryView(await source.history?.() ?? [], Date.now()));
5237
5242
  }
5243
+ },
5244
+ {
5245
+ path: MERGE_ROUTES.decision,
5246
+ methods: ["POST"],
5247
+ requestBody: "buffered",
5248
+ fetch: async (request) => {
5249
+ if (request.method !== "POST") return methodNotAllowed(["POST"]);
5250
+ if (deps.decide === void 0) return absent();
5251
+ let body;
5252
+ try {
5253
+ body = await request.json();
5254
+ } catch {
5255
+ return bad("decision body must be JSON");
5256
+ }
5257
+ if (typeof body !== "object" || body === null) return bad("decision body must be an object");
5258
+ const input = body;
5259
+ if (typeof input.rowId !== "string" || input.rowId === "") return bad("rowId is required");
5260
+ if (input.verb !== "approve" && input.verb !== "reject") return bad("verb must be approve or reject");
5261
+ if (typeof input.sessionId !== "string" || input.sessionId === "") return bad("sessionId is required");
5262
+ const result = await deps.decide({
5263
+ rowId: input.rowId,
5264
+ verb: input.verb,
5265
+ sessionId: input.sessionId
5266
+ }, _audit);
5267
+ return json(result.ok ? 200 : result.code === "E-305" ? 409 : 400, {
5268
+ wireVersion: 1,
5269
+ ...result
5270
+ });
5271
+ }
5238
5272
  }
5239
5273
  ];
5240
5274
  }
@@ -9262,16 +9296,22 @@ function unitFor(deps) {
9262
9296
  function createMergeRuntime(deps) {
9263
9297
  const readOnly = createReadOnlyMergeService(deps.io, deps.home);
9264
9298
  const timeout = deps.timeoutMinutes > 0 ? deps.timeoutMinutes : void 0;
9265
- const common = {
9299
+ const paramsOf = () => {
9300
+ if (deps.getParams !== void 0) return deps.getParams();
9301
+ if (deps.params !== void 0) return deps.params;
9302
+ throw new Error("merge runtime requires getParams or params");
9303
+ };
9304
+ const platformWriteOf = () => deps.getPlatformWrite?.() ?? deps.platformWrite ?? false;
9305
+ const common = () => ({
9266
9306
  io: deps.io,
9267
9307
  home: deps.home,
9268
9308
  registry: deps.registry,
9269
- platformWrite: deps.platformWrite,
9309
+ platformWrite: platformWriteOf(),
9270
9310
  now: deps.now,
9271
9311
  ...deps.model === void 0 ? {} : { model: deps.model },
9272
9312
  ...deps.isOwnerActive === void 0 ? {} : { isOwnerActive: deps.isOwnerActive },
9273
9313
  ...timeout === void 0 ? {} : { timeoutMinutes: timeout }
9274
- };
9314
+ });
9275
9315
  const stage = deps.stage;
9276
9316
  const stageHooks = stage === void 0 ? {} : {
9277
9317
  onChild: (context) => {
@@ -9285,6 +9325,7 @@ function createMergeRuntime(deps) {
9285
9325
  const parent = context?.parent ?? deps.parent;
9286
9326
  const agentless = context?.agentless === true;
9287
9327
  const detached = deps.detached !== false;
9328
+ const params = paramsOf();
9288
9329
  if (command.verb === "pair") {
9289
9330
  const signal = new AbortController().signal;
9290
9331
  if (command.a === command.b) return fail("pair", "pair-same-source", "merge pair 需要两个不同技能源:" + command.a, "重新指定两个不同的 family-managed 技能");
@@ -9324,9 +9365,9 @@ function createMergeRuntime(deps) {
9324
9365
  if (existing?.state === "pending") return fail("pair", "pair-pending", "该 pair 已在待批准队列:" + existing.id, "先运行 /evolution merge approve/reject/discard " + existing.id, existing.runId);
9325
9366
  if (!cleared && (existing?.state === "approved" || existing?.state === "fuse")) return fail("pair", "pair-fuse", "该 pair 已在待合并队列,可复用决策重跑", "运行 /evolution merge resume --retry " + (existing.runId ?? existing.id), existing.runId);
9326
9367
  const forced = command.force === true ? forcedDecisionFor(command.a, command.b) : void 0;
9327
- const approval = deps.params.mergeApproval;
9368
+ const approval = params.mergeApproval;
9328
9369
  const result = await runScanCommand({
9329
- ...common,
9370
+ ...common(),
9330
9371
  approval,
9331
9372
  cards,
9332
9373
  ...detached ? { detached: true } : {},
@@ -9341,12 +9382,12 @@ function createMergeRuntime(deps) {
9341
9382
  parent,
9342
9383
  allow: deps.allow,
9343
9384
  ...deps.cwd === void 0 ? {} : { cwd: deps.cwd },
9344
- timeoutMs: deps.params.mergePairTimeoutMinutes * 6e4,
9385
+ timeoutMs: params.mergePairTimeoutMinutes * 6e4,
9345
9386
  approval,
9346
9387
  commitPort: skillLibraryPort(deps.skills),
9347
9388
  now: deps.now,
9348
9389
  ...unitFor(deps) === void 0 ? {} : { unit: unitFor(deps) },
9349
- ...deps.params.limitsSnapshot === void 0 ? {} : { limits: deps.params.limitsSnapshot.skillLimits },
9390
+ ...params.limitsSnapshot === void 0 ? {} : { limits: params.limitsSnapshot.skillLimits },
9350
9391
  ...deps.isBlocked === void 0 ? {} : { isBlocked: deps.isBlocked },
9351
9392
  ...stageHooks
9352
9393
  });
@@ -9380,12 +9421,12 @@ function createMergeRuntime(deps) {
9380
9421
  for (const card of cards) if (await deps.isBlocked?.(card.name) === true) blocked.add(card.name);
9381
9422
  const managed = new Set(cards.filter((card) => card.managed === true && card.protectedBy == null && !blocked.has(card.name)).map((card) => card.name));
9382
9423
  const loadedState = await loadMergeState(deps.io, deps.home);
9383
- const skipped = skippedPairKeys(loadedState.state.rows, deps.params.mergeSkippedTtlDays, deps.now());
9424
+ const skipped = skippedPairKeys(loadedState.state.rows, params.mergeSkippedTtlDays, deps.now());
9384
9425
  const held = new Set(loadedState.state.rows.filter((row) => row.queue === "pending" || row.queue === "fuse" || row.queue === "verify" && (row.runId !== void 0 && deps.now() - Date.parse(row.updatedAt) < 10 * 6e4 || !canRetry(row))).map((row) => row.pairKey));
9385
9426
  const notes = 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);
9386
9427
  const result = await runScanCommand({
9387
- ...common,
9388
- approval: deps.params.mergeApproval,
9428
+ ...common(),
9429
+ approval: params.mergeApproval,
9389
9430
  notes,
9390
9431
  cards,
9391
9432
  ...detached ? { detached: true } : {},
@@ -9417,12 +9458,12 @@ function createMergeRuntime(deps) {
9417
9458
  parent,
9418
9459
  allow: deps.allow,
9419
9460
  ...deps.cwd === void 0 ? {} : { cwd: deps.cwd },
9420
- timeoutMs: deps.params.mergePairTimeoutMinutes * 6e4,
9421
- approval: deps.params.mergeApproval,
9461
+ timeoutMs: params.mergePairTimeoutMinutes * 6e4,
9462
+ approval: params.mergeApproval,
9422
9463
  commitPort: skillLibraryPort(deps.skills),
9423
9464
  now: deps.now,
9424
9465
  ...unitFor(deps) === void 0 ? {} : { unit: unitFor(deps) },
9425
- ...deps.params.limitsSnapshot === void 0 ? {} : { limits: deps.params.limitsSnapshot.skillLimits },
9466
+ ...params.limitsSnapshot === void 0 ? {} : { limits: params.limitsSnapshot.skillLimits },
9426
9467
  ...deps.isBlocked === void 0 ? {} : { isBlocked: deps.isBlocked },
9427
9468
  ...stageHooks
9428
9469
  })
@@ -9450,8 +9491,8 @@ function createMergeRuntime(deps) {
9450
9491
  b: row.b
9451
9492
  };
9452
9493
  const result = await runScanCommand({
9453
- ...common,
9454
- approval: deps.params.mergeApproval,
9494
+ ...common(),
9495
+ approval: params.mergeApproval,
9455
9496
  notes: ["candidates --retry " + row.id],
9456
9497
  cards,
9457
9498
  makePorts: (runId) => createChannelScanPorts({
@@ -9468,12 +9509,12 @@ function createMergeRuntime(deps) {
9468
9509
  parent,
9469
9510
  allow: deps.allow,
9470
9511
  ...deps.cwd === void 0 ? {} : { cwd: deps.cwd },
9471
- timeoutMs: deps.params.mergePairTimeoutMinutes * 6e4,
9472
- approval: deps.params.mergeApproval,
9512
+ timeoutMs: params.mergePairTimeoutMinutes * 6e4,
9513
+ approval: params.mergeApproval,
9473
9514
  commitPort: skillLibraryPort(deps.skills),
9474
9515
  now: deps.now,
9475
9516
  ...unitFor(deps) === void 0 ? {} : { unit: unitFor(deps) },
9476
- ...deps.params.limitsSnapshot === void 0 ? {} : { limits: deps.params.limitsSnapshot.skillLimits },
9517
+ ...params.limitsSnapshot === void 0 ? {} : { limits: params.limitsSnapshot.skillLimits },
9477
9518
  ...deps.isBlocked === void 0 ? {} : { isBlocked: deps.isBlocked },
9478
9519
  ...stageHooks
9479
9520
  })
@@ -9515,7 +9556,7 @@ function createMergeRuntime(deps) {
9515
9556
  io: deps.io,
9516
9557
  home: deps.home,
9517
9558
  now: deps.now,
9518
- ttlDays: deps.params.mergeSkippedTtlDays,
9559
+ ttlDays: params.mergeSkippedTtlDays,
9519
9560
  ...managed === void 0 ? {} : { managed },
9520
9561
  ...live === void 0 ? {} : { live }
9521
9562
  });
@@ -9530,7 +9571,7 @@ function createMergeRuntime(deps) {
9530
9571
  }
9531
9572
  if (command.verb === "doctor") return {
9532
9573
  ok: true,
9533
- text: (await mergeDoctor(deps.io, deps.home, deps.params.mergeModelsRoot)).map((row) => row.key + " [" + row.status + "] " + row.label + ":" + row.detail).join("\n")
9574
+ text: (await mergeDoctor(deps.io, deps.home, params.mergeModelsRoot)).map((row) => row.key + " [" + row.status + "] " + row.label + ":" + row.detail).join("\n")
9534
9575
  };
9535
9576
  if (command.verb === "report" || command.verb === "runs" || command.verb === "cancel") {
9536
9577
  const registry = deps.registry;
@@ -9542,7 +9583,7 @@ function createMergeRuntime(deps) {
9542
9583
  io: deps.io,
9543
9584
  home: deps.home,
9544
9585
  runs: mergeRuns,
9545
- approval: deps.params.mergeApproval,
9586
+ approval: params.mergeApproval,
9546
9587
  now: deps.now
9547
9588
  })))
9548
9589
  };
@@ -9658,13 +9699,13 @@ function createMergeRuntime(deps) {
9658
9699
  round2: () => failed("author", "resume-needs-child", "fuse resume needs a new child")
9659
9700
  };
9660
9701
  const result = await runResumeCommand({
9661
- ...common,
9702
+ ...common(),
9662
9703
  rowId: row.id,
9663
9704
  ports: basePorts,
9664
- approval: deps.params.mergeApproval,
9705
+ approval: params.mergeApproval,
9665
9706
  ...agentless ? { agentless: true } : {},
9666
9707
  ...detached ? { detached: true } : {},
9667
- pairTimeoutMinutes: deps.params.mergePairTimeoutMinutes,
9708
+ pairTimeoutMinutes: params.mergePairTimeoutMinutes,
9668
9709
  ...deps.isOwnerActive === void 0 ? {} : { isOwnerActive: deps.isOwnerActive },
9669
9710
  makePorts: (runId) => createChannelScanPorts({
9670
9711
  io: deps.io,
@@ -9676,12 +9717,12 @@ function createMergeRuntime(deps) {
9676
9717
  parent,
9677
9718
  allow: deps.allow,
9678
9719
  ...deps.cwd === void 0 ? {} : { cwd: deps.cwd },
9679
- timeoutMs: deps.params.mergePairTimeoutMinutes * 6e4,
9680
- approval: deps.params.mergeApproval,
9720
+ timeoutMs: params.mergePairTimeoutMinutes * 6e4,
9721
+ approval: params.mergeApproval,
9681
9722
  commitPort: skillLibraryPort(deps.skills),
9682
9723
  now: deps.now,
9683
9724
  ...unitFor(deps) === void 0 ? {} : { unit: unitFor(deps) },
9684
- ...deps.params.limitsSnapshot === void 0 ? {} : { limits: deps.params.limitsSnapshot.skillLimits },
9725
+ ...params.limitsSnapshot === void 0 ? {} : { limits: params.limitsSnapshot.skillLimits },
9685
9726
  ...deps.isBlocked === void 0 ? {} : { isBlocked: deps.isBlocked }
9686
9727
  }),
9687
9728
  round2WithRun: createNewChildRound2({
@@ -9693,11 +9734,11 @@ function createMergeRuntime(deps) {
9693
9734
  parentRunId: (runId) => runId,
9694
9735
  allow: deps.allow,
9695
9736
  ...deps.cwd === void 0 ? {} : { cwd: deps.cwd },
9696
- timeoutMs: deps.params.mergePairTimeoutMinutes * 6e4,
9737
+ timeoutMs: params.mergePairTimeoutMinutes * 6e4,
9697
9738
  commitPort: skillLibraryPort(deps.skills),
9698
9739
  now: deps.now,
9699
9740
  ...unitFor(deps) === void 0 ? {} : { unit: unitFor(deps) },
9700
- ...deps.params.limitsSnapshot === void 0 ? {} : { limits: deps.params.limitsSnapshot.skillLimits },
9741
+ ...params.limitsSnapshot === void 0 ? {} : { limits: params.limitsSnapshot.skillLimits },
9701
9742
  ...deps.isBlocked === void 0 ? {} : { isBlocked: deps.isBlocked },
9702
9743
  ...stageHooks
9703
9744
  })
@@ -9812,11 +9853,11 @@ function createMergeRuntime(deps) {
9812
9853
  }
9813
9854
  }
9814
9855
  const result = await runApproveCommand({
9815
- ...common,
9856
+ ...common(),
9816
9857
  rowId: command.id,
9817
9858
  ...agentless ? { agentless: true } : {},
9818
9859
  ...detached ? { detached: true } : {},
9819
- pairTimeoutMinutes: deps.params.mergePairTimeoutMinutes,
9860
+ pairTimeoutMinutes: params.mergePairTimeoutMinutes,
9820
9861
  ports: {
9821
9862
  stage: createStageForCards({
9822
9863
  io: deps.io,
@@ -9834,11 +9875,11 @@ function createMergeRuntime(deps) {
9834
9875
  parentRunId: (runId) => runId,
9835
9876
  allow: deps.allow,
9836
9877
  ...deps.cwd === void 0 ? {} : { cwd: deps.cwd },
9837
- timeoutMs: deps.params.mergePairTimeoutMinutes * 6e4,
9878
+ timeoutMs: params.mergePairTimeoutMinutes * 6e4,
9838
9879
  commitPort: skillLibraryPort(deps.skills),
9839
9880
  now: deps.now,
9840
9881
  ...unitFor(deps) === void 0 ? {} : { unit: unitFor(deps) },
9841
- ...deps.params.limitsSnapshot === void 0 ? {} : { limits: deps.params.limitsSnapshot.skillLimits },
9882
+ ...params.limitsSnapshot === void 0 ? {} : { limits: params.limitsSnapshot.skillLimits },
9842
9883
  ...deps.isBlocked === void 0 ? {} : { isBlocked: deps.isBlocked },
9843
9884
  ...stageHooks
9844
9885
  })
@@ -9865,7 +9906,7 @@ function createMergeRuntime(deps) {
9865
9906
  io: deps.io,
9866
9907
  home: deps.home,
9867
9908
  runs: runs !== void 0 && runs.some((item) => item.id === found.id) ? runs : [...runs ?? [], found],
9868
- approval: deps.params.mergeApproval,
9909
+ approval: params.mergeApproval,
9869
9910
  now: deps.now
9870
9911
  });
9871
9912
  if (snapshot.blocked !== void 0) return fail("status", snapshot.blocked.code, snapshot.blocked.message, "先修复合并状态(/evolution merge doctor),再查看运行");
@@ -9882,7 +9923,7 @@ function createMergeRuntime(deps) {
9882
9923
  const list = cards ?? await deps.cards(new AbortController().signal).catch(() => []);
9883
9924
  return list.length === 0 ? void 0 : liveRowFactsOf(list);
9884
9925
  };
9885
- const text = await handleReadOnlyMergeCommand(command, deps.io, deps.home, cards, runs, deps.params.mergeApproval, liveForRead);
9926
+ const text = await handleReadOnlyMergeCommand(command, deps.io, deps.home, cards, runs, params.mergeApproval, liveForRead);
9886
9927
  if (text !== null) return {
9887
9928
  ok: true,
9888
9929
  text
@@ -10546,8 +10587,9 @@ const inject = [];
10546
10587
  function apply(ctx, rawConfig = {}) {
10547
10588
  registerOwnerSettings(ctx, "evolution-merge", Config);
10548
10589
  configureMergePresentation(ctx);
10549
- let params = resolveMergeParams(rawConfig);
10590
+ const startupParams = resolveMergeParams(rawConfig);
10550
10591
  let mergeRun;
10592
+ let decide;
10551
10593
  let source;
10552
10594
  let doctorFact;
10553
10595
  const mergeService = {
@@ -10569,7 +10611,17 @@ function apply(ctx, rawConfig = {}) {
10569
10611
  };
10570
10612
  const mergeContext = ctx;
10571
10613
  if (typeof mergeContext.provide === "function") mergeContext.provide("evolutionMerge", mergeService);
10572
- registerHostRoutes(ctx, (audit) => makeMergeRoutes({ getSource: () => source }, audit));
10614
+ registerHostRoutes(ctx, (audit) => makeMergeRoutes({
10615
+ getSource: () => source,
10616
+ decide: async (request, _audit) => {
10617
+ if (decide === void 0) return {
10618
+ ok: false,
10619
+ code: "E-305",
10620
+ message: "合并服务尚未完成挂载"
10621
+ };
10622
+ return await decide(request);
10623
+ }
10624
+ }, audit));
10573
10625
  ctx.inject(["commands", "evolutionIo"], (scope) => {
10574
10626
  const registry = scope.get("evolutionIo");
10575
10627
  if (registry === void 0) return;
@@ -10620,13 +10672,15 @@ function apply(ctx, rawConfig = {}) {
10620
10672
  });
10621
10673
  }, "evolution-merge: restore supersede");
10622
10674
  const limitsSnapshot = resolveLimitsSnapshot(ctx.get("evolutionPolicy")?.get?.());
10623
- const hostApproval = ctx.get("evolutionApproval");
10624
- const platformWrite = typeof hostApproval?.platformWrite === "boolean" ? hostApproval.platformWrite : params.platformWrite ?? false;
10625
- params = {
10626
- ...params,
10627
- limitsSnapshot,
10628
- platformWrite
10675
+ const getPlatformWrite = () => {
10676
+ const hostApproval = ctx.get("evolutionApproval");
10677
+ return typeof hostApproval?.platformWrite === "boolean" ? hostApproval.platformWrite : resolveMergeParams(rawConfig).platformWrite ?? false;
10629
10678
  };
10679
+ const getParams = () => ({
10680
+ ...resolveMergeParams(rawConfig),
10681
+ limitsSnapshot,
10682
+ platformWrite: getPlatformWrite()
10683
+ });
10630
10684
  const skills = newSkillLibrary({
10631
10685
  io,
10632
10686
  ctx,
@@ -10669,7 +10723,7 @@ function apply(ctx, rawConfig = {}) {
10669
10723
  io,
10670
10724
  home,
10671
10725
  now: () => Date.now(),
10672
- ttlDays: params.mergeSkippedTtlDays,
10726
+ ttlDays: getParams().mergeSkippedTtlDays,
10673
10727
  ...live === void 0 ? {} : { live }
10674
10728
  });
10675
10729
  if (reconciled.actions.length > 0) ctx.logger?.info?.("merge reconcile: " + String(reconciled.actions.length) + " action(s)");
@@ -10706,7 +10760,7 @@ function apply(ctx, rawConfig = {}) {
10706
10760
  } : void 0;
10707
10761
  });
10708
10762
  const stage = new StageToolRegistry();
10709
- const engineInput = resolveEngineInput({ modelsRoot: params.mergeModelsRoot });
10763
+ const engineInput = resolveEngineInput({ modelsRoot: startupParams.mergeModelsRoot });
10710
10764
  const engineConfig = engineConfigFromRoot(engineInput.modelsRoot);
10711
10765
  const llm = ctx.get("llm");
10712
10766
  const defaultModel = ctx.get("agentDefaultModel");
@@ -10762,12 +10816,13 @@ function apply(ctx, rawConfig = {}) {
10762
10816
  });
10763
10817
  source = {
10764
10818
  async snapshot() {
10819
+ const current = getParams();
10765
10820
  return await readMergeSnapshot({
10766
10821
  io,
10767
10822
  home,
10768
10823
  runs: runs.runs("merge"),
10769
- approval: params.mergeApproval,
10770
- platformWrite,
10824
+ approval: current.mergeApproval,
10825
+ platformWrite: getPlatformWrite(),
10771
10826
  hostOnly: hostCapabilities.hostOnly,
10772
10827
  live: async () => liveRowFactsOf(await readSkillCards(skills)),
10773
10828
  history: async () => {
@@ -10788,18 +10843,20 @@ function apply(ctx, rawConfig = {}) {
10788
10843
  });
10789
10844
  const archived = await skills.archivedNames?.() ?? [];
10790
10845
  const reasonOf = skills.readArchiveReason;
10846
+ const archivedAtOf = skills.readArchivedAt;
10791
10847
  for (const name of archived) {
10792
10848
  if (items.has(name)) continue;
10793
10849
  const rawReason = reasonOf === void 0 ? null : await reasonOf.call(skills, name).catch(() => null);
10794
10850
  const lower = (rawReason ?? "").toLowerCase();
10795
10851
  const kind = lower.includes("split") ? "split" : lower.includes("delet") || lower.includes("remove") ? "deleted" : "other";
10852
+ const archivedAt = archivedAtOf === void 0 ? null : await archivedAtOf.call(skills, name).catch(() => null);
10796
10853
  items.set(name, {
10797
10854
  name,
10798
10855
  kind,
10799
10856
  reason: rawReason ?? "技能已归档,最后一版保留可查。",
10800
10857
  reasonCode: kind === "other" ? "archived" : kind,
10801
10858
  lastVersion: 1,
10802
- archivedAt: Date.now()
10859
+ archivedAt
10803
10860
  });
10804
10861
  }
10805
10862
  const out = [];
@@ -10819,7 +10876,7 @@ function apply(ctx, rawConfig = {}) {
10819
10876
  return await resolveBody(io, home, ref, async () => ref.side === "archive" ? await skills.readArchived(ref.name) : await skills.read(ref.name));
10820
10877
  },
10821
10878
  approval() {
10822
- return params.mergeApproval;
10879
+ return getParams().mergeApproval;
10823
10880
  },
10824
10881
  readiness() {
10825
10882
  return ready;
@@ -10855,13 +10912,13 @@ function apply(ctx, rawConfig = {}) {
10855
10912
  registry: runs,
10856
10913
  skills,
10857
10914
  now: () => Date.now(),
10858
- params,
10859
- platformWrite,
10915
+ getParams,
10916
+ getPlatformWrite,
10860
10917
  ...defaultModel === void 0 ? {} : { model: defaultModel.currentSelection().model },
10861
10918
  channel,
10862
10919
  parent: void 0,
10863
10920
  allow: ["merge_stage"],
10864
- timeoutMinutes: params.mergeRunTimeoutMinutes,
10921
+ timeoutMinutes: startupParams.mergeRunTimeoutMinutes,
10865
10922
  hostCapabilities,
10866
10923
  isBlocked: async (name) => {
10867
10924
  return await ctx.get("evolutionCurator")?.isBlocked?.(name) === true;
@@ -10893,6 +10950,20 @@ function apply(ctx, rawConfig = {}) {
10893
10950
  };
10894
10951
  return runMergeCommand(runtime, rawInput, invocation);
10895
10952
  };
10953
+ const agents = ctx.get("agents");
10954
+ decide = async ({ rowId, verb, sessionId }) => {
10955
+ const agent = agents?.get(sessionId);
10956
+ if (agent === void 0 || agent === null) return {
10957
+ ok: false,
10958
+ code: "E-305",
10959
+ message: "当前会话没有可用的 Agent;请在会话打开时操作,或复制命令到 CLI 执行"
10960
+ };
10961
+ const result = await mergeService.run("/evolution merge " + verb + " " + rowId, { agent });
10962
+ return {
10963
+ ok: result.ok,
10964
+ message: result.text
10965
+ };
10966
+ };
10896
10967
  const command = createMergeCommand(runtime);
10897
10968
  if (command !== null) scope.effect(() => commands.register(command), "evolution-merge: command");
10898
10969
  ctx.inject(["tools"], (toolScope) => {
@@ -47,7 +47,7 @@ export interface HistoryItem {
47
47
  readonly reason: string;
48
48
  readonly reasonCode?: string;
49
49
  readonly lastVersion: number;
50
- readonly archivedAt: number;
50
+ readonly archivedAt: number | null;
51
51
  readonly sourceRunId?: string;
52
52
  readonly runId?: string;
53
53
  readonly absorbedInto?: string;
@@ -137,8 +137,9 @@ export const inject = [];
137
137
  export function apply(ctx, rawConfig = {}) {
138
138
  registerOwnerSettings(ctx, 'evolution-merge', Config);
139
139
  configureMergePresentation(ctx);
140
- let params = resolveMergeParams(rawConfig);
140
+ const startupParams = resolveMergeParams(rawConfig);
141
141
  let mergeRun;
142
+ let decide;
142
143
  let source;
143
144
  let doctorFact;
144
145
  const mergeService = {
@@ -157,7 +158,14 @@ export function apply(ctx, rawConfig = {}) {
157
158
  ;
158
159
  mergeContext.provide('evolutionMerge', mergeService);
159
160
  }
160
- registerHostRoutes(ctx, audit => makeMergeRoutes({ getSource: () => source }, audit));
161
+ registerHostRoutes(ctx, audit => makeMergeRoutes({
162
+ getSource: () => source,
163
+ decide: async (request, _audit) => {
164
+ if (decide === undefined)
165
+ return { ok: false, code: 'E-305', message: '合并服务尚未完成挂载' };
166
+ return await decide(request);
167
+ },
168
+ }, audit));
161
169
  ctx.inject(['commands', 'evolutionIo'], (scope) => {
162
170
  const registry = scope.get('evolutionIo');
163
171
  if (registry === undefined)
@@ -188,9 +196,11 @@ export function apply(ctx, rawConfig = {}) {
188
196
  }, 'evolution-merge: restore supersede');
189
197
  const policySnapshot = ctx.get('evolutionPolicy');
190
198
  const limitsSnapshot = resolveLimitsSnapshot(policySnapshot?.get?.());
191
- const hostApproval = ctx.get('evolutionApproval');
192
- const platformWrite = typeof hostApproval?.platformWrite === 'boolean' ? hostApproval.platformWrite : (params.platformWrite ?? false);
193
- params = { ...params, limitsSnapshot, platformWrite };
199
+ const getPlatformWrite = () => {
200
+ const hostApproval = ctx.get('evolutionApproval');
201
+ return typeof hostApproval?.platformWrite === 'boolean' ? hostApproval.platformWrite : (resolveMergeParams(rawConfig).platformWrite ?? false);
202
+ };
203
+ const getParams = () => ({ ...resolveMergeParams(rawConfig), limitsSnapshot, platformWrite: getPlatformWrite() });
194
204
  const skills = newSkillLibrary({
195
205
  io, ctx, authority: authorityFrom({ origin: 'subagent' }),
196
206
  limits: limitsSnapshot.skillLimits,
@@ -225,7 +235,7 @@ export function apply(ctx, rawConfig = {}) {
225
235
  const pending = v4.state.rows.some(row => row.state === 'pending');
226
236
  const library = pending ? await cards().catch(() => []) : [];
227
237
  const live = library.length === 0 ? undefined : liveRowFactsOf(library);
228
- const reconciled = await reconcileMergeState({ io, home, now: () => Date.now(), ttlDays: params.mergeSkippedTtlDays, ...(live === undefined ? {} : { live }) });
238
+ const reconciled = await reconcileMergeState({ io, home, now: () => Date.now(), ttlDays: getParams().mergeSkippedTtlDays, ...(live === undefined ? {} : { live }) });
229
239
  if (reconciled.actions.length > 0)
230
240
  ctx.logger?.info?.('merge reconcile: ' + String(reconciled.actions.length) + ' action(s)');
231
241
  await transactIo(io, reportsSweepLockTarget(), async () => {
@@ -242,7 +252,7 @@ export function apply(ctx, rawConfig = {}) {
242
252
  return state.kind === 'unavailable' ? { code: 'E-323', detail: state.reason } : undefined;
243
253
  });
244
254
  const stage = new StageToolRegistry();
245
- const engineInput = resolveEngineInput({ modelsRoot: params.mergeModelsRoot });
255
+ const engineInput = resolveEngineInput({ modelsRoot: startupParams.mergeModelsRoot });
246
256
  const engineConfig = engineConfigFromRoot(engineInput.modelsRoot);
247
257
  const llm = ctx.get('llm');
248
258
  const defaultModel = ctx.get('agentDefaultModel');
@@ -283,7 +293,8 @@ export function apply(ctx, rawConfig = {}) {
283
293
  });
284
294
  source = {
285
295
  async snapshot() {
286
- return await readMergeSnapshot({ io, home, runs: runs.runs('merge'), approval: params.mergeApproval, platformWrite, hostOnly: hostCapabilities.hostOnly, live: async () => liveRowFactsOf(await readSkillCards(skills)), history: async () => {
296
+ const current = getParams();
297
+ return await readMergeSnapshot({ io, home, runs: runs.runs('merge'), approval: current.mergeApproval, platformWrite: getPlatformWrite(), hostOnly: hostCapabilities.hostOnly, live: async () => liveRowFactsOf(await readSkillCards(skills)), history: async () => {
287
298
  const items = new Map();
288
299
  const receipts = [...await readCommitReceipts(io, home)].sort((a, b) => a.at - b.at);
289
300
  const generationOf = new Map(receipts.map((receipt, index) => [receipt.runId, index + 1]));
@@ -292,13 +303,15 @@ export function apply(ctx, rawConfig = {}) {
292
303
  items.set(name, { name, kind: 'merged', reason: '已合并进「' + receipt.target + '」,最后一版保留可查。', reasonCode: 'merged', lastVersion: 1, archivedAt: receipt.at, sourceRunId: receipt.runId, runId: receipt.runId, absorbedInto: receipt.target, generation: generationOf.get(receipt.runId) ?? 1 });
293
304
  const archived = await skills.archivedNames?.() ?? [];
294
305
  const reasonOf = skills.readArchiveReason;
306
+ const archivedAtOf = skills.readArchivedAt;
295
307
  for (const name of archived) {
296
308
  if (items.has(name))
297
309
  continue;
298
310
  const rawReason = reasonOf === undefined ? null : await reasonOf.call(skills, name).catch(() => null);
299
311
  const lower = (rawReason ?? '').toLowerCase();
300
312
  const kind = lower.includes('split') ? 'split' : lower.includes('delet') || lower.includes('remove') ? 'deleted' : 'other';
301
- items.set(name, { name, kind, reason: rawReason ?? '技能已归档,最后一版保留可查。', reasonCode: kind === 'other' ? 'archived' : kind, lastVersion: 1, archivedAt: Date.now() });
313
+ const archivedAt = archivedAtOf === undefined ? null : await archivedAtOf.call(skills, name).catch(() => null);
314
+ items.set(name, { name, kind, reason: rawReason ?? '技能已归档,最后一版保留可查。', reasonCode: kind === 'other' ? 'archived' : kind, lastVersion: 1, archivedAt });
302
315
  }
303
316
  const out = [];
304
317
  for (const item of items.values()) {
@@ -310,7 +323,7 @@ export function apply(ctx, rawConfig = {}) {
310
323
  } });
311
324
  },
312
325
  async bodyRef(ref) { return await resolveBody(io, home, ref, async () => ref.side === 'archive' ? await skills.readArchived(ref.name) : await skills.read(ref.name)); },
313
- approval() { return params.mergeApproval; },
326
+ approval() { return getParams().mergeApproval; },
314
327
  readiness() { return ready; },
315
328
  commands() {
316
329
  return ['merge scan', 'merge doctor', 'merge status', 'merge pending', 'merge skipped', 'merge report', 'merge runs', 'merge cancel', 'merge reconcile'];
@@ -327,9 +340,9 @@ export function apply(ctx, rawConfig = {}) {
327
340
  if (commands === undefined)
328
341
  return;
329
342
  const runtime = createMergeRuntime({
330
- io, home, registry: runs, skills, now: () => Date.now(), params, platformWrite,
343
+ io, home, registry: runs, skills, now: () => Date.now(), getParams, getPlatformWrite,
331
344
  ...(defaultModel === undefined ? {} : { model: defaultModel.currentSelection().model }),
332
- channel, parent: undefined, allow: ['merge_stage'], timeoutMinutes: params.mergeRunTimeoutMinutes,
345
+ channel, parent: undefined, allow: ['merge_stage'], timeoutMinutes: startupParams.mergeRunTimeoutMinutes,
333
346
  hostCapabilities,
334
347
  isBlocked: async (name) => {
335
348
  const curator = ctx.get('evolutionCurator');
@@ -358,6 +371,15 @@ export function apply(ctx, rawConfig = {}) {
358
371
  return { ok: false, text: formatMergeError(commandError('merge', state.code, state.message, '先修复合并状态(/evolution merge doctor),再重试')) };
359
372
  return runMergeCommand(runtime, rawInput, invocation);
360
373
  };
374
+ const agents = ctx.get('agents');
375
+ decide = async ({ rowId, verb, sessionId }) => {
376
+ const agent = agents?.get(sessionId);
377
+ if (agent === undefined || agent === null) {
378
+ return { ok: false, code: 'E-305', message: '当前会话没有可用的 Agent;请在会话打开时操作,或复制命令到 CLI 执行' };
379
+ }
380
+ const result = await mergeService.run('/evolution merge ' + verb + ' ' + rowId, { agent });
381
+ return { ok: result.ok, message: result.text };
382
+ };
361
383
  const command = createMergeCommand(runtime);
362
384
  if (command !== null)
363
385
  scope.effect(() => commands.register(command), 'evolution-merge: command');
@@ -12,13 +12,25 @@ export declare const MERGE_ROUTES: {
12
12
  rows: `/api/dsh-evolution/${string}`;
13
13
  body: `/api/dsh-evolution/${string}`;
14
14
  history: `/api/dsh-evolution/${string}`;
15
+ decision: `/api/dsh-evolution/${string}`;
15
16
  };
17
+ export interface MergeDecisionRequest {
18
+ readonly rowId: string;
19
+ readonly verb: 'approve' | 'reject';
20
+ readonly sessionId: string;
21
+ }
22
+ export interface MergeDecisionResult {
23
+ readonly ok: boolean;
24
+ readonly message: string;
25
+ readonly code?: string;
26
+ readonly runId?: string;
27
+ }
16
28
  export interface MergeHistoryItem {
17
29
  readonly name: string;
18
30
  readonly kind: 'merged' | 'deleted' | 'split' | 'other';
19
31
  readonly reason: string;
20
32
  readonly lastVersion: number;
21
- readonly archivedAt: string;
33
+ readonly archivedAt: string | null;
22
34
  readonly sourceRunId?: string;
23
35
  }
24
36
  export interface MergeRouteSource {
@@ -35,5 +47,6 @@ export interface MergeRouteSource {
35
47
  }
36
48
  export declare function makeMergeRoutes(deps: {
37
49
  getSource: () => MergeRouteSource | undefined;
50
+ decide?: (request: MergeDecisionRequest, audit: DecisionAudit) => Promise<MergeDecisionResult>;
38
51
  }, _audit: DecisionAudit): ConnectionFetchRouteLike[];
39
52
  //# sourceMappingURL=merge-routes.d.ts.map
@@ -9,6 +9,7 @@ export const MERGE_ROUTES = {
9
9
  rows: MERGE_PATHS.rows,
10
10
  body: (MERGE_PATHS.body ?? MERGE_BASE + 'body'),
11
11
  history: (MERGE_PATHS.history ?? MERGE_BASE + 'history'),
12
+ decision: (MERGE_BASE + 'decision'),
12
13
  };
13
14
  export function makeMergeRoutes(deps, _audit) {
14
15
  const absent = () => json(404, { wireVersion: 1, code: 'E-325', message: 'merge service is not mounted' });
@@ -110,8 +111,6 @@ export function makeMergeRoutes(deps, _audit) {
110
111
  const id = query.get('id');
111
112
  const name = query.get('name');
112
113
  const side = query.get('side');
113
- if (id !== null && name !== null)
114
- return bad('id and name are mutually exclusive');
115
114
  if ((id === null || id === '') && (name === null || name === ''))
116
115
  return json(404, { wireVersion: 1, code: 'merge-body-not-found', message: 'merge body requires id or name' });
117
116
  if (source.snapshot !== undefined) {
@@ -120,16 +119,18 @@ export function makeMergeRoutes(deps, _audit) {
120
119
  return json(503, { wireVersion: 1, code: 'merge-state-blocked', readiness: snapshot.blocked.readiness });
121
120
  // Body/history are read-only projections of already-written files; a host without
122
121
  // the live agent data plane can still read past reports and archived bodies.
123
- const refs = id !== null && id !== '' ? snapshot.bodyRefs.filter(ref => ref.rowId === id) : snapshot.bodyRefs.filter(ref => ref.name === name);
122
+ const sideFilter = side === 'a' ? 'live' : side === 'b' ? 'archive' : side === 'live' || side === 'archive' ? side : undefined;
123
+ const refs = snapshot.bodyRefs.filter(ref => (id === null || id === '' || ref.rowId === id)
124
+ && (name === null || name === '' || ref.name === name)
125
+ && (sideFilter === undefined || ref.side === sideFilter));
124
126
  if (refs.length === 0)
125
127
  return json(404, { wireVersion: 1, code: 'merge-body-not-found', message: 'merge body not found' });
126
- if (name !== null && refs.length > 1)
127
- return bad('name matches multiple merge rows');
128
+ if (refs.length > 1)
129
+ return bad('merge body is ambiguous; pass id, name and side');
128
130
  const row = snapshot.rows.find(item => item.row.id === refs[0].rowId);
129
131
  if (row?.stage?.state === 'lost' || row?.stage?.state === 'expired')
130
132
  return json(404, { wireVersion: 1, code: 'merge-body-not-found', message: 'merge stage is lost' });
131
- const sideFilter = side === 'a' ? 'live' : side === 'b' ? 'archive' : side === 'live' || side === 'archive' ? side : undefined;
132
- const ref = (sideFilter === undefined ? refs[0] : refs.find(item => item.side === sideFilter)) ?? refs[0];
133
+ const ref = refs[0];
133
134
  const content = source.bodyRef === undefined ? null : await source.bodyRef(ref);
134
135
  return json(200, buildBodyView(snapshot, ref, content ?? ''));
135
136
  }
@@ -167,6 +168,35 @@ export function makeMergeRoutes(deps, _audit) {
167
168
  return json(200, toHistoryView(await source.history?.() ?? [], Date.now()));
168
169
  },
169
170
  },
171
+ {
172
+ path: MERGE_ROUTES.decision,
173
+ methods: ['POST'],
174
+ requestBody: 'buffered',
175
+ fetch: async (request) => {
176
+ if (request.method !== 'POST')
177
+ return methodNotAllowed(['POST']);
178
+ if (deps.decide === undefined)
179
+ return absent();
180
+ let body;
181
+ try {
182
+ body = await request.json();
183
+ }
184
+ catch {
185
+ return bad('decision body must be JSON');
186
+ }
187
+ if (typeof body !== 'object' || body === null)
188
+ return bad('decision body must be an object');
189
+ const input = body;
190
+ if (typeof input.rowId !== 'string' || input.rowId === '')
191
+ return bad('rowId is required');
192
+ if (input.verb !== 'approve' && input.verb !== 'reject')
193
+ return bad('verb must be approve or reject');
194
+ if (typeof input.sessionId !== 'string' || input.sessionId === '')
195
+ return bad('sessionId is required');
196
+ const result = await deps.decide({ rowId: input.rowId, verb: input.verb, sessionId: input.sessionId }, _audit);
197
+ return json(result.ok ? 200 : result.code === 'E-305' ? 409 : 400, { wireVersion: 1, ...result });
198
+ },
199
+ },
170
200
  ];
171
201
  }
172
202
  //# sourceMappingURL=merge-routes.js.map
@@ -18,8 +18,13 @@ export interface MergeRuntimeDeps {
18
18
  readonly registry: MergeRegistryPort;
19
19
  readonly skills: SkillLibrary;
20
20
  readonly now: () => number;
21
- readonly params: MergeParams;
22
- readonly platformWrite: boolean;
21
+ /** Live params: re-resolve at each operation, never cache an apply-time value. */
22
+ readonly getParams?: () => MergeParams;
23
+ /** Legacy fixture shape; used only when getParams is absent. */
24
+ readonly params?: MergeParams;
25
+ readonly getPlatformWrite?: () => boolean;
26
+ /** Legacy fixture shape; used only when getPlatformWrite is absent. */
27
+ readonly platformWrite?: boolean;
23
28
  readonly channel: ConversationChannelLike;
24
29
  readonly hostCapabilities?: HostCapabilities;
25
30
  readonly parent: unknown;
@@ -78,13 +78,21 @@ export function unitFor(deps) {
78
78
  export function createMergeRuntime(deps) {
79
79
  const readOnly = createReadOnlyMergeService(deps.io, deps.home);
80
80
  const timeout = deps.timeoutMinutes > 0 ? deps.timeoutMinutes : undefined;
81
- const common = {
81
+ const paramsOf = () => {
82
+ if (deps.getParams !== undefined)
83
+ return deps.getParams();
84
+ if (deps.params !== undefined)
85
+ return deps.params;
86
+ throw new Error('merge runtime requires getParams or params');
87
+ };
88
+ const platformWriteOf = () => deps.getPlatformWrite?.() ?? deps.platformWrite ?? false;
89
+ const common = () => ({
82
90
  io: deps.io, home: deps.home, registry: deps.registry,
83
- platformWrite: deps.platformWrite, now: deps.now,
91
+ platformWrite: platformWriteOf(), now: deps.now,
84
92
  ...(deps.model === undefined ? {} : { model: deps.model }),
85
93
  ...(deps.isOwnerActive === undefined ? {} : { isOwnerActive: deps.isOwnerActive }),
86
94
  ...(timeout === undefined ? {} : { timeoutMinutes: timeout }),
87
- };
95
+ });
88
96
  const stage = deps.stage;
89
97
  const stageHooks = stage === undefined ? {} : {
90
98
  onChild: (context) => { stage.bind(context); },
@@ -95,6 +103,7 @@ export function createMergeRuntime(deps) {
95
103
  const parent = context?.parent ?? deps.parent;
96
104
  const agentless = context?.agentless === true;
97
105
  const detached = deps.detached !== false;
106
+ const params = paramsOf();
98
107
  if (command.verb === 'pair') {
99
108
  const signal = new AbortController().signal;
100
109
  if (command.a === command.b)
@@ -155,9 +164,9 @@ export function createMergeRuntime(deps) {
155
164
  return fail('pair', 'pair-fuse', '该 pair 已在待合并队列,可复用决策重跑', '运行 /evolution merge resume --retry ' + (existing.runId ?? existing.id), existing.runId);
156
165
  }
157
166
  const forced = command.force === true ? forcedDecisionFor(command.a, command.b) : undefined;
158
- const approval = deps.params.mergeApproval;
167
+ const approval = params.mergeApproval;
159
168
  const result = await runScanCommand({
160
- ...common,
169
+ ...common(),
161
170
  approval,
162
171
  cards,
163
172
  ...(detached ? { detached: true } : {}),
@@ -167,10 +176,10 @@ export function createMergeRuntime(deps) {
167
176
  screening: () => Promise.resolve(pair),
168
177
  channel: deps.channel, parent, allow: deps.allow,
169
178
  ...(deps.cwd === undefined ? {} : { cwd: deps.cwd }),
170
- timeoutMs: deps.params.mergePairTimeoutMinutes * 60_000,
179
+ timeoutMs: params.mergePairTimeoutMinutes * 60_000,
171
180
  approval,
172
181
  commitPort: skillLibraryPort(deps.skills), now: deps.now, ...(unitFor(deps) === undefined ? {} : { unit: unitFor(deps) }),
173
- ...(deps.params.limitsSnapshot === undefined ? {} : { limits: deps.params.limitsSnapshot.skillLimits }),
182
+ ...(params.limitsSnapshot === undefined ? {} : { limits: params.limitsSnapshot.skillLimits }),
174
183
  ...(deps.isBlocked === undefined ? {} : { isBlocked: deps.isBlocked }),
175
184
  ...stageHooks,
176
185
  });
@@ -201,7 +210,7 @@ export function createMergeRuntime(deps) {
201
210
  .filter(card => card.managed === true && card.protectedBy == null && !blocked.has(card.name))
202
211
  .map(card => card.name));
203
212
  const loadedState = await loadMergeState(deps.io, deps.home);
204
- const skipped = skippedPairKeys(loadedState.state.rows, deps.params.mergeSkippedTtlDays, deps.now());
213
+ const skipped = skippedPairKeys(loadedState.state.rows, params.mergeSkippedTtlDays, deps.now());
205
214
  const held = new Set(loadedState.state.rows.filter(row => row.queue === 'pending' || row.queue === 'fuse'
206
215
  || (row.queue === 'verify' && (row.runId !== undefined && deps.now() - Date.parse(row.updatedAt) < 10 * 60_000 || !canRetry(row))))
207
216
  .map(row => row.pairKey));
@@ -209,8 +218,8 @@ export function createMergeRuntime(deps) {
209
218
  .filter(row => row.state === 'pending' || row.state === 'approved' || row.state === 'fuse' || row.state === 'skipped')
210
219
  .map(row => 'conflict:' + row.state + ':' + row.pairKey + ':' + row.id);
211
220
  const result = await runScanCommand({
212
- ...common,
213
- approval: deps.params.mergeApproval,
221
+ ...common(),
222
+ approval: params.mergeApproval,
214
223
  notes,
215
224
  cards,
216
225
  ...(detached ? { detached: true } : {}),
@@ -238,10 +247,10 @@ export function createMergeRuntime(deps) {
238
247
  },
239
248
  channel: deps.channel, parent, allow: deps.allow,
240
249
  ...(deps.cwd === undefined ? {} : { cwd: deps.cwd }),
241
- timeoutMs: deps.params.mergePairTimeoutMinutes * 60_000,
242
- approval: deps.params.mergeApproval,
250
+ timeoutMs: params.mergePairTimeoutMinutes * 60_000,
251
+ approval: params.mergeApproval,
243
252
  commitPort: skillLibraryPort(deps.skills), now: deps.now, ...(unitFor(deps) === undefined ? {} : { unit: unitFor(deps) }),
244
- ...(deps.params.limitsSnapshot === undefined ? {} : { limits: deps.params.limitsSnapshot.skillLimits }),
253
+ ...(params.limitsSnapshot === undefined ? {} : { limits: params.limitsSnapshot.skillLimits }),
245
254
  ...(deps.isBlocked === undefined ? {} : { isBlocked: deps.isBlocked }),
246
255
  ...stageHooks,
247
256
  }),
@@ -267,8 +276,8 @@ export function createMergeRuntime(deps) {
267
276
  const cards = await deps.cards(signal);
268
277
  const pairCandidate = { a: row.a, b: row.b };
269
278
  const result = await runScanCommand({
270
- ...common,
271
- approval: deps.params.mergeApproval,
279
+ ...common(),
280
+ approval: params.mergeApproval,
272
281
  notes: ['candidates --retry ' + row.id],
273
282
  cards,
274
283
  makePorts: runId => createChannelScanPorts({
@@ -280,10 +289,10 @@ export function createMergeRuntime(deps) {
280
289
  },
281
290
  channel: deps.channel, parent, allow: deps.allow,
282
291
  ...(deps.cwd === undefined ? {} : { cwd: deps.cwd }),
283
- timeoutMs: deps.params.mergePairTimeoutMinutes * 60_000,
284
- approval: deps.params.mergeApproval,
292
+ timeoutMs: params.mergePairTimeoutMinutes * 60_000,
293
+ approval: params.mergeApproval,
285
294
  commitPort: skillLibraryPort(deps.skills), now: deps.now, ...(unitFor(deps) === undefined ? {} : { unit: unitFor(deps) }),
286
- ...(deps.params.limitsSnapshot === undefined ? {} : { limits: deps.params.limitsSnapshot.skillLimits }),
295
+ ...(params.limitsSnapshot === undefined ? {} : { limits: params.limitsSnapshot.skillLimits }),
287
296
  ...(deps.isBlocked === undefined ? {} : { isBlocked: deps.isBlocked }),
288
297
  ...stageHooks,
289
298
  }),
@@ -318,7 +327,7 @@ export function createMergeRuntime(deps) {
318
327
  const managed = cards === undefined || cards.length === 0 ? undefined : new Set(cards.map(card => card.name));
319
328
  const live = cards === undefined || cards.length === 0 ? undefined : liveRowFactsOf(cards);
320
329
  try {
321
- const report = await reconcileMergeState({ io: deps.io, home: deps.home, now: deps.now, ttlDays: deps.params.mergeSkippedTtlDays, ...(managed === undefined ? {} : { managed }), ...(live === undefined ? {} : { live }) });
330
+ const report = await reconcileMergeState({ io: deps.io, home: deps.home, now: deps.now, ttlDays: params.mergeSkippedTtlDays, ...(managed === undefined ? {} : { managed }), ...(live === undefined ? {} : { live }) });
322
331
  if (report.state === 'blocked')
323
332
  return fail('reconcile', report.code ?? 'merge-state-blocked', report.message ?? '合并状态不可读', '先修复合并状态,再运行 /evolution merge doctor');
324
333
  return { ok: true, text: formatReconcileReport(report) };
@@ -328,14 +337,14 @@ export function createMergeRuntime(deps) {
328
337
  }
329
338
  }
330
339
  if (command.verb === 'doctor') {
331
- const rows = await mergeDoctor(deps.io, deps.home, deps.params.mergeModelsRoot);
340
+ const rows = await mergeDoctor(deps.io, deps.home, params.mergeModelsRoot);
332
341
  return { ok: true, text: rows.map(row => row.key + ' [' + row.status + '] ' + row.label + ':' + row.detail).join('\n') };
333
342
  }
334
343
  if (command.verb === 'report' || command.verb === 'runs' || command.verb === 'cancel') {
335
344
  const registry = deps.registry;
336
345
  if (command.verb === 'runs') {
337
346
  const mergeRuns = registry.runs('merge');
338
- const snapshot = await readMergeSnapshot({ io: deps.io, home: deps.home, runs: mergeRuns, approval: deps.params.mergeApproval, now: deps.now });
347
+ const snapshot = await readMergeSnapshot({ io: deps.io, home: deps.home, runs: mergeRuns, approval: params.mergeApproval, now: deps.now });
339
348
  return { ok: true, text: formatRunsView(buildRunsView(snapshot)) };
340
349
  }
341
350
  if (command.verb === 'cancel') {
@@ -443,27 +452,27 @@ export function createMergeRuntime(deps) {
443
452
  round2: () => failed('author', 'resume-needs-child', 'fuse resume needs a new child'),
444
453
  };
445
454
  const result = await runResumeCommand({
446
- ...common, rowId: row.id, ports: basePorts, approval: deps.params.mergeApproval,
455
+ ...common(), rowId: row.id, ports: basePorts, approval: params.mergeApproval,
447
456
  ...(agentless ? { agentless: true } : {}),
448
457
  ...(detached ? { detached: true } : {}),
449
- pairTimeoutMinutes: deps.params.mergePairTimeoutMinutes,
458
+ pairTimeoutMinutes: params.mergePairTimeoutMinutes,
450
459
  ...(deps.isOwnerActive === undefined ? {} : { isOwnerActive: deps.isOwnerActive }),
451
460
  makePorts: runId => createChannelScanPorts({
452
461
  io: deps.io, home: deps.home, runId, cards, screening: pair,
453
462
  channel: deps.channel, parent, allow: deps.allow,
454
463
  ...(deps.cwd === undefined ? {} : { cwd: deps.cwd }),
455
- timeoutMs: deps.params.mergePairTimeoutMinutes * 60_000,
456
- approval: deps.params.mergeApproval, commitPort: skillLibraryPort(deps.skills), now: deps.now, ...(unitFor(deps) === undefined ? {} : { unit: unitFor(deps) }),
457
- ...(deps.params.limitsSnapshot === undefined ? {} : { limits: deps.params.limitsSnapshot.skillLimits }),
464
+ timeoutMs: params.mergePairTimeoutMinutes * 60_000,
465
+ approval: params.mergeApproval, commitPort: skillLibraryPort(deps.skills), now: deps.now, ...(unitFor(deps) === undefined ? {} : { unit: unitFor(deps) }),
466
+ ...(params.limitsSnapshot === undefined ? {} : { limits: params.limitsSnapshot.skillLimits }),
458
467
  ...(deps.isBlocked === undefined ? {} : { isBlocked: deps.isBlocked }),
459
468
  }),
460
469
  round2WithRun: createNewChildRound2({
461
470
  io: deps.io, home: deps.home, cards, channel: deps.channel, parent,
462
471
  parentRunId: (runId) => runId,
463
472
  allow: deps.allow, ...(deps.cwd === undefined ? {} : { cwd: deps.cwd }),
464
- timeoutMs: deps.params.mergePairTimeoutMinutes * 60_000,
473
+ timeoutMs: params.mergePairTimeoutMinutes * 60_000,
465
474
  commitPort: skillLibraryPort(deps.skills), now: deps.now, ...(unitFor(deps) === undefined ? {} : { unit: unitFor(deps) }),
466
- ...(deps.params.limitsSnapshot === undefined ? {} : { limits: deps.params.limitsSnapshot.skillLimits }),
475
+ ...(params.limitsSnapshot === undefined ? {} : { limits: params.limitsSnapshot.skillLimits }),
467
476
  ...(deps.isBlocked === undefined ? {} : { isBlocked: deps.isBlocked }),
468
477
  ...stageHooks,
469
478
  }),
@@ -563,20 +572,20 @@ export function createMergeRuntime(deps) {
563
572
  }
564
573
  }
565
574
  const result = await runApproveCommand({
566
- ...common,
575
+ ...common(),
567
576
  rowId: command.id,
568
577
  ...(agentless ? { agentless: true } : {}),
569
578
  ...(detached ? { detached: true } : {}),
570
- pairTimeoutMinutes: deps.params.mergePairTimeoutMinutes,
579
+ pairTimeoutMinutes: params.mergePairTimeoutMinutes,
571
580
  ports: {
572
581
  stage: createStageForCards({ io: deps.io, home: deps.home, runId: command.id, cards, now: deps.now }),
573
582
  round2: createNewChildRound2({
574
583
  io: deps.io, home: deps.home, cards, channel: deps.channel, parent,
575
584
  parentRunId: (runId) => runId,
576
585
  allow: deps.allow, ...(deps.cwd === undefined ? {} : { cwd: deps.cwd }),
577
- timeoutMs: deps.params.mergePairTimeoutMinutes * 60_000,
586
+ timeoutMs: params.mergePairTimeoutMinutes * 60_000,
578
587
  commitPort: skillLibraryPort(deps.skills), now: deps.now, ...(unitFor(deps) === undefined ? {} : { unit: unitFor(deps) }),
579
- ...(deps.params.limitsSnapshot === undefined ? {} : { limits: deps.params.limitsSnapshot.skillLimits }),
588
+ ...(params.limitsSnapshot === undefined ? {} : { limits: params.limitsSnapshot.skillLimits }),
580
589
  ...(deps.isBlocked === undefined ? {} : { isBlocked: deps.isBlocked }),
581
590
  ...stageHooks,
582
591
  }),
@@ -597,7 +606,7 @@ export function createMergeRuntime(deps) {
597
606
  const found = runs?.find(item => item.id === command.id) ?? registry.find(command.id);
598
607
  if (found !== undefined && found.kind === 'merge') {
599
608
  // CLI consumer of the RunDetail envelope: current pair, per-pair outcome, stop reason.
600
- const snapshot = await readMergeSnapshot({ io: deps.io, home: deps.home, runs: runs !== undefined && runs.some(item => item.id === found.id) ? runs : [...(runs ?? []), found], approval: deps.params.mergeApproval, now: deps.now });
609
+ const snapshot = await readMergeSnapshot({ io: deps.io, home: deps.home, runs: runs !== undefined && runs.some(item => item.id === found.id) ? runs : [...(runs ?? []), found], approval: params.mergeApproval, now: deps.now });
601
610
  if (snapshot.blocked !== undefined)
602
611
  return fail('status', snapshot.blocked.code, snapshot.blocked.message, '先修复合并状态(/evolution merge doctor),再查看运行');
603
612
  const detail = buildRunDetail(snapshot, found.id);
@@ -615,7 +624,7 @@ export function createMergeRuntime(deps) {
615
624
  const list = cards ?? await deps.cards(new AbortController().signal).catch(() => []);
616
625
  return list.length === 0 ? undefined : liveRowFactsOf(list);
617
626
  };
618
- const text = await handleReadOnlyMergeCommand(command, deps.io, deps.home, cards, runs, deps.params.mergeApproval, liveForRead);
627
+ const text = await handleReadOnlyMergeCommand(command, deps.io, deps.home, cards, runs, params.mergeApproval, liveForRead);
619
628
  if (text !== null)
620
629
  return { ok: true, text };
621
630
  return readOnly.execute(command);
@@ -88,9 +88,11 @@ function runView(run, readAt, includeResultRef = false) {
88
88
  const stopReason = run.meta?.stopReason ?? (record.state === 'cancelled' ? 'cancelled' : record.state === 'failed' ? 'failed' : record.state === 'succeeded' ? 'completed' : undefined);
89
89
  // The maker records progress while running; a terminal run that actually produced pairs
90
90
  // must not keep the last in-flight 0/N. The UI shows what happened, not the last tick.
91
- const progress = record.progress === undefined ? null : {
92
- done: state === 'succeeded' || state === 'undone' ? Math.max(record.progress.done, run.pairs.length) : record.progress.done,
93
- total: record.progress.total,
91
+ const reportPairs = run.report?.processedPairs?.length ?? run.report?.pairs.length;
92
+ const terminal = state === 'succeeded' || state === 'undone';
93
+ const progress = record.progress === undefined && reportPairs === undefined ? null : {
94
+ done: terminal ? Math.max(record.progress?.done ?? 0, reportPairs ?? 0, run.pairs.length) : record.progress?.done ?? reportPairs ?? 0,
95
+ total: record.progress?.total ?? reportPairs ?? 0,
94
96
  };
95
97
  return { wireVersion: 1, readAt, id: record.id, state, command: record.command ?? 'scan', ...(record.stage === undefined ? {} : { stage: record.stage }), progress, startedAt: record.startedAt, ...(record.endedAt === undefined ? {} : { endedAt: record.endedAt }), ...(stopReason === undefined ? {} : { stopReason }), ...(record.failure === undefined ? {} : { failure: { stage: 'settle', code: 'run-failed', retryable: false, retryFrom: 'none', detail: record.failure } }), ...(record.parentRunId === undefined ? {} : { parentRunId: record.parentRunId }), ...(run.pairs[0]?.pairKey === undefined ? {} : { currentPairKey: run.pairs[0].pairKey }), ...(record.resultRef === undefined ? {} : { resultRef: record.resultRef }), actions: [{ id: 'status', command: base + 'status ' + record.id, confirm: false, enabled: true }, { id: 'report', command: base + 'report ' + record.id, confirm: false, enabled: record.resultRef !== undefined }, { id: 'cancel', command: base + 'cancel ' + record.id, confirm: false, enabled: record.state === 'running' }] };
96
98
  }
@@ -30,7 +30,7 @@ export declare function toHistoryView(items: readonly {
30
30
  kind: 'merged' | 'deleted' | 'split' | 'other';
31
31
  reason: string;
32
32
  lastVersion: number;
33
- archivedAt: string;
33
+ archivedAt: string | number | null;
34
34
  sourceRunId?: string;
35
35
  }[], readAt: number): HistoryView;
36
36
  //# sourceMappingURL=wire-projection.d.ts.map
@@ -69,6 +69,9 @@ export function toBodyView(name, side, text, readAt) {
69
69
  return { wireVersion: 1, readAt, name, side, text, markdown: true, truncated: false, totalBytes: Buffer.byteLength(text, 'utf8') };
70
70
  }
71
71
  export function toHistoryView(items, readAt) {
72
- return { wireVersion: 1, readAt, items: items.map(item => ({ name: item.name, kind: item.kind, reason: item.reason, lastVersion: item.lastVersion, archivedAt: Date.parse(item.archivedAt), ...(item.sourceRunId === undefined ? {} : { sourceRunId: item.sourceRunId }) })) };
72
+ return { wireVersion: 1, readAt, items: items.map(item => {
73
+ const parsed = item.archivedAt === null ? null : typeof item.archivedAt === 'number' ? item.archivedAt : Date.parse(item.archivedAt);
74
+ return { name: item.name, kind: item.kind, reason: item.reason, lastVersion: item.lastVersion, archivedAt: parsed === null || Number.isFinite(parsed) ? parsed : null, ...(item.sourceRunId === undefined ? {} : { sourceRunId: item.sourceRunId }) };
75
+ }) };
73
76
  }
74
77
  //# sourceMappingURL=wire-projection.js.map
@@ -184,7 +184,7 @@ export interface HistoryItem {
184
184
  readonly reasonCode?: string;
185
185
  readonly generation?: number;
186
186
  readonly lastVersion: number;
187
- readonly archivedAt: number;
187
+ readonly archivedAt: number | null;
188
188
  readonly sourceRunId?: string;
189
189
  readonly runId?: string;
190
190
  readonly absorbedInto?: string;
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.33.0",
4
+ "version": "0.35.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -32,9 +32,9 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@deepseek-ai/schemastery": "^3.18.5",
35
- "@lmzhen/dsh-evolution-core": "^0.33.0",
36
- "@lmzhen/dsh-evolution-commands": "^0.33.0",
37
- "@lmzhen/dsh-evolution-merge-engine": "0.33.0"
35
+ "@lmzhen/dsh-evolution-core": "^0.35.0",
36
+ "@lmzhen/dsh-evolution-commands": "^0.35.0",
37
+ "@lmzhen/dsh-evolution-merge-engine": "0.35.0"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "@deepseek-ai/cordis": "^4.0.5"