@lmzhen/dsh-evolution-state-storage 0.3.66 → 0.3.68

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
@@ -95,6 +95,7 @@ function recordIssue(table, record) {
95
95
  if (typeof value.kind !== "string" || !PENDING_KINDS.has(value.kind)) return "kind must be memory|skill|capability";
96
96
  if (typeof value.summary !== "string") return "summary must be a string";
97
97
  if (!Object.prototype.hasOwnProperty.call(value, "args")) return "args key is required (may be any cloneable value)";
98
+ if (value.args === void 0) return "args must be a cloneable value — `undefined` is dropped by the json medium and the record would be unreadable after a restart (pass {} instead)";
98
99
  if (typeof value.createdAt !== "string") return "createdAt must be a string";
99
100
  if (typeof value.status !== "string" || !PENDING_STATUSES.has(value.status)) return "status must be pending|executing|approved|rejected";
100
101
  for (const field of [
@@ -157,6 +158,64 @@ const releasedStatus = (status) => status === "executing" ? "pending" : status;
157
158
  * The audit ARCHIVE sidecar that json maintains beyond the cap stays
158
159
  * json-specific (domain has no sidecar facility) — declared in both READMEs. */
159
160
  const PENDING_RESOLVED_CAP = 200;
161
+ /**
162
+ * V24-08 (v24): session rows in the review-state table, per session id. The
163
+ * review pipeline saves on EVERY turn/end of EVERY session and nothing ever
164
+ * deleted rows, so the file grew (and was fully rewritten) with the deploy's
165
+ * whole session history — the same unbounded-growth class the pending cap
166
+ * above already fixed for approvals. A review row is advisory cadence state:
167
+ * evicting the least-recently-active session merely lets that session's next
168
+ * review fire from a fresh counter, so a generous cap is loss-less in
169
+ * practice. Enforced by BOTH providers inside their save path (no seam
170
+ * interface change, no background sweeper).
171
+ */
172
+ const REVIEW_STATE_SESSION_CAP = 500;
173
+ /**
174
+ * V27 G2.2: WHICH pending records the audit cap evicts, as one pure rule both
175
+ * providers apply (the two implementations had drifted into separate files and
176
+ * only one of them was ever updated by a later fix).
177
+ *
178
+ * Eligible = resolved (`approved`/`rejected`) and NOT a capability approval:
179
+ * v23 (AP-1) exempts those because the Creator-mode contract reads the LIVE
180
+ * approved list and a capability cannot be re-submitted for the same package,
181
+ * so eviction would make an approved capability permanently unactivatable.
182
+ * Ordering = oldest `resolvedAt` first; a missing or unparseable timestamp sorts
183
+ * LAST (json parity, v16) — an unknown time must never make a record the victim,
184
+ * and pending/executing rows are live work that is never trimmed.
185
+ *
186
+ * @param records - every pending record currently in the live table.
187
+ * @param cap - how many resolved records may be kept.
188
+ * @returns the records to evict, oldest first (empty when within the cap).
189
+ */
190
+ function selectPendingOverflow(records, cap = 200) {
191
+ const resolved = records.filter((record) => (record.status === "approved" || record.status === "rejected") && record.kind !== "capability");
192
+ const overflow = resolved.length - cap;
193
+ if (overflow <= 0) return [];
194
+ const resolvedAtMs = (record) => {
195
+ if (!record.resolvedAt) return Number.MAX_SAFE_INTEGER;
196
+ const parsed = Date.parse(record.resolvedAt);
197
+ return Number.isNaN(parsed) ? Number.MAX_SAFE_INTEGER : parsed;
198
+ };
199
+ return [...resolved].sort((a, b) => resolvedAtMs(a) - resolvedAtMs(b)).slice(0, overflow);
200
+ }
201
+ /**
202
+ * V27 G2.2: which review-state session rows a save evicts, as one pure rule
203
+ * both providers apply. The saving session is never a candidate (it is the most
204
+ * recent write by definition); the rest are ordered by their provider stamp
205
+ * ascending, with a missing stamp read as 0 = oldest, because an active session
206
+ * re-stamps its row on its next save and an unknown stamp is stale by
207
+ * construction. Exactly one row is dropped per over-cap save, which keeps the
208
+ * table at the cap in steady state.
209
+ *
210
+ * @param rows - the other sessions' rows, with their stamps.
211
+ * @param cap - how many session rows may exist.
212
+ * @returns the keys to delete, oldest first.
213
+ */
214
+ function selectSessionOverflow(rows, options, cap = 500) {
215
+ const overflow = rows.length - cap + 1;
216
+ if (overflow <= 0) return [];
217
+ return [...rows].sort((a, b) => options.stampOf(a) - options.stampOf(b)).slice(0, overflow).map((row) => options.keyOf(row));
218
+ }
160
219
  var EvolutionStateStorageRegistry = class extends Service {
161
220
  providers = /* @__PURE__ */ new Map();
162
221
  /** C-7 (v18): per-name dispose, mirroring the evolution-io registry. */
@@ -203,4 +262,4 @@ var EvolutionStateStorageRegistry = class extends Service {
203
262
  }
204
263
  };
205
264
  //#endregion
206
- export { CURATOR_STATE_FILE, CURATOR_STATE_KEY, CURATOR_STATE_TABLE, EvolutionStateStorageRegistry, EvolutionStateStorageRegistry as default, PENDING_ARCHIVE_BAK_FILE, PENDING_ARCHIVE_FILE, PENDING_LEGACY_FILE, PENDING_RESOLVED_CAP, PENDING_STATE_FILE, PENDING_TABLE, PROVIDER_DOMAIN, PROVIDER_JSON, REVIEW_STATE_FILE, REVIEW_STATE_TABLE, UNKNOWN_FIELD_POLICY, assertCloneable, canClaimPending, canResolvePending, cloneRecord, recordIssue, releasedStatus };
265
+ export { CURATOR_STATE_FILE, CURATOR_STATE_KEY, CURATOR_STATE_TABLE, EvolutionStateStorageRegistry, EvolutionStateStorageRegistry as default, PENDING_ARCHIVE_BAK_FILE, PENDING_ARCHIVE_FILE, PENDING_LEGACY_FILE, PENDING_RESOLVED_CAP, PENDING_STATE_FILE, PENDING_TABLE, PROVIDER_DOMAIN, PROVIDER_JSON, REVIEW_STATE_FILE, REVIEW_STATE_SESSION_CAP, REVIEW_STATE_TABLE, UNKNOWN_FIELD_POLICY, assertCloneable, canClaimPending, canResolvePending, cloneRecord, recordIssue, releasedStatus, selectPendingOverflow, selectSessionOverflow };
@@ -48,6 +48,53 @@ export declare const releasedStatus: (status: PendingStatus) => PendingStatus;
48
48
  * The audit ARCHIVE sidecar that json maintains beyond the cap stays
49
49
  * json-specific (domain has no sidecar facility) — declared in both READMEs. */
50
50
  export declare const PENDING_RESOLVED_CAP = 200;
51
+ /**
52
+ * V24-08 (v24): session rows in the review-state table, per session id. The
53
+ * review pipeline saves on EVERY turn/end of EVERY session and nothing ever
54
+ * deleted rows, so the file grew (and was fully rewritten) with the deploy's
55
+ * whole session history — the same unbounded-growth class the pending cap
56
+ * above already fixed for approvals. A review row is advisory cadence state:
57
+ * evicting the least-recently-active session merely lets that session's next
58
+ * review fire from a fresh counter, so a generous cap is loss-less in
59
+ * practice. Enforced by BOTH providers inside their save path (no seam
60
+ * interface change, no background sweeper).
61
+ */
62
+ export declare const REVIEW_STATE_SESSION_CAP = 500;
63
+ /**
64
+ * V27 G2.2: WHICH pending records the audit cap evicts, as one pure rule both
65
+ * providers apply (the two implementations had drifted into separate files and
66
+ * only one of them was ever updated by a later fix).
67
+ *
68
+ * Eligible = resolved (`approved`/`rejected`) and NOT a capability approval:
69
+ * v23 (AP-1) exempts those because the Creator-mode contract reads the LIVE
70
+ * approved list and a capability cannot be re-submitted for the same package,
71
+ * so eviction would make an approved capability permanently unactivatable.
72
+ * Ordering = oldest `resolvedAt` first; a missing or unparseable timestamp sorts
73
+ * LAST (json parity, v16) — an unknown time must never make a record the victim,
74
+ * and pending/executing rows are live work that is never trimmed.
75
+ *
76
+ * @param records - every pending record currently in the live table.
77
+ * @param cap - how many resolved records may be kept.
78
+ * @returns the records to evict, oldest first (empty when within the cap).
79
+ */
80
+ export declare function selectPendingOverflow(records: readonly PendingRecord[], cap?: number): PendingRecord[];
81
+ /**
82
+ * V27 G2.2: which review-state session rows a save evicts, as one pure rule
83
+ * both providers apply. The saving session is never a candidate (it is the most
84
+ * recent write by definition); the rest are ordered by their provider stamp
85
+ * ascending, with a missing stamp read as 0 = oldest, because an active session
86
+ * re-stamps its row on its next save and an unknown stamp is stale by
87
+ * construction. Exactly one row is dropped per over-cap save, which keeps the
88
+ * table at the cap in steady state.
89
+ *
90
+ * @param rows - the other sessions' rows, with their stamps.
91
+ * @param cap - how many session rows may exist.
92
+ * @returns the keys to delete, oldest first.
93
+ */
94
+ export declare function selectSessionOverflow<T>(rows: readonly T[], options: {
95
+ keyOf(row: T): string;
96
+ stampOf(row: T): number;
97
+ }, cap?: number): string[];
51
98
  /**
52
99
  * Claim lifecycle (S3.3): pending →(claim)→ executing →(resolve)→ approved/rejected.
53
100
  * release() rolls executing back to pending (failure path). A crash between
@@ -101,6 +148,13 @@ export interface EvolutionStateStorage {
101
148
  * whole read → transform → write runs inside one provider transact, so a
102
149
  * setPaused racing the run-core bookkeeping write can never interleave a
103
150
  * stale load with a newer save.
151
+ *
152
+ * V27 S4: the record handed to `task` belongs to the task — a provider must
153
+ * NEVER pass the object it stores (both current providers hand out a copy:
154
+ * json re-parses its medium, the domain clones its record). Mutating the
155
+ * argument is not a supported way to write: a task that does so and then lets
156
+ * validation refuse the result would otherwise leave a mutated object in the
157
+ * provider's in-memory store.
104
158
  */
105
159
  transactCuratorState(task: (current: CuratorStateRecord | null) => CuratorStateRecord | null): Promise<void>;
106
160
  listPending(status?: PendingStatus): Promise<PendingRecord[]>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-state-storage",
3
3
  "description": "Provider registry seam for durable evolution state (community build)",
4
- "version": "0.3.66",
4
+ "version": "0.3.68",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },