@lmzhen/dsh-evolution-commands 0.18.0 → 0.19.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/README.md CHANGED
@@ -17,6 +17,7 @@ The full `/evolution` subcommand table is single-sourced in the family README's
17
17
  ## Known limitations
18
18
 
19
19
  - No known durable consumer gaps at this time. Runtime contracts are covered by package and boundary tests.
20
+ - **`/evolution maintain` runs in the BACKGROUND (0.19.0).** The command answers with a run id and the scan keeps working: read it with `/evolution maintain report <id>` (a run that produced nothing answers "no result" instead of an empty plan) or `/evolution maintain status [<id>]` (in flight plus the recent terminal states). The run's lifetime belongs to this plugin through `evolution-core`'s run registry, **not** to the request that started it: the desktop shell ends a request at ~305 s (measured four times) while the same scan completes in 949.7 s on the web plane, and `maintainTimeoutMs` (default 600000) bounds the run itself. **Cancelling the request no longer stops the scan** — use `/evolution maintain cancel <id>`, or unmount the row. The plan is written once at settle to `<evolutionHome>/reports/maintain-<runId>.{json,md}`, under the same per-home reports window the curator's reports use.
20
21
  - **① variant form: the `/evolution …` command face is visible in every session** (accepted, not fixed). Commands are registered in scope layers, and the family registers once on the host plane, so a session on a platform original preset still lists the management-only subcommands even though no automatic family behaviour reaches it. The difference and its reasoning are single-sourced in `INSTALL.md` ("Known difference").
21
22
 
22
23
  **Runtime invariant:** No companion is published. The platform auto-assembles nothing and the family mounts no `<pkg>/invariant` cordis row, so a companion here would never execute (v37 S2.1 / I-3).
package/lib/index.js CHANGED
@@ -2,7 +2,7 @@ import { createRequire } from "node:module";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { effectiveSessionPolicy } from "@lmzhen/dsh-evolution-approval";
4
4
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
5
- import { DEFAULT_SKILL_LIMITS, MAX_TIMER_DELAY_MS, PARAM_EXPOSURE, PARAM_GROUP_LABELS, absentHostServices, appendEvolutionEvent, applyNamespaceMigration, assertSkillsRootAliasRetired, buildLearnPrompt, canonicalWriteId, clampedNumber, classifyNamespaceMigration, composePresetEntry, elapsedSince, errorText, eventsFile, evolutionRoot, isDeprecatedParamId, isMissingPath, mergePresetRow, newSkillLibrary, paramSettingsId, partitionVersions, planNamespaceMigration, policyStageLimits, presetPatchText, presetRowBlock, presetRowBody, presetRowId, resolveBasePresetPatch, resolveParamId, resolveRootConfig, scopedProbeReport } from "@lmzhen/dsh-evolution-core";
5
+ import { DEFAULT_SKILL_LIMITS, MAX_TIMER_DELAY_MS, PARAM_EXPOSURE, PARAM_GROUP_LABELS, absentHostServices, appendEvolutionEvent, applyNamespaceMigration, assertSkillsRootAliasRetired, buildLearnPrompt, canonicalWriteId, clampedNumber, classifyNamespaceMigration, composePresetEntry, elapsedSince, errorText, eventsFile, evolutionHome, evolutionRoot, isDeprecatedParamId, isMissingPath, isPresent, isUnknown, mergePresetRow, newRunRegistry, newSkillLibrary, paramSettingsId, partitionVersions, planNamespaceMigration, policyStageLimits, presetPatchText, presetRowBlock, presetRowBody, presetRowId, probeText, reportsSweepLockTarget, resolveBasePresetPatch, resolveParamId, resolveRootConfig, scopedProbeReport, sweepReports, transactIo } from "@lmzhen/dsh-evolution-core";
6
6
  import { buildEnrichment, buildMaintainFacts, enrichmentSnapshotOptions, runMaintain, snapshotFromLibrary } from "@lmzhen/dsh-evolution-maintenance";
7
7
  import { copyFileSync, existsSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
8
8
  import { dirname, join } from "node:path";
@@ -1332,7 +1332,19 @@ const COMMAND_ENTRIES = [
1332
1332
  },
1333
1333
  {
1334
1334
  usage: "maintain [--timeout=<ms> | --facts]",
1335
- summary: "运行一次维护扫描(--facts 为 0 token 预览)"
1335
+ summary: "起一次维护扫描并立即回 run id(后台跑;--facts 为 0 token 预览)"
1336
+ },
1337
+ {
1338
+ usage: "maintain status [<id>]",
1339
+ summary: "看维护扫描:在飞的那次(含已跑多久)或某个 run 的终态"
1340
+ },
1341
+ {
1342
+ usage: "maintain report <id>",
1343
+ summary: "读某个 run 的结果正文(没有结果就明说,不装成空结果)"
1344
+ },
1345
+ {
1346
+ usage: "maintain cancel <id>",
1347
+ summary: "停掉在飞的那次维护扫描"
1336
1348
  },
1337
1349
  {
1338
1350
  usage: "migrate",
@@ -1386,13 +1398,119 @@ const Config = z.object({
1386
1398
  maintainTimeoutMs: z.number().min(1).default(6e5),
1387
1399
  threatExemptLabels: z.array(z.string()).default([])
1388
1400
  });
1401
+ /** How one run reads in `maintain status` output. */
1402
+ function renderRunRecord(record) {
1403
+ const seconds = Math.max(1, Math.round(((record.endedAt ?? Date.now()) - record.startedAt) / 1e3));
1404
+ const parts = [`${record.id} ${record.state} ${seconds}s`];
1405
+ if (record.failure !== void 0) parts.push(`— ${record.failure}`);
1406
+ if (record.resultRef !== void 0) parts.push(`→ ${record.resultRef}`);
1407
+ return parts.join(" ");
1408
+ }
1409
+ /**
1410
+ * Write one run's result to its ONE home: `<evolutionHome>/reports/maintain-<id>.json`
1411
+ * plus the `.md` digest, then sweep that window.
1412
+ *
1413
+ * A write failure does not fail the run — because the run has no result then, and
1414
+ * `maintain report <id>` says exactly that instead of pretending otherwise.
1415
+ * @param io - the evolution io seam.
1416
+ * @param record - the run's outcome, as persisted.
1417
+ * @param warn - warn channel for retention notes.
1418
+ * @returns the report path, or `undefined` when nothing could be written.
1419
+ */
1420
+ /**
1421
+ * Re-read the run index before answering a command (review P1-1).
1422
+ *
1423
+ * One DSH_HOME is shared by both planes, so a scan the OTHER process started is
1424
+ * only visible after a refresh — without it the in-flight guard, the cooldown and
1425
+ * `status <id>` would all describe this process's memory alone. A read FAILURE is
1426
+ * not "no runs": the caller decides what to say.
1427
+ * @param registry - this row's registry.
1428
+ * @returns whether the history could be read, and the note to surface when there is one.
1429
+ */
1430
+ async function syncRuns(registry) {
1431
+ try {
1432
+ return await registry.load();
1433
+ } catch (error) {
1434
+ return {
1435
+ ok: false,
1436
+ note: `the run index could not be read (${error instanceof Error ? error.message : String(error)})`
1437
+ };
1438
+ }
1439
+ }
1440
+ async function writeMaintainReport(io, record, warn, evolutionHomePath) {
1441
+ const dir = join(evolutionHomePath, "reports");
1442
+ const path = join(dir, `maintain-${record.runId}.json`);
1443
+ let written = true;
1444
+ try {
1445
+ await io.writeText(path, JSON.stringify(record, null, 2));
1446
+ await io.writeText(join(dir, `maintain-${record.runId}.md`), record.text.trim() === "" ? `Maintenance run ${record.runId}: ${record.failure ?? "no plan"}\n` : record.text);
1447
+ } catch (error) {
1448
+ written = false;
1449
+ warn(`evolution-commands: could not write the report for run ${record.runId} (${error instanceof Error ? error.message : String(error)}) — that run will answer "no result"`);
1450
+ }
1451
+ await transactIo(io, reportsSweepLockTarget(), async () => {
1452
+ await sweepReports({
1453
+ io,
1454
+ dir,
1455
+ buckets: [{
1456
+ prefix: "maintain-",
1457
+ keep: 20
1458
+ }],
1459
+ owner: "evolution-commands",
1460
+ warn
1461
+ });
1462
+ return null;
1463
+ });
1464
+ return written ? path : void 0;
1465
+ }
1466
+ /** Three-state read of a run's result: present, missing, or unreadable (never collapsed). */
1467
+ async function readRunReport(io, record) {
1468
+ const probed = await probeText(io, record.resultRef ?? join(evolutionHome(), "reports", `maintain-${record.id}.json`));
1469
+ if (isUnknown(probed)) return {
1470
+ kind: "unknown",
1471
+ reason: probed.reason
1472
+ };
1473
+ if (!isPresent(probed)) return { kind: "missing" };
1474
+ try {
1475
+ const parsed = JSON.parse(probed.value);
1476
+ return {
1477
+ kind: "present",
1478
+ text: `${`Maintenance run ${record.id} — ${record.state}${record.failure === void 0 ? "" : ` (${record.failure})`}`}\n${typeof parsed.text === "string" && parsed.text.trim() !== "" ? parsed.text : renderRunRecord(record)}`
1479
+ };
1480
+ } catch (error) {
1481
+ return {
1482
+ kind: "unknown",
1483
+ reason: `the report is not JSON (${error instanceof Error ? error.message : String(error)})`
1484
+ };
1485
+ }
1486
+ }
1389
1487
  function apply(ctx, rawConfig = {}) {
1390
1488
  const config = rawConfig;
1391
1489
  assertSkillsRootAliasRetired(rawConfig);
1392
1490
  const skillsRootValue = resolveRootConfig(rawConfig).root;
1393
- let lastMaintainAt = 0;
1394
- let lastMaintainRunId = "";
1395
- let maintainInFlightSince = 0;
1491
+ let runs;
1492
+ ctx.inject(["evolutionIo"], (ioCtx) => {
1493
+ const ioRegistry = ioCtx.get("evolutionIo");
1494
+ if (!ioRegistry) return;
1495
+ const registry = newRunRegistry({
1496
+ io: ioRegistry.provider(),
1497
+ home: evolutionHome(),
1498
+ warn: (message) => {
1499
+ ctx.logger.warn(message);
1500
+ }
1501
+ });
1502
+ runs = registry;
1503
+ registry.load().then((loaded) => {
1504
+ if (!loaded.ok) ctx.logger.warn(`evolution-commands: the run index is not usable — ${loaded.note ?? "unknown reason"}`);
1505
+ else if (loaded.note !== void 0) ctx.logger.warn(`evolution-commands: ${loaded.note}`);
1506
+ }).catch((error) => {
1507
+ ctx.logger.warn(`evolution-commands: could not read the run index (${error instanceof Error ? error.message : String(error)})`);
1508
+ });
1509
+ ioCtx.effect(() => () => {
1510
+ registry.cancelAll();
1511
+ runs = void 0;
1512
+ }, "evolution-commands.run-registry");
1513
+ });
1396
1514
  ctx.inject(["commands"], (commandCtx) => {
1397
1515
  const commands = commandCtx.commands;
1398
1516
  const commandBody = { run: async (invocation) => {
@@ -1670,57 +1788,155 @@ function apply(ctx, rawConfig = {}) {
1670
1788
  if (maintainArgs) {
1671
1789
  const runTimeoutMs = maintainArgs[1] ? Number(maintainArgs[1]) : config.maintainTimeoutMs ?? 6e5;
1672
1790
  if (!Number.isSafeInteger(runTimeoutMs) || runTimeoutMs <= 0 || runTimeoutMs > MAX_TIMER_DELAY_MS) return err(maintainArgs[1] ? `Invalid --timeout value: expected a positive integer number of milliseconds up to ${MAX_TIMER_DELAY_MS} (e.g. /evolution maintain --timeout 600000).` : `The maintainTimeoutMs config is invalid: expected a positive integer number of milliseconds up to ${MAX_TIMER_DELAY_MS}. Fix the evolution-commands row config (maintainTimeoutMs), then retry.`);
1673
- if (maintainInFlightSince > 0) return err(`Maintenance scan is already running (since ~${Math.max(1, Math.round((Date.now() - maintainInFlightSince) / 1e3))}s ago) — re-submitting now would cancel it. Wait for it to settle; the result appears when it finishes.`);
1674
- const cooldownMs = clampedNumber(config.maintainCooldownMs, 3e4, { min: 0 });
1675
- const sinceLast = Date.now() - lastMaintainAt;
1676
- if (cooldownMs > 0 && sinceLast < cooldownMs) return err(`Maintenance cooldown active (${Math.ceil((cooldownMs - sinceLast) / 1e3)}s)${lastMaintainRunId === "" ? "" : ` — latest scan ${lastMaintainRunId}`}; re-running now would spend another model call.`);
1791
+ const registry = runs;
1792
+ if (!registry) return err("Evolution IO registry not mounted — maintenance scan unavailable.");
1793
+ const synced = await syncRuns(registry);
1794
+ if (!synced.ok) ctx.logger.warn(`evolution-commands: ${synced.note ?? "run index unreadable"} — the in-flight guard cannot see other processes' runs`);
1795
+ else if (synced.note !== void 0) ctx.logger.warn(`evolution-commands: ${synced.note}`);
1677
1796
  const ioRegistry = ctx.get("evolutionIo");
1678
1797
  const subagents = ctx.get("subagents");
1679
1798
  if (!ioRegistry) return err("Evolution IO registry not mounted — maintenance scan unavailable.");
1680
1799
  if (!subagents) return err("Subagents service not mounted — maintenance scan unavailable.");
1681
- try {
1682
- maintainInFlightSince = Date.now();
1683
- const library = newSkillLibrary({
1684
- config: { root: skillsRootValue },
1685
- io: ioRegistry.provider()
1686
- });
1687
- const enrichment = await buildEnrichment(ctx, library);
1688
- const outcome = await runMaintain({
1689
- library,
1690
- subagents,
1691
- parent: invocation.agent,
1692
- evolutionPolicy: { get: () => ctx.get("evolutionPolicy")?.get() },
1693
- tools: { get: (name) => ctx.get("tools")?.get(name) },
1694
- logger: { warn: (message) => {
1800
+ const running = registry.inFlight();
1801
+ if (running) {
1802
+ const elapsed = Math.max(1, Math.round((Date.now() - running.startedAt) / 1e3));
1803
+ return err(`Maintenance run ${running.id} is already running (started ~${elapsed}s ago) — re-submitting now would start a second scan. Status: /evolution maintain status ${running.id}; stop it: /evolution maintain cancel ${running.id}.`);
1804
+ }
1805
+ const cooldownMs = clampedNumber(config.maintainCooldownMs, 3e4, { min: 0 });
1806
+ const lastSettled = registry.lastSettledAt();
1807
+ const sinceLast = lastSettled === void 0 ? Number.POSITIVE_INFINITY : Date.now() - lastSettled;
1808
+ if (cooldownMs > 0 && sinceLast < cooldownMs) {
1809
+ const remaining = Math.ceil((cooldownMs - sinceLast) / 1e3);
1810
+ const latest = registry.runs().find((record) => record.state !== "running");
1811
+ return err(`Maintenance cooldown active (${remaining}s)${latest === void 0 ? "" : ` — latest run ${latest.id}`}; re-running now would spend another model call.`);
1812
+ }
1813
+ const runHome = evolutionHome();
1814
+ const runRoot = evolutionRoot();
1815
+ const handle = registry.begin("maintain");
1816
+ (async () => {
1817
+ try {
1818
+ const library = newSkillLibrary({
1819
+ config: { root: skillsRootValue },
1820
+ io: ioRegistry.provider()
1821
+ });
1822
+ const enrichment = await buildEnrichment(ctx, library);
1823
+ const outcome = await runMaintain({
1824
+ library,
1825
+ subagents,
1826
+ parent: invocation.agent,
1827
+ evolutionPolicy: { get: () => ctx.get("evolutionPolicy")?.get() },
1828
+ tools: { get: (name) => ctx.get("tools")?.get(name) },
1829
+ logger: { warn: (message) => {
1830
+ ctx.logger.warn(message);
1831
+ } },
1832
+ rootExists: skillsRootValue === "" ? void 0 : () => ioRegistry.provider().exists(skillsRootValue),
1833
+ skillRoot: skillsRootValue
1834
+ }, {
1835
+ timeoutMs: runTimeoutMs,
1836
+ signal: handle.signal,
1837
+ runId: handle.id,
1838
+ enrichment: () => enrichment
1839
+ });
1840
+ const endedAt = Date.now();
1841
+ const resultRef = await writeMaintainReport(ioRegistry.provider(), {
1842
+ runId: handle.id,
1843
+ ...outcome.ok ? {
1844
+ verdict: outcome.verdict,
1845
+ recommendationCount: outcome.recommendationCount
1846
+ } : {},
1847
+ ...outcome.ok ? {} : { failure: handle.signal.aborted ? "cancelled: the scan was stopped before it settled" : outcome.error ?? "Maintenance scan failed." },
1848
+ startedAt: handle.startedAt,
1849
+ endedAt,
1850
+ text: outcome.text ?? ""
1851
+ }, (message) => {
1695
1852
  ctx.logger.warn(message);
1696
- } },
1697
- rootExists: skillsRootValue === "" ? void 0 : () => ioRegistry.provider().exists(skillsRootValue),
1698
- skillRoot: skillsRootValue
1699
- }, {
1700
- timeoutMs: runTimeoutMs,
1701
- signal: invocation.signal,
1702
- enrichment: () => enrichment
1703
- });
1704
- if (outcome.ok) lastMaintainRunId = outcome.runId ?? "";
1705
- if (!outcome.ok) return err(outcome.error ?? "Maintenance scan failed.");
1706
- appendEvolutionEvent(ioRegistry.provider(), eventsFile(evolutionRoot()), {
1707
- type: "maintain",
1708
- source: "manual",
1709
- runId: outcome.runId,
1710
- verdict: outcome.verdict,
1711
- recommendations: outcome.recommendationCount
1712
- }).catch((error) => {
1713
- ctx.logger.warn(`evolution-commands: failed to record maintain event: ${String(error)}`);
1714
- });
1715
- return ok(`Maintenance scan ${outcome.runId}:\n${outcome.text ?? ""}`);
1716
- } catch (error) {
1717
- return err(`Maintenance scan failed: ${error instanceof Error ? error.message : String(error)}`);
1718
- } finally {
1719
- maintainInFlightSince = 0;
1720
- lastMaintainAt = Date.now();
1853
+ }, runHome);
1854
+ const state = outcome.ok ? "succeeded" : handle.signal.aborted ? "cancelled" : "failed";
1855
+ await registry.settle(handle.id, {
1856
+ state,
1857
+ ...resultRef === void 0 ? {} : { resultRef },
1858
+ ...outcome.ok ? {} : { failure: handle.signal.aborted ? "cancelled: the scan was stopped before it settled" : outcome.error ?? "Maintenance scan failed." }
1859
+ });
1860
+ if (outcome.ok) appendEvolutionEvent(ioRegistry.provider(), eventsFile(runRoot), {
1861
+ type: "maintain",
1862
+ source: "manual",
1863
+ runId: handle.id,
1864
+ verdict: outcome.verdict,
1865
+ recommendations: outcome.recommendationCount
1866
+ }).catch((error) => {
1867
+ ctx.logger.warn(`evolution-commands: failed to record maintain event: ${String(error)}`);
1868
+ });
1869
+ else ctx.logger.warn(`evolution-commands: maintenance run ${handle.id} produced no plan (${outcome.error ?? "unknown reason"})`);
1870
+ } catch (error) {
1871
+ const aborted = handle.signal.aborted;
1872
+ const failure = aborted ? "cancelled: the scan was stopped before it settled" : `Maintenance scan failed: ${error instanceof Error ? error.message : String(error)}`;
1873
+ await registry.settle(handle.id, {
1874
+ state: aborted ? "cancelled" : "failed",
1875
+ failure
1876
+ });
1877
+ ctx.logger.warn(`evolution-commands: maintenance run ${handle.id} threw (${failure})`);
1878
+ }
1879
+ })();
1880
+ return ok([
1881
+ `Maintenance run ${handle.id} started — the scan keeps running after this reply.`,
1882
+ `- status: /evolution maintain status ${handle.id}`,
1883
+ `- result: /evolution maintain report ${handle.id}`,
1884
+ `- stop: /evolution maintain cancel ${handle.id}`
1885
+ ].join("\n"));
1886
+ }
1887
+ const maintainStatus = /^maintain\s+status(?:\s+(\S+))?\s*$/.exec(input);
1888
+ if (maintainStatus) {
1889
+ const registry = runs;
1890
+ if (!registry) return err("Evolution IO registry not mounted — run status unavailable.");
1891
+ const synced = await syncRuns(registry);
1892
+ const wanted = maintainStatus[1];
1893
+ if (wanted !== void 0) {
1894
+ const record = registry.find(wanted);
1895
+ if (!record) {
1896
+ if (!synced.ok) return err(`Run ${wanted} is not in this process's history, and the home's run index is not readable (${synced.note ?? "unknown reason"}) — that is NOT proof the run never happened.`);
1897
+ return err(`No run ${wanted} in this home — /evolution maintain status lists the recent ones.`);
1898
+ }
1899
+ return ok(synced.ok ? renderRunRecord(record) : `${renderRunRecord(record)}\n(index not readable: ${synced.note ?? "unknown reason"} — only this process's view is shown)`);
1900
+ }
1901
+ if (!synced.ok) return err(`The run history of this home is not readable (${synced.note ?? "unknown reason"}) — this is NOT "no runs". Fix or remove <evolutionHome>/runs.json to start a clean history.`);
1902
+ const all = registry.runs();
1903
+ if (all.length === 0) return ok("No maintenance run recorded in this home yet — /evolution maintain starts one.");
1904
+ const live = all.filter((record) => record.state === "running");
1905
+ const recent = all.filter((record) => record.state !== "running").slice(0, 5);
1906
+ return ok([live.length === 0 ? "No maintenance run in flight." : `In flight:\n${live.map(renderRunRecord).join("\n")}`, recent.length === 0 ? "" : `Recent:\n${recent.map(renderRunRecord).join("\n")}`].filter((part) => part !== "").join("\n"));
1907
+ }
1908
+ const maintainReport = /^maintain\s+report\s+(\S+)\s*$/.exec(input);
1909
+ if (maintainReport) {
1910
+ const registry = runs;
1911
+ if (!registry) return err("Evolution IO registry not mounted — run results unavailable.");
1912
+ await syncRuns(registry);
1913
+ const wanted = maintainReport[1];
1914
+ if (wanted === void 0) return err("maintain report needs a run id: /evolution maintain report <id>.");
1915
+ const record = registry.find(wanted);
1916
+ if (!record) return err(`No run ${wanted} in this home — /evolution maintain status lists the recent ones.`);
1917
+ const ioRegistry = ctx.get("evolutionIo");
1918
+ if (!ioRegistry) return err("Evolution IO registry not mounted — run results unavailable.");
1919
+ const probed = await readRunReport(ioRegistry.provider(), record);
1920
+ if (probed.kind === "missing") return err(`Run ${record.id} (${record.state}) has NO result: no report was written${record.failure === void 0 ? "" : ` (${record.failure})`}.`);
1921
+ if (probed.kind === "unknown") return err(`Run ${record.id} has a report that could not be read (${probed.reason}) — this is not "no result".`);
1922
+ return ok(probed.text);
1923
+ }
1924
+ const maintainCancel = /^maintain\s+cancel\s+(\S+)\s*$/.exec(input);
1925
+ if (maintainCancel) {
1926
+ const registry = runs;
1927
+ if (!registry) return err("Evolution IO registry not mounted — run cancellation unavailable.");
1928
+ await syncRuns(registry);
1929
+ const wanted = maintainCancel[1];
1930
+ if (wanted === void 0) return err("maintain cancel needs a run id: /evolution maintain cancel <id>.");
1931
+ const record = registry.find(wanted);
1932
+ if (!record) return err(`No run ${wanted} in this home — /evolution maintain status lists the recent ones.`);
1933
+ if (!registry.cancel(record.id)) {
1934
+ if (record.state === "running") return err(`Run ${record.id} is running in ANOTHER process sharing this home — it can only be cancelled there.`);
1935
+ return err(`Run ${record.id} is already ${record.state} — nothing to cancel.`);
1721
1936
  }
1937
+ return ok(`Maintenance run ${record.id} cancelled — the scan stops at its next checkpoint. /evolution maintain status ${record.id} shows the terminal state.`);
1722
1938
  }
1723
- if (/^maintain\b/.test(input)) return err("Unknown maintain arguments: expected bare `maintain`, `maintain --timeout <ms>` or `maintain --timeout=<ms>` (a positive integer). Got: " + input);
1939
+ if (/^maintain\b/.test(input)) return err("Unknown maintain arguments: expected `maintain`, `maintain --timeout <ms>` / `maintain --timeout=<ms>`, `maintain status [<id>]`, `maintain report <id>` or `maintain cancel <id>`. Got: " + input);
1724
1940
  const presetInstall = /^preset install(?: --base (\S+))?$/.exec(input);
1725
1941
  if (presetInstall !== null || /^preset\b/.test(input)) {
1726
1942
  if (presetInstall === null) return err("Unknown preset arguments: expected `preset install` or `preset install --base <name>[,<name>...]` (the names live in the agent package's bases.json). Got: " + input);
@@ -24,10 +24,11 @@ export interface Config {
24
24
  * of dropping it silently; the plugin never reads it as a root. */
25
25
  skillsRoot?: string | undefined;
26
26
  /** Cooldown window for scan commands (ms) — misclick/rapid-trigger guard;
27
- * secondary calls inside the window return the previous runId instead of
28
- * spending another model call. Default 30s (0.3.5). NOTE: the window starts
29
- * AFTER a run settles (lastMaintainAt updates post-run) — it does NOT dedupe
30
- * in-flight runs, so the old 130s ">= timeout" rationale was a comment bug.
27
+ * secondary calls inside the window are refused instead of spending another
28
+ * model call. Default 30s (0.3.5). NOTE: the window starts AFTER a run settles
29
+ * (0.19.0/S2: the run registry's newest terminal time) — it does NOT dedupe
30
+ * in-flight runs; that refusal belongs to the registry's in-flight query, not
31
+ * to this window (the old 130s ">= timeout" rationale was a comment bug).
31
32
  * Transient (per-process). */
32
33
  maintainCooldownMs?: number | undefined;
33
34
  /** Subagent deadline for one maintenance scan (ms). Default 600s (0.3.10):
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-commands",
3
3
  "description": "Human commands for the evolution family (/evolution pending|curator|maintain|doctor) (community build)",
4
- "version": "0.18.0",
4
+ "version": "0.19.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -27,12 +27,12 @@
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
29
  "@deepseek-ai/schemastery": "^3.18.4",
30
- "@lmzhen/dsh-evolution-approval": "^0.18.0",
31
- "@lmzhen/dsh-evolution-core": "^0.18.0",
32
- "@lmzhen/dsh-evolution-maintenance": "^0.18.0"
30
+ "@lmzhen/dsh-evolution-approval": "^0.19.0",
31
+ "@lmzhen/dsh-evolution-core": "^0.19.0",
32
+ "@lmzhen/dsh-evolution-maintenance": "^0.19.0"
33
33
  },
34
34
  "optionalDependencies": {
35
- "@lmzhen/dsh-evolution-agent-preset": "^0.18.0"
35
+ "@lmzhen/dsh-evolution-agent-preset": "^0.19.0"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@deepseek-ai/cordis": "^4.0.1",