@lmzhen/dsh-evolution-core 0.18.1 → 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
@@ -1,6 +1,6 @@
1
1
  # @lmzhen/dsh-evolution-core
2
2
 
3
- Shared pure library for the family: `SkillLibrary`/`MemoryStore`, the prompts and guidance text, the threat/quality/drift signals, the skill CONTENT history (`skill-history.ts`: content-addressed versions behind every write) and the frame classification the plan path reports over (`evidence.ts`), plus the IO primitives (`nodeEvolutionIo`, `transactIo`, `evolutionIoAdapter`) and the two helpers every host route surface shares (`http-routes.ts`: the loopback fence, the bounded JSON body reader and the JSON writer). It is **not a Cordis row** — it registers no service, tool or prompt
3
+ Shared pure library for the family: `SkillLibrary`/`MemoryStore`, the prompts and guidance text, the threat/quality/drift signals, the skill CONTENT history (`skill-history.ts`: content-addressed versions behind every write) and the frame classification the plan path reports over (`evidence.ts`), plus the IO primitives (`nodeEvolutionIo`, `transactIo`, `evolutionIoAdapter`), the two helpers every host route surface shares (`http-routes.ts`: the loopback fence, the bounded JSON body reader and the JSON writer), the run lifecycle owner and the shared report window (`run-registry.ts`: the single owner of what is in flight, how a run ended and where its result lives, with read-time orphan convergence and no timer; `reports.ts`: one bucket sweep for every run-report kind, plus the per-home lock target both sweeps hold), and the long-run inventory the published table is rendered from (`long-runs.ts`). It is **not a Cordis row** — it registers no service, tool or prompt
4
4
  section, and importing the package root is its only entry.
5
5
 
6
6
  ## Model surface
package/lib/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { parse } from "yaml";
2
2
  import { basename, dirname, join, resolve } from "node:path";
3
3
  import { cp, lstat, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
4
- import { createHash, randomBytes } from "node:crypto";
4
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
5
5
  import { homedir } from "node:os";
6
6
  import { existsSync, readFileSync, readdirSync } from "node:fs";
7
7
  import { fileURLToPath } from "node:url";
@@ -5925,6 +5925,464 @@ function sweepReviewChannelSessions(isAlive) {
5925
5925
  return removed;
5926
5926
  }
5927
5927
  //#endregion
5928
+ //#region lib/types/run-registry.js
5929
+ /**
5930
+ * 0.19.0 (S2): the family's run-lifecycle owner.
5931
+ *
5932
+ * A "run" is work that can outlive one interaction — today the maintenance scan.
5933
+ * Before this module the only record of such work was the requesting invocation:
5934
+ * `lastMaintainAt` / `lastMaintainRunId` / `maintainInFlightSince` were
5935
+ * module-scope globals in evolution-commands, and the plan text existed ONLY in
5936
+ * the command's return string. An interaction that ended (measured: the desktop
5937
+ * shell hangs up at ~305 s) therefore erased every trace of a still-running scan.
5938
+ *
5939
+ * This registry is the single owner of (a) what is in flight, (b) how a run
5940
+ * ended, and (c) WHERE its result lives (`resultRef` is a path, never the result
5941
+ * itself — the report file is the one result home).
5942
+ *
5943
+ * Two disciplines are structural, not stylistic:
5944
+ *
5945
+ * - **No timer.** A run whose process died is converged at READ time: the index
5946
+ * file can only carry `running` records written by a PREVIOUS process life,
5947
+ * because the live ones are in memory. `load()` therefore rewrites every
5948
+ * `running` record as `failed(orphan)` once — the same "converge when read"
5949
+ * posture the approval window uses for its TTL.
5950
+ * - **One terminal state.** `settle()` refuses to overwrite a record that is
5951
+ * already terminal, so a cancel that raced a failure cannot flip the answer
5952
+ * the operator already read.
5953
+ *
5954
+ * The store is PER INSTANCE (a factory, not module scope): one registry per
5955
+ * mounted row, disposed with its fiber. There is no module-scope mutable state
5956
+ * here to register under N12.
5957
+ * @module
5958
+ */
5959
+ /** Current index schema. Bump only for a structural change. */
5960
+ const RUNS_SCHEMA_VERSION = 1;
5961
+ /** Retained run records, newest first. */
5962
+ const DEFAULT_RUN_RECORDS = 50;
5963
+ /** Index path for a home. */
5964
+ function runsFile(home) {
5965
+ return join(home, "runs.json");
5966
+ }
5967
+ const TERMINAL = new Set([
5968
+ "succeeded",
5969
+ "failed",
5970
+ "cancelled"
5971
+ ]);
5972
+ /**
5973
+ * Parse an index body into records.
5974
+ *
5975
+ * `ok: false` means the body could not be read as an index at all — the caller
5976
+ * decides what to say (`load()` reports it; `persist()` merges with what it can).
5977
+ * @param raw - the file's text, or null when it does not exist.
5978
+ * @returns the parsed records and whether the body was an index.
5979
+ */
5980
+ function parseIndex(raw) {
5981
+ if (raw === null) return {
5982
+ ok: true,
5983
+ records: [],
5984
+ foreign: []
5985
+ };
5986
+ let parsed;
5987
+ try {
5988
+ parsed = JSON.parse(raw);
5989
+ } catch {
5990
+ return {
5991
+ ok: false,
5992
+ records: [],
5993
+ foreign: []
5994
+ };
5995
+ }
5996
+ const rows = parsed.runs;
5997
+ if (!Array.isArray(rows)) return {
5998
+ ok: false,
5999
+ records: [],
6000
+ foreign: []
6001
+ };
6002
+ const notes = [];
6003
+ const version = parsed.schemaVersion;
6004
+ if (typeof version === "number" && version !== 1) notes.push(`the index declares schemaVersion ${version} while this build writes 1`);
6005
+ const records = [];
6006
+ const foreign = [];
6007
+ for (const row of rows) {
6008
+ const record = row;
6009
+ if (typeof record.id !== "string" || typeof record.startedAt !== "number") {
6010
+ foreign.push(row);
6011
+ continue;
6012
+ }
6013
+ if (typeof record.kind !== "string" || typeof record.state !== "string") {
6014
+ foreign.push(row);
6015
+ continue;
6016
+ }
6017
+ if (record.state !== "running" && !TERMINAL.has(record.state)) {
6018
+ foreign.push(row);
6019
+ continue;
6020
+ }
6021
+ records.push({
6022
+ id: record.id,
6023
+ kind: record.kind,
6024
+ state: record.state,
6025
+ startedAt: record.startedAt,
6026
+ ...typeof record.pid === "number" ? { pid: record.pid } : {},
6027
+ ...typeof record.endedAt === "number" ? { endedAt: record.endedAt } : {},
6028
+ ...typeof record.resultRef === "string" ? { resultRef: record.resultRef } : {},
6029
+ ...typeof record.failure === "string" ? { failure: record.failure } : {}
6030
+ });
6031
+ }
6032
+ return {
6033
+ ok: true,
6034
+ records,
6035
+ foreign,
6036
+ ...notes.length > 0 ? { note: notes.join("; ") } : {}
6037
+ };
6038
+ }
6039
+ /**
6040
+ * Create the run registry for one mounted row.
6041
+ * @param options - io seam, evolution home, warn channel, clock and record cap.
6042
+ * @returns the registry; call {@link RunRegistry.load} once before answering reads.
6043
+ */
6044
+ function newRunRegistry(options) {
6045
+ const now = options.now ?? (() => Date.now());
6046
+ const maxRecords = clampedNumber(options.maxRecords ?? 50, 50, { min: 1 });
6047
+ const warn = options.warn ?? (() => {});
6048
+ const path = runsFile(options.home);
6049
+ const records = [];
6050
+ const controllers = /* @__PURE__ */ new Map();
6051
+ const ordered = () => [...records].sort((a, b) => b.startedAt - a.startedAt);
6052
+ /** Keep every RUNNING record plus the newest terminal ones: memory and the
6053
+ * index share one cap, so the two can never disagree about the history. */
6054
+ const trim = () => {
6055
+ const terminal = records.filter((record) => TERMINAL.has(record.state)).sort((a, b) => (b.endedAt ?? b.startedAt) - (a.endedAt ?? a.startedAt));
6056
+ for (const stale of terminal.slice(maxRecords)) {
6057
+ const index = records.indexOf(stale);
6058
+ if (index >= 0) records.splice(index, 1);
6059
+ }
6060
+ };
6061
+ const persist = async () => {
6062
+ trim();
6063
+ try {
6064
+ await transactIo(options.io, path, (current) => {
6065
+ const onDisk = parseIndex(current);
6066
+ const merged = /* @__PURE__ */ new Map();
6067
+ for (const record of onDisk.records) merged.set(record.id, record);
6068
+ for (const record of records) merged.set(record.id, record);
6069
+ const view = [...merged.values()].sort((a, b) => b.startedAt - a.startedAt);
6070
+ const live = view.filter((record) => record.state === "running");
6071
+ const done = view.filter((record) => TERMINAL.has(record.state)).slice(0, maxRecords);
6072
+ return JSON.stringify({
6073
+ schemaVersion: 1,
6074
+ runs: [
6075
+ ...live,
6076
+ ...done,
6077
+ ...onDisk.foreign
6078
+ ]
6079
+ }, null, 2);
6080
+ });
6081
+ } catch (error) {
6082
+ warn(`evolution-core: could not persist the run index (${error instanceof Error ? error.message : String(error)})`);
6083
+ }
6084
+ };
6085
+ return {
6086
+ async load() {
6087
+ let raw;
6088
+ try {
6089
+ raw = await options.io.readText(path);
6090
+ } catch (error) {
6091
+ return {
6092
+ ok: false,
6093
+ note: `run index unreadable: ${error instanceof Error ? error.message : String(error)}`
6094
+ };
6095
+ }
6096
+ const parsed = parseIndex(raw);
6097
+ if (!parsed.ok) return {
6098
+ ok: false,
6099
+ note: "run index could not be read as an index (not JSON, or no runs array) — the previous run history is unknown (the reports are unaffected)"
6100
+ };
6101
+ let converged = 0;
6102
+ let liveElsewhere = 0;
6103
+ for (const record of parsed.records) {
6104
+ if (records.some((item) => item.id === record.id)) continue;
6105
+ if (record.state !== "running") {
6106
+ records.push(record);
6107
+ continue;
6108
+ }
6109
+ if (record.pid !== void 0 && isProcessAlive(record.pid)) {
6110
+ liveElsewhere++;
6111
+ records.push(record);
6112
+ continue;
6113
+ }
6114
+ converged++;
6115
+ records.push({
6116
+ ...record,
6117
+ state: "failed",
6118
+ endedAt: now(),
6119
+ failure: "no live owner process — the run was interrupted before it settled (orphan)"
6120
+ });
6121
+ }
6122
+ if (converged > 0) await persist();
6123
+ const notes = [
6124
+ parsed.note,
6125
+ converged > 0 ? `${converged} run(s) with no live owner were recorded as failed(orphan)` : void 0,
6126
+ liveElsewhere > 0 ? `${liveElsewhere} run(s) are in flight in another process sharing this home` : void 0
6127
+ ].filter((note) => note !== void 0);
6128
+ return {
6129
+ ok: true,
6130
+ ...notes.length > 0 ? { note: notes.join("; ") } : {}
6131
+ };
6132
+ },
6133
+ begin(kind) {
6134
+ const id = randomUUID();
6135
+ const startedAt = now();
6136
+ const controller = new AbortController();
6137
+ controllers.set(id, controller);
6138
+ records.push({
6139
+ id,
6140
+ kind,
6141
+ state: "running",
6142
+ startedAt,
6143
+ pid: process.pid
6144
+ });
6145
+ persist();
6146
+ return {
6147
+ id,
6148
+ kind,
6149
+ signal: controller.signal,
6150
+ startedAt
6151
+ };
6152
+ },
6153
+ async settle(id, outcome) {
6154
+ const record = records.find((item) => item.id === id);
6155
+ if (!record || TERMINAL.has(record.state)) return;
6156
+ record.state = outcome.state;
6157
+ record.endedAt = now();
6158
+ if (outcome.resultRef !== void 0) record.resultRef = outcome.resultRef;
6159
+ if (outcome.failure !== void 0) record.failure = outcome.failure;
6160
+ controllers.delete(id);
6161
+ await persist();
6162
+ },
6163
+ inFlight() {
6164
+ return ordered().find((record) => record.state === "running");
6165
+ },
6166
+ lastSettledAt() {
6167
+ return ordered().find((record) => TERMINAL.has(record.state))?.endedAt;
6168
+ },
6169
+ runs() {
6170
+ return ordered();
6171
+ },
6172
+ find(id) {
6173
+ return records.find((record) => record.id === id);
6174
+ },
6175
+ cancel(id) {
6176
+ const controller = controllers.get(id);
6177
+ if (controller === void 0) return false;
6178
+ const record = records.find((item) => item.id === id);
6179
+ if (!record || record.state !== "running") return false;
6180
+ controller.abort();
6181
+ controllers.delete(id);
6182
+ record.state = "cancelled";
6183
+ record.endedAt = now();
6184
+ record.failure = "cancelled by operator";
6185
+ persist();
6186
+ return true;
6187
+ },
6188
+ cancelAll() {
6189
+ let count = 0;
6190
+ for (const id of [...controllers.keys()]) if (this.cancel(id)) count++;
6191
+ return count;
6192
+ }
6193
+ };
6194
+ }
6195
+ //#endregion
6196
+ //#region lib/types/reports.js
6197
+ /**
6198
+ * 0.19.0 (S2): the family's ONE report-retention sweep.
6199
+ *
6200
+ * Reports accumulate by design (every run writes one), so each kind needs a
6201
+ * window or the reports home becomes an unbounded directory. The logic lived
6202
+ * inside the curator only; the maintenance run reports added in 0.19.0 needed
6203
+ * the same thing, and a second copy of "list, order, delete beyond the window"
6204
+ * is exactly the kind of near-duplicate this family treats as a defect.
6205
+ *
6206
+ * Three behaviours are the curator's, kept verbatim because they are the reason
6207
+ * the sweep is safe to run at all:
6208
+ *
6209
+ * - An UNLISTABLE directory is not "no reports" — nothing is deleted and the
6210
+ * caller is told (the same three-state rule the probe files use).
6211
+ * - A report with no usable timestamp is KEPT (never delete what cannot be
6212
+ * ordered) and the fact is said out loud once.
6213
+ * - The order is TOTAL: declared time, then mtime, then name. A tie falling
6214
+ * through to readdir order evicted a different set per platform (CI-caught).
6215
+ * @module
6216
+ */
6217
+ /**
6218
+ * The lock target a reports sweep holds for one home.
6219
+ *
6220
+ * `transactIo` takes the IO write lock on THIS path (minting `<path>.lock` with
6221
+ * the io protocol's `pid:token` body) and holds it for the whole task; nothing
6222
+ * ever writes the target, because the sweep's exclusion is the point. It lives
6223
+ * inside `<home>/reports` next to the directory it protects (the sweeps' own name
6224
+ * filters ignore it, and the lock file is removed on release).
6225
+ *
6226
+ * Moved here from evolution-curator in 0.19.0 (S2): the maintenance reports'
6227
+ * sweep needs the SAME exclusion, and a second lock would protect nothing.
6228
+ * @returns the lock target path.
6229
+ */
6230
+ function reportsSweepLockTarget() {
6231
+ return join(evolutionHome(), "reports", ".retention");
6232
+ }
6233
+ /**
6234
+ * Delete the reports beyond each bucket's window, each with its `.md` digest.
6235
+ *
6236
+ * Best-effort by construction: pruning never fails the run that wrote a report.
6237
+ * @param options - io seam, directory, windows, warn channel and owner name.
6238
+ * @returns nothing; deletions and skips are reported through `warn`.
6239
+ */
6240
+ async function sweepReports(options) {
6241
+ const warn = options.warn ?? (() => {});
6242
+ const listed = await probeList(options.io, options.dir);
6243
+ if (isUnknown(listed)) {
6244
+ warn(`${options.owner}: report retention skipped — the reports directory could not be listed (${listed.reason})`);
6245
+ return;
6246
+ }
6247
+ const entries = isPresent(listed) ? listed.value : [];
6248
+ const buckets = [...options.buckets].sort((a, b) => b.prefix.length - a.prefix.length);
6249
+ const dated = new Map(buckets.map((bucket) => [bucket.prefix, []]));
6250
+ let unorderableWarned = false;
6251
+ for (const name of entries) {
6252
+ if (!name.endsWith(".json")) continue;
6253
+ const bucket = buckets.find((item) => name.startsWith(item.prefix));
6254
+ if (!bucket) continue;
6255
+ try {
6256
+ const path = join(options.dir, name);
6257
+ const raw = await options.io.readText(path);
6258
+ if (raw === null) continue;
6259
+ const startedAt = await reportTime(options.io, path, JSON.parse(raw));
6260
+ const probed = await probeMtime(options.io, path);
6261
+ const mtime = isPresent(probed) ? probed.value : null;
6262
+ if (startedAt !== null) dated.get(bucket.prefix)?.push({
6263
+ name,
6264
+ startedAt,
6265
+ mtime
6266
+ });
6267
+ else if (!unorderableWarned) {
6268
+ unorderableWarned = true;
6269
+ warn(`${options.owner}: report "${name}" carries no usable timestamp and this backend has no mtime probe - it is kept outside the retention window`);
6270
+ }
6271
+ } catch {}
6272
+ }
6273
+ const newestFirst = (a, b) => b.startedAt - a.startedAt || (b.mtime ?? 0) - (a.mtime ?? 0) || b.name.localeCompare(a.name);
6274
+ for (const bucket of buckets) {
6275
+ const rows = dated.get(bucket.prefix) ?? [];
6276
+ rows.sort(newestFirst);
6277
+ for (const old of rows.slice(bucket.keep)) {
6278
+ const stem = old.name.replace(/\.json$/, "");
6279
+ try {
6280
+ await options.io.remove(join(options.dir, old.name));
6281
+ } catch {}
6282
+ try {
6283
+ await options.io.remove(join(options.dir, `${stem}.md`));
6284
+ } catch {}
6285
+ }
6286
+ }
6287
+ }
6288
+ //#endregion
6289
+ //#region lib/types/long-runs.js
6290
+ /**
6291
+ * 0.19.0 (S4): the family's long-run inventory — one table, one owner per entry.
6292
+ *
6293
+ * A "long run" is work whose duration is NOT bounded by the interaction that asked
6294
+ * for it: a maintenance scan, a curator pass, a review pass. The measured fact this
6295
+ * table exists to record is that a CLIENT can end a request long before the work is
6296
+ * done (measured: the desktop shell ends a request at ~305 s while the same command
6297
+ * finishes in 949.7 s on the web plane), so the lifetime of such work must come from
6298
+ * its OWNER — never from the request.
6299
+ *
6300
+ * Two fields carry the judgments, and a spec pins both:
6301
+ *
6302
+ * - `lifetime`: `'plugin'` entries must name an owner and a result home (the work
6303
+ * outlives the request and must be readable afterwards); `'request'` entries
6304
+ * must say WHY their cost cannot reach the wall.
6305
+ * - `measured`: only measured numbers, with their basis. An unmeasured bound is
6306
+ * written as exactly that — this table is not a place to guess.
6307
+ *
6308
+ * The rendered table is pinned byte-for-byte into `packages/README.md` by
6309
+ * `tests/long-runs.spec.ts` (the T-WD2 pattern the command registry already uses),
6310
+ * so the published table cannot drift from this list.
6311
+ * @module
6312
+ */
6313
+ /**
6314
+ * The inventory. Order is the rendered order; a spec pins it into the README.
6315
+ */
6316
+ const LONG_RUNS = [
6317
+ {
6318
+ id: "/evolution maintain",
6319
+ kind: "command",
6320
+ owner: "evolution-commands",
6321
+ lifetime: "plugin",
6322
+ measured: "the same scan measured 949.7 s end to end on the web plane (no Electron); the desktop shell ends the REQUEST at ~305 s (four runs: 305.6/307.0/306.0/305.7 s)",
6323
+ budget: "maintainTimeoutMs (row config, default 600000) or --timeout <ms> for one run",
6324
+ resultHome: "<evolutionHome>/reports/maintain-<runId>.{json,md}, indexed in <evolutionHome>/runs.json",
6325
+ note: "The ~305 s wall is a CLIENT lifetime, not a budget: the run keeps working and the result is read back with maintain status/report. Its cancellation is the run registry handle (maintain cancel <id>) or plugin dispose."
6326
+ },
6327
+ {
6328
+ id: "maintenance_probe",
6329
+ kind: "tool",
6330
+ owner: "evolution-maintenance",
6331
+ lifetime: "request",
6332
+ measured: "188/220/225 ms on the deployed library (28 skills / 418 KB); ~5.5 ms per skill and linear (100 skills 0.4-0.6 s, 400 skills 1.9-2.5 s)",
6333
+ budget: "none, deliberately: reaching the ~305 s wall would take roughly 5e4 skills",
6334
+ resultHome: "the tool result (JSON detail), answered in-turn",
6335
+ note: "O(2N) tree reads (enrichment + snapshot), uncached by design. Reopen the budget question when the library nears ~1e4 skills, when the io provider stops being local, or when a model leg enters a tool execute."
6336
+ },
6337
+ {
6338
+ id: "skill_manage",
6339
+ kind: "tool",
6340
+ owner: "tool-skill-manage",
6341
+ lifetime: "request",
6342
+ measured: "not measured as a duration: single-file IO plus a library listing the cross-source gate already reads; the only unbounded wait is a HUMAN answer",
6343
+ budget: "its own gate deadline (skillWriteConfirm + pendingTtlMs) — a second budget here would compete with it",
6344
+ resultHome: "the tool result, plus the mutation audit records",
6345
+ note: "The human wait is the reason this entry exists: it is bounded by the write gate, which the tool owns."
6346
+ },
6347
+ {
6348
+ id: "memory",
6349
+ kind: "tool",
6350
+ owner: "tool-memory",
6351
+ lifetime: "request",
6352
+ measured: "single-file IO on the memory store",
6353
+ budget: "none needed",
6354
+ resultHome: "the tool result",
6355
+ note: "No tree walk, no model leg, no human wait."
6356
+ },
6357
+ {
6358
+ id: "curator pass",
6359
+ kind: "pass",
6360
+ owner: "evolution-curator",
6361
+ lifetime: "plugin",
6362
+ measured: "not measured end to end; the LLM nomination leg is bounded separately",
6363
+ budget: "curatorReviewTimeoutMs (E3 parameter) for the LLM leg",
6364
+ resultHome: "<evolutionHome>/reports/curator-<runId>.{json,md}",
6365
+ note: "Runs from the plugin interval timer, and a failed or timed-out LLM leg is a distinguishable state in the run report (nominationsWarnings)."
6366
+ },
6367
+ {
6368
+ id: "review pass",
6369
+ kind: "pass",
6370
+ owner: "evolution-review",
6371
+ lifetime: "plugin",
6372
+ measured: "not measured end to end; the subagent leg carries its own deadline",
6373
+ budget: "reviewTimeoutMs (E2 parameter)",
6374
+ resultHome: "the review inbox plus evolution/plan-applied",
6375
+ note: "The write leg replays a staged plan through the registered runner; cancellation follows the plugin fiber."
6376
+ }
6377
+ ];
6378
+ /**
6379
+ * Render the inventory as the README table rows (no header).
6380
+ * @returns one markdown row per entry, in inventory order.
6381
+ */
6382
+ function renderLongRunTable() {
6383
+ return LONG_RUNS.map((entry) => `| \`${entry.id}\` | ${entry.kind} | ${entry.owner} | ${entry.lifetime} | ${entry.measured} | ${entry.resultHome} |`).join("\n");
6384
+ }
6385
+ //#endregion
5928
6386
  //#region lib/types/platform-services.js
5929
6387
  /**
5930
6388
  * Every platform service the family reaches for with `ctx.get`, and what degrades without it.
@@ -12959,4 +13417,4 @@ function foldUntrustedLines(text, limit) {
12959
13417
  return limit > 0 ? marked.slice(0, limit) : marked;
12960
13418
  }
12961
13419
  //#endregion
12962
- export { AGENT_PRESET_PACKAGE, AGENT_PRESET_ROOT_HINT, ALIVE_LOCK_TAKEOVER_MS, AUTHORING_DESCRIPTION_BAR, AUTHORING_SPLIT_LINE_CHARS, BASELINE_ACTION, CITATION_SUPPORT_DIRS, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CONTENT_SPLIT_HINT, COST_ASCII_TOKEN_HIGH, COST_ASCII_TOKEN_LOW, COST_CJK_TOKEN_HIGH, COST_CJK_TOKEN_LOW, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEAD_LOCK_TAKEOVER_MS, DEDUP_MAX_PAIR_COMPARISONS, DEDUP_SIMILARITY_THRESHOLD, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_ARCHIVE_RETENTION_POLICY, DEFAULT_CITATION_POLICY, DEFAULT_CITATION_REF_BUDGET, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_MODEL, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MEMORY_REVIEW_MODEL, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_PROBE_TIMEOUT_MS, DEFAULT_REFERENCE_REWRITE_POLICY, DEFAULT_REVIEW_CONTEXT_MESSAGES, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_MESSAGE_CHARS, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_REVIEW_TIMEOUT_MS, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_MODEL, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_SKILL_VERSION_KEEP, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_SUPPORT_FILE_CHAR_POLICY, DEFAULT_SUPPORT_READ_TOOL_NAMES, DEFAULT_THREAT_MAX_SCAN_CHARS, DEFAULT_USER_CHAR_LIMIT, DISPATCH_EVENT_TYPES, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, EMPTY_LOCK_TAKEOVER_MS, EMPTY_PATCH_SEED, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_ERRORS, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FAMILY_SESSION_TOOL_NAMES, FORBIDDEN_CONTROL_KEYS, FUZZY_MAX_PATTERN_CHARS, FUZZY_MAX_WORK, HEALTH_STAMP_RE, HISTORY_DIR, HISTORY_INDEX_VERSION, HOOK_LINE_RE, INSTANCE_KEYS, KEEP_LINE_RE, LOCK_BODY_RE, LOCK_SUFFIX, LOCK_TEAR_TAKEOVER_MS, LOW_QUALITY_THRESHOLD, LostWriteLock, MAINTAIN_OUTPUT_INSTRUCTION, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_REQUEST_BODY_BYTES, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MAX_SUPPORT_READ_PATHS, MAX_TIMER_DELAY_MS, MEMORY_GUIDANCE_SECTION_ORDER, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, NAMESPACE_MIGRATIONS, NATIVE_CALL_EVENT, NATIVE_RESULT_EVENT, PARAM_ALIASES, PARAM_EXPOSURE, PARAM_GROUP_LABELS, PARAM_NAMESPACES, PATTERN_OVERLAP, PLATFORM_CATALOG_DESCRIPTION_DEFAULT, PLATFORM_ESTIMATE_CHARS_PER_TOKEN, PLATFORM_SERVICE_PROBES, POLICY_STAGE_DEFAULTS, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, PTC_DISPATCH_EVENT, PTC_DISPATCH_START_EVENT, QUALITY_WEIGHTS, READ_REQUIRED_ACTIONS, RESTRUCTURE_TARGET_RE, RETENTION_FEEDBACK_KEEP_RATIO, RETENTION_FEEDBACK_MIN_CHARS, SKILLS_GUIDANCE, SKILLS_GUIDANCE_SECTION_ORDER, SKILL_ACTION_REQUIRED_FIELDS, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUMMARY_DUPLICATE_HINT_THRESHOLD, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, TEXT_DIFF_MAX_CHARS, TEXT_DIFF_MAX_LINES, THREAT_EXEMPTION_HINT, TOKEN_ASCII_WEIGHT, TOKEN_CJK_WEIGHT, ToolDispatchNormalizer, ToolDispatchPayloadError, UNTRUSTED_LINE_BREAK, UNTRUSTED_LINE_MARK, UPSTREAM_LIMIT_CHARS_PER_TOKEN, VERSION_SUMMARY_MAX_CHARS, absentHostServices, advanceReview, affinity, ageCopyKey, allowRowCollisions, anchorVerdict, appendEvolutionEvent, applyCuratorLifecycleFields, applyCuratorMetaFields, applyNamespaceMigration, applyReferenceRewrite, assertDispatchPayload, assertSkillsRootAliasRetired, assessStructureHealth, authoringFeedback, basePresetPlugins, blobPath, bodyCost, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpSupportRead, bumpUse, bumpView, callingScope, canonicalWriteId, claimInstance, clampOnce, clampedNumber, classifyNamespaceMigration, clearReviewChannel, collectReadSkillNames, composePresetComposition, composePresetEntry, composePresetInsert, composePresetRow, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, contentRetentionFeedback, countDispatches, createGateSet, daysSinceIso, decideTakeover, declaredReportStamp, describeReferenceRewrite, dispatchKeyOf, dispatchStepOf, displayBodyOf, duplicateHeadings, elapsedSince, emptyRecord, entryTarget, errorText, evaluateThreat, eventsFile, evidenceKindIndex, evolutionActivityProjection, evolutionEventPayloadIssue, evolutionEvidenceProjection, evolutionHome, evolutionIoAdapter, evolutionReadsProjection, evolutionRoot, evolutionSignalsProjection, filterUnreadSkillOps, findDriftSignal, foldCuratorFields, foldToolDispatches, foldTurnEvent, foldTurnEvents, foldUntrustedLines, frontmatterBlock, frontmatterCatalogInvalid, fuzzyPatch, getRecord, historyIndexFile, historyRoot, identity, idleDays, initialTurnFold, installEvolutionProjections, instanceClaimKey, instanceClaimedWriteSites, instanceHolder, isAbsent, isBookkeepingFrame, isCommittedWarning, isDeprecatedParamId, isFileShapedPath, isLoopbackRequest, isMissingPath, isPresent, isProcessAlive, isProgramToolName, isReviewChannelSession, isSkillReadToolName, isSkillToolName, isUnknown, isUnreadWrite, latestActivityAt, latestVersionAt, lifecycleCandidate, listEventArchives, loadMutations, loadRowOverrides, loadSuppressedNames, loadUsage, loadVersionContent, loadVersions, makeSerialQueue, mapProbe, markAgentCreated, markReviewChannel, markerEntryName, memoryRoot, mergePresetRow, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nearDuplicateSummaries, neutralizePromptVariables, newSkillLibrary, nextHistoryIndex, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, normalizeVersionSummary, observeEvent, orderVersions, overlongLines, paramNamespace, paramRowId, paramSettingsId, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, parseHistoryIndex, parseLockBody, partitionVersions, payloadText, pendingSelfCleanup, persistedWriteSite, persistedWriteSites, pickWithPolicy, planNamespaceMigration, planReferenceRewrite, planRehoming, policyStageLimits, presetPatchText, presetRowBlock, presetRowBody, presetRowId, probeAbsent, probeList, probeMtime, probePresent, probeReason, probeText, probeUnknown, projectionText, readDispatchSignal, readEvolutionEvents, readEvolutionTimeline, readHistoryIndex, readJsonBody, readJsonObject, readNumberParam, readParam, readingStats, recordMutation, recordVersions, redactSecrets, relatedSkillNames, releaseInstance, removePresetRow, renameWithRetry, renderCuratorReportMarkdown, reportTime, resolveBasePresetPatch, resolveCitations, resolveExecOrigins, resolveOrigins, resolveParamId, resolveRootConfig, resolveSkillsRoot, retainEventArchives, retirementReport, reviewPrompt, saveSuppressedNames, saveUsage, scanBodyHooks, scanContentThreats, scanMemoryThreats, scanThreats, scopedProbeReport, sessionAudited, sessionEvidenceIndex, sessionLastEventTime, sessionReadNames, sessionReadSkillNames, sessionSeesFamilyTools, sessionTurnSignals, skillReadNameOf, skillRowCells, skillStateKey, skillsRoot, supportFileReadOf, suppressedFile, sweepReviewChannelSessions, textDiffFacts, tokenLineFor, transactIo, transactTaskGuard, trimPatternBoundaries, unhookedSupportPointers, updateSuppressedNames, usageFile, usageObserved, userSetKeys, validateFrontmatter, validateRestructureTarget, valueOr, verifyPromptBundle, versionActionKind, versionRowCells, versionSummaryPrompt, versionTarget, vocabulary, writeDurableTmp, writeJson, yamlPlainScalarNeedsQuotes };
13420
+ export { AGENT_PRESET_PACKAGE, AGENT_PRESET_ROOT_HINT, ALIVE_LOCK_TAKEOVER_MS, AUTHORING_DESCRIPTION_BAR, AUTHORING_SPLIT_LINE_CHARS, BASELINE_ACTION, CITATION_SUPPORT_DIRS, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CONTENT_SPLIT_HINT, COST_ASCII_TOKEN_HIGH, COST_ASCII_TOKEN_LOW, COST_CJK_TOKEN_HIGH, COST_CJK_TOKEN_LOW, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEAD_LOCK_TAKEOVER_MS, DEDUP_MAX_PAIR_COMPARISONS, DEDUP_SIMILARITY_THRESHOLD, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_ARCHIVE_RETENTION_POLICY, DEFAULT_CITATION_POLICY, DEFAULT_CITATION_REF_BUDGET, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_MODEL, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MEMORY_REVIEW_MODEL, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_PROBE_TIMEOUT_MS, DEFAULT_REFERENCE_REWRITE_POLICY, DEFAULT_REVIEW_CONTEXT_MESSAGES, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_MESSAGE_CHARS, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_REVIEW_TIMEOUT_MS, DEFAULT_RUN_RECORDS, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_MODEL, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_SKILL_VERSION_KEEP, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_SUPPORT_FILE_CHAR_POLICY, DEFAULT_SUPPORT_READ_TOOL_NAMES, DEFAULT_THREAT_MAX_SCAN_CHARS, DEFAULT_USER_CHAR_LIMIT, DISPATCH_EVENT_TYPES, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, EMPTY_LOCK_TAKEOVER_MS, EMPTY_PATCH_SEED, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_ERRORS, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FAMILY_SESSION_TOOL_NAMES, FORBIDDEN_CONTROL_KEYS, FUZZY_MAX_PATTERN_CHARS, FUZZY_MAX_WORK, HEALTH_STAMP_RE, HISTORY_DIR, HISTORY_INDEX_VERSION, HOOK_LINE_RE, INSTANCE_KEYS, KEEP_LINE_RE, LOCK_BODY_RE, LOCK_SUFFIX, LOCK_TEAR_TAKEOVER_MS, LONG_RUNS, LOW_QUALITY_THRESHOLD, LostWriteLock, MAINTAIN_OUTPUT_INSTRUCTION, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_REQUEST_BODY_BYTES, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MAX_SUPPORT_READ_PATHS, MAX_TIMER_DELAY_MS, MEMORY_GUIDANCE_SECTION_ORDER, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, NAMESPACE_MIGRATIONS, NATIVE_CALL_EVENT, NATIVE_RESULT_EVENT, PARAM_ALIASES, PARAM_EXPOSURE, PARAM_GROUP_LABELS, PARAM_NAMESPACES, PATTERN_OVERLAP, PLATFORM_CATALOG_DESCRIPTION_DEFAULT, PLATFORM_ESTIMATE_CHARS_PER_TOKEN, PLATFORM_SERVICE_PROBES, POLICY_STAGE_DEFAULTS, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, PTC_DISPATCH_EVENT, PTC_DISPATCH_START_EVENT, QUALITY_WEIGHTS, READ_REQUIRED_ACTIONS, RESTRUCTURE_TARGET_RE, RETENTION_FEEDBACK_KEEP_RATIO, RETENTION_FEEDBACK_MIN_CHARS, RUNS_SCHEMA_VERSION, SKILLS_GUIDANCE, SKILLS_GUIDANCE_SECTION_ORDER, SKILL_ACTION_REQUIRED_FIELDS, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUMMARY_DUPLICATE_HINT_THRESHOLD, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, TEXT_DIFF_MAX_CHARS, TEXT_DIFF_MAX_LINES, THREAT_EXEMPTION_HINT, TOKEN_ASCII_WEIGHT, TOKEN_CJK_WEIGHT, ToolDispatchNormalizer, ToolDispatchPayloadError, UNTRUSTED_LINE_BREAK, UNTRUSTED_LINE_MARK, UPSTREAM_LIMIT_CHARS_PER_TOKEN, VERSION_SUMMARY_MAX_CHARS, absentHostServices, advanceReview, affinity, ageCopyKey, allowRowCollisions, anchorVerdict, appendEvolutionEvent, applyCuratorLifecycleFields, applyCuratorMetaFields, applyNamespaceMigration, applyReferenceRewrite, assertDispatchPayload, assertSkillsRootAliasRetired, assessStructureHealth, authoringFeedback, basePresetPlugins, blobPath, bodyCost, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpSupportRead, bumpUse, bumpView, callingScope, canonicalWriteId, claimInstance, clampOnce, clampedNumber, classifyNamespaceMigration, clearReviewChannel, collectReadSkillNames, composePresetComposition, composePresetEntry, composePresetInsert, composePresetRow, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, contentRetentionFeedback, countDispatches, createGateSet, daysSinceIso, decideTakeover, declaredReportStamp, describeReferenceRewrite, dispatchKeyOf, dispatchStepOf, displayBodyOf, duplicateHeadings, elapsedSince, emptyRecord, entryTarget, errorText, evaluateThreat, eventsFile, evidenceKindIndex, evolutionActivityProjection, evolutionEventPayloadIssue, evolutionEvidenceProjection, evolutionHome, evolutionIoAdapter, evolutionReadsProjection, evolutionRoot, evolutionSignalsProjection, filterUnreadSkillOps, findDriftSignal, foldCuratorFields, foldToolDispatches, foldTurnEvent, foldTurnEvents, foldUntrustedLines, frontmatterBlock, frontmatterCatalogInvalid, fuzzyPatch, getRecord, historyIndexFile, historyRoot, identity, idleDays, initialTurnFold, installEvolutionProjections, instanceClaimKey, instanceClaimedWriteSites, instanceHolder, isAbsent, isBookkeepingFrame, isCommittedWarning, isDeprecatedParamId, isFileShapedPath, isLoopbackRequest, isMissingPath, isPresent, isProcessAlive, isProgramToolName, isReviewChannelSession, isSkillReadToolName, isSkillToolName, isUnknown, isUnreadWrite, latestActivityAt, latestVersionAt, lifecycleCandidate, listEventArchives, loadMutations, loadRowOverrides, loadSuppressedNames, loadUsage, loadVersionContent, loadVersions, makeSerialQueue, mapProbe, markAgentCreated, markReviewChannel, markerEntryName, memoryRoot, mergePresetRow, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nearDuplicateSummaries, neutralizePromptVariables, newRunRegistry, newSkillLibrary, nextHistoryIndex, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, normalizeVersionSummary, observeEvent, orderVersions, overlongLines, paramNamespace, paramRowId, paramSettingsId, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, parseHistoryIndex, parseLockBody, partitionVersions, payloadText, pendingSelfCleanup, persistedWriteSite, persistedWriteSites, pickWithPolicy, planNamespaceMigration, planReferenceRewrite, planRehoming, policyStageLimits, presetPatchText, presetRowBlock, presetRowBody, presetRowId, probeAbsent, probeList, probeMtime, probePresent, probeReason, probeText, probeUnknown, projectionText, readDispatchSignal, readEvolutionEvents, readEvolutionTimeline, readHistoryIndex, readJsonBody, readJsonObject, readNumberParam, readParam, readingStats, recordMutation, recordVersions, redactSecrets, relatedSkillNames, releaseInstance, removePresetRow, renameWithRetry, renderCuratorReportMarkdown, renderLongRunTable, reportTime, reportsSweepLockTarget, resolveBasePresetPatch, resolveCitations, resolveExecOrigins, resolveOrigins, resolveParamId, resolveRootConfig, resolveSkillsRoot, retainEventArchives, retirementReport, reviewPrompt, runsFile, saveSuppressedNames, saveUsage, scanBodyHooks, scanContentThreats, scanMemoryThreats, scanThreats, scopedProbeReport, sessionAudited, sessionEvidenceIndex, sessionLastEventTime, sessionReadNames, sessionReadSkillNames, sessionSeesFamilyTools, sessionTurnSignals, skillReadNameOf, skillRowCells, skillStateKey, skillsRoot, supportFileReadOf, suppressedFile, sweepReports, sweepReviewChannelSessions, textDiffFacts, tokenLineFor, transactIo, transactTaskGuard, trimPatternBoundaries, unhookedSupportPointers, updateSuppressedNames, usageFile, usageObserved, userSetKeys, validateFrontmatter, validateRestructureTarget, valueOr, verifyPromptBundle, versionActionKind, versionRowCells, versionSummaryPrompt, versionTarget, vocabulary, writeDurableTmp, writeJson, yamlPlainScalarNeedsQuotes };
@@ -51,6 +51,9 @@ export * from './prompts.ts';
51
51
  export * from './quality.ts';
52
52
  export * from './redact.ts';
53
53
  export * from './review-channel.ts';
54
+ export * from './run-registry.ts';
55
+ export * from './reports.ts';
56
+ export * from './long-runs.ts';
54
57
  export * from './serial.ts';
55
58
  export * from './probe.ts';
56
59
  export * from './platform-services.ts';
@@ -0,0 +1,53 @@
1
+ /**
2
+ * 0.19.0 (S4): the family's long-run inventory — one table, one owner per entry.
3
+ *
4
+ * A "long run" is work whose duration is NOT bounded by the interaction that asked
5
+ * for it: a maintenance scan, a curator pass, a review pass. The measured fact this
6
+ * table exists to record is that a CLIENT can end a request long before the work is
7
+ * done (measured: the desktop shell ends a request at ~305 s while the same command
8
+ * finishes in 949.7 s on the web plane), so the lifetime of such work must come from
9
+ * its OWNER — never from the request.
10
+ *
11
+ * Two fields carry the judgments, and a spec pins both:
12
+ *
13
+ * - `lifetime`: `'plugin'` entries must name an owner and a result home (the work
14
+ * outlives the request and must be readable afterwards); `'request'` entries
15
+ * must say WHY their cost cannot reach the wall.
16
+ * - `measured`: only measured numbers, with their basis. An unmeasured bound is
17
+ * written as exactly that — this table is not a place to guess.
18
+ *
19
+ * The rendered table is pinned byte-for-byte into `packages/README.md` by
20
+ * `tests/long-runs.spec.ts` (the T-WD2 pattern the command registry already uses),
21
+ * so the published table cannot drift from this list.
22
+ * @module
23
+ */
24
+ /** What kind of work an entry describes. */
25
+ export type LongRunKind = 'command' | 'tool' | 'pass';
26
+ /** Whose lifetime the work borrows. `'plugin'` = it must outlive the request. */
27
+ export type LongRunLifetime = 'request' | 'plugin';
28
+ /** One entry of the long-run inventory. */
29
+ export interface LongRunEntry {
30
+ /** Command/entry id as a user sees it. */
31
+ id: string;
32
+ kind: LongRunKind;
33
+ /** The package that owns the work (its lifecycle, its budget, its result). */
34
+ owner: string;
35
+ lifetime: LongRunLifetime;
36
+ /** The measured cost, with its basis. Never a guess. */
37
+ measured: string;
38
+ /** Where the deadline/budget comes from, or why there is none. */
39
+ budget: string;
40
+ /** Where a result is readable after the work settles. */
41
+ resultHome: string;
42
+ note: string;
43
+ }
44
+ /**
45
+ * The inventory. Order is the rendered order; a spec pins it into the README.
46
+ */
47
+ export declare const LONG_RUNS: readonly LongRunEntry[];
48
+ /**
49
+ * Render the inventory as the README table rows (no header).
50
+ * @returns one markdown row per entry, in inventory order.
51
+ */
52
+ export declare function renderLongRunTable(): string;
53
+ //# sourceMappingURL=long-runs.d.ts.map
@@ -0,0 +1,61 @@
1
+ /**
2
+ * 0.19.0 (S2): the family's ONE report-retention sweep.
3
+ *
4
+ * Reports accumulate by design (every run writes one), so each kind needs a
5
+ * window or the reports home becomes an unbounded directory. The logic lived
6
+ * inside the curator only; the maintenance run reports added in 0.19.0 needed
7
+ * the same thing, and a second copy of "list, order, delete beyond the window"
8
+ * is exactly the kind of near-duplicate this family treats as a defect.
9
+ *
10
+ * Three behaviours are the curator's, kept verbatim because they are the reason
11
+ * the sweep is safe to run at all:
12
+ *
13
+ * - An UNLISTABLE directory is not "no reports" — nothing is deleted and the
14
+ * caller is told (the same three-state rule the probe files use).
15
+ * - A report with no usable timestamp is KEPT (never delete what cannot be
16
+ * ordered) and the fact is said out loud once.
17
+ * - The order is TOTAL: declared time, then mtime, then name. A tie falling
18
+ * through to readdir order evicted a different set per platform (CI-caught).
19
+ * @module
20
+ */
21
+ import type { EvolutionIoLike } from './io.ts';
22
+ /**
23
+ * The lock target a reports sweep holds for one home.
24
+ *
25
+ * `transactIo` takes the IO write lock on THIS path (minting `<path>.lock` with
26
+ * the io protocol's `pid:token` body) and holds it for the whole task; nothing
27
+ * ever writes the target, because the sweep's exclusion is the point. It lives
28
+ * inside `<home>/reports` next to the directory it protects (the sweeps' own name
29
+ * filters ignore it, and the lock file is removed on release).
30
+ *
31
+ * Moved here from evolution-curator in 0.19.0 (S2): the maintenance reports'
32
+ * sweep needs the SAME exclusion, and a second lock would protect nothing.
33
+ * @returns the lock target path.
34
+ */
35
+ export declare function reportsSweepLockTarget(): string;
36
+ /** One retention window: files whose name starts with `prefix`, newest `keep` survive. */
37
+ export interface ReportBucket {
38
+ prefix: string;
39
+ keep: number;
40
+ }
41
+ /** Inputs for {@link sweepReports}. */
42
+ export interface SweepReportsOptions {
43
+ io: EvolutionIoLike;
44
+ /** Reports directory (`<evolutionHome()>/reports`). */
45
+ dir: string;
46
+ /** Buckets. The FIRST prefix that matches classifies a file — pass specific ones first. */
47
+ buckets: readonly ReportBucket[];
48
+ /** Warn channel for an unlistable directory or an unorderable report. */
49
+ warn?: ((message: string) => void) | undefined;
50
+ /** Prefix of the warning messages, naming the owning package. */
51
+ owner: string;
52
+ }
53
+ /**
54
+ * Delete the reports beyond each bucket's window, each with its `.md` digest.
55
+ *
56
+ * Best-effort by construction: pruning never fails the run that wrote a report.
57
+ * @param options - io seam, directory, windows, warn channel and owner name.
58
+ * @returns nothing; deletions and skips are reported through `warn`.
59
+ */
60
+ export declare function sweepReports(options: SweepReportsOptions): Promise<void>;
61
+ //# sourceMappingURL=reports.d.ts.map
@@ -0,0 +1,114 @@
1
+ /**
2
+ * 0.19.0 (S2): the family's run-lifecycle owner.
3
+ *
4
+ * A "run" is work that can outlive one interaction — today the maintenance scan.
5
+ * Before this module the only record of such work was the requesting invocation:
6
+ * `lastMaintainAt` / `lastMaintainRunId` / `maintainInFlightSince` were
7
+ * module-scope globals in evolution-commands, and the plan text existed ONLY in
8
+ * the command's return string. An interaction that ended (measured: the desktop
9
+ * shell hangs up at ~305 s) therefore erased every trace of a still-running scan.
10
+ *
11
+ * This registry is the single owner of (a) what is in flight, (b) how a run
12
+ * ended, and (c) WHERE its result lives (`resultRef` is a path, never the result
13
+ * itself — the report file is the one result home).
14
+ *
15
+ * Two disciplines are structural, not stylistic:
16
+ *
17
+ * - **No timer.** A run whose process died is converged at READ time: the index
18
+ * file can only carry `running` records written by a PREVIOUS process life,
19
+ * because the live ones are in memory. `load()` therefore rewrites every
20
+ * `running` record as `failed(orphan)` once — the same "converge when read"
21
+ * posture the approval window uses for its TTL.
22
+ * - **One terminal state.** `settle()` refuses to overwrite a record that is
23
+ * already terminal, so a cancel that raced a failure cannot flip the answer
24
+ * the operator already read.
25
+ *
26
+ * The store is PER INSTANCE (a factory, not module scope): one registry per
27
+ * mounted row, disposed with its fiber. There is no module-scope mutable state
28
+ * here to register under N12.
29
+ * @module
30
+ */
31
+ import type { EvolutionIoLike } from './io.ts';
32
+ /** The kinds of long-running work the registry tracks. Extend as rows gain runs. */
33
+ export type RunKind = 'maintain';
34
+ /** The four terminal-or-live states of a run. */
35
+ export type RunState = 'running' | 'succeeded' | 'failed' | 'cancelled';
36
+ /** One tracked run. `resultRef` is a path to the run's report, never its content. */
37
+ export interface RunRecord {
38
+ id: string;
39
+ kind: RunKind;
40
+ state: RunState;
41
+ startedAt: number;
42
+ /** The process that owns a RUNNING record. Required to tell an in-flight run of
43
+ * another plane (both planes share one DSH_HOME) from the orphan of a dead one;
44
+ * absent means the owner cannot be named, which is treated as gone. */
45
+ pid?: number | undefined;
46
+ endedAt?: number | undefined;
47
+ /** Absolute path of the run's report, written by the run owner at settle. */
48
+ resultRef?: string | undefined;
49
+ /** Why the run did not succeed (an abort reason, a validation failure). */
50
+ failure?: string | undefined;
51
+ }
52
+ /** What a started run gets: its id and the signal its work must honour. */
53
+ export interface RunHandle {
54
+ readonly id: string;
55
+ readonly kind: RunKind;
56
+ readonly signal: AbortSignal;
57
+ /** When the run was registered — the same instant the record carries. */
58
+ readonly startedAt: number;
59
+ }
60
+ /** How a run ended, as reported by its owner. */
61
+ export interface RunOutcome {
62
+ state: 'succeeded' | 'failed' | 'cancelled';
63
+ resultRef?: string | undefined;
64
+ failure?: string | undefined;
65
+ }
66
+ /** The registry surface the commands row drives. */
67
+ export interface RunRegistry {
68
+ /** Converge the persisted index once at mount; see the module doc. */
69
+ load(): Promise<{
70
+ ok: boolean;
71
+ note?: string;
72
+ }>;
73
+ /** Register a new run, arm its cancellation, and return its handle. */
74
+ begin(kind: RunKind): RunHandle;
75
+ /** Record a run's terminal state (first writer wins). */
76
+ settle(id: string, outcome: RunOutcome): Promise<void>;
77
+ /** The live run, when there is one (one kind today — see the implementation note). */
78
+ inFlight(): RunRecord | undefined;
79
+ /** When the newest terminal run ended (`undefined` when none). */
80
+ lastSettledAt(): number | undefined;
81
+ /** Every tracked run, newest first. */
82
+ runs(): readonly RunRecord[];
83
+ /** One run by id, converged view included. */
84
+ find(id: string): RunRecord | undefined;
85
+ /** Ask a running run to stop; it settles as `cancelled`. @returns whether it was running. */
86
+ cancel(id: string): boolean;
87
+ /** Ask every running run to stop (fiber dispose). @returns how many were running. */
88
+ cancelAll(): number;
89
+ }
90
+ /** Options for {@link newRunRegistry}. */
91
+ export interface RunRegistryOptions {
92
+ io: EvolutionIoLike;
93
+ /** Evolution home (`evolutionHome()`); the index lives at `<home>/runs.json`. */
94
+ home: string;
95
+ /** Warn channel for a failed index write (the run itself is unaffected). */
96
+ warn?: (message: string) => void;
97
+ /** Clock seam for tests. */
98
+ now?: () => number;
99
+ /** Retained records (N3-clamped). */
100
+ maxRecords?: number;
101
+ }
102
+ /** Current index schema. Bump only for a structural change. */
103
+ export declare const RUNS_SCHEMA_VERSION = 1;
104
+ /** Retained run records, newest first. */
105
+ export declare const DEFAULT_RUN_RECORDS = 50;
106
+ /** Index path for a home. */
107
+ export declare function runsFile(home: string): string;
108
+ /**
109
+ * Create the run registry for one mounted row.
110
+ * @param options - io seam, evolution home, warn channel, clock and record cap.
111
+ * @returns the registry; call {@link RunRegistry.load} once before answering reads.
112
+ */
113
+ export declare function newRunRegistry(options: RunRegistryOptions): RunRegistry;
114
+ //# sourceMappingURL=run-registry.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-core",
3
3
  "description": "Shared stores, prompts, signals and lifecycle logic for the dsh-evolution plugin family (community build)",
4
- "version": "0.18.1",
4
+ "version": "0.19.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -62,6 +62,24 @@
62
62
  "state": [],
63
63
  "note": "Curator run reports + their retention sweep. The sweep is a multi-step list + delete-beyond-the-window over a DIRECTORY, so it has no per-file lock: v43 FLOW2-1 put it inside the IO backend's cross-process write lock (transactIo) on a per-home lock target, <evolutionHome>/reports/.retention (lock file <target>.lock, pid:token body). The former declaration here was instance-claim, which is in-process ONLY (instance-scope.ts is a module-scope Map). Two curator ROWS in one process still cannot sweep at once (the loser yields on that claim), and report writes are name-unique per runId. Trigger for the remaining gap: a second PROCESS sharing DSH_HOME now WAITS on the lock instead of sweeping concurrently, and on a backend without transact there is no lock at all, so the sweep degrades to best-effort idempotent deletes."
64
64
  },
65
+ {
66
+ "id": "runs-index",
67
+ "path": "<evolutionHome>/runs.json",
68
+ "writer": "evolution-core/src/run-registry.ts",
69
+ "serializedBy": "transact",
70
+ "marker": "transactIo(options.io, path",
71
+ "state": [],
72
+ "note": "The run registry's index (0.19.0/S2): what is in flight, how each run ended, where its report lives. Read-modify-write under the IO backend's cross-process lock, and it MERGES instead of overwriting — the desktop and the web plane share one DSH_HOME, so two hosts each know their own runs and this file is their shared view. resultRef only POINTS at the report; results never live here."
73
+ },
74
+ {
75
+ "id": "maintain-reports",
76
+ "path": "<evolutionHome>/reports/maintain-*.json",
77
+ "writer": "evolution-commands/src/index.ts",
78
+ "serializedBy": "transact",
79
+ "marker": "transactIo(io, reportsSweepLockTarget()",
80
+ "state": [],
81
+ "note": "One report per maintenance run (0.19.0/S2), written once at settle by that run's owner under a name unique to the run, so the write itself needs no per-file lock. The retention sweep is multi-step (list the directory, then delete beyond the window) and takes the SAME per-home <evolutionHome>/reports/.retention lock the curator's sweep holds — one lock for one directory. On a backend without transact it degrades to idempotent best-effort deletes."
82
+ },
65
83
  {
66
84
  "id": "skill-tree",
67
85
  "path": "<skillsRoot>/**",