@lmzhen/dsh-evolution-core 0.19.2 → 0.19.3
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 +105 -79
- package/lib/types/run-registry.d.ts +23 -14
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -5927,7 +5927,7 @@ function sweepReviewChannelSessions(isAlive) {
|
|
|
5927
5927
|
//#endregion
|
|
5928
5928
|
//#region lib/types/run-registry.js
|
|
5929
5929
|
/**
|
|
5930
|
-
* 0.19.0 (S2): the family's run-lifecycle owner.
|
|
5930
|
+
* 0.19.0 (S2) / 0.19.3 (restructure): the family's run-lifecycle owner.
|
|
5931
5931
|
*
|
|
5932
5932
|
* A "run" is work that can outlive one interaction — today the maintenance scan.
|
|
5933
5933
|
* Before this module the only record of such work was the requesting invocation:
|
|
@@ -5940,16 +5940,25 @@ function sweepReviewChannelSessions(isAlive) {
|
|
|
5940
5940
|
* ended, and (c) WHERE its result lives (`resultRef` is a path, never the result
|
|
5941
5941
|
* itself — the report file is the one result home).
|
|
5942
5942
|
*
|
|
5943
|
-
*
|
|
5944
|
-
*
|
|
5945
|
-
* - **
|
|
5946
|
-
*
|
|
5947
|
-
*
|
|
5948
|
-
*
|
|
5949
|
-
*
|
|
5950
|
-
*
|
|
5951
|
-
*
|
|
5952
|
-
*
|
|
5943
|
+
* Three disciplines are structural, not stylistic:
|
|
5944
|
+
*
|
|
5945
|
+
* - **One writer per row** (0.19.3). A run's row is authoritative in the memory of
|
|
5946
|
+
* the process that STARTED it (`owned`) and in the shared index for everyone else
|
|
5947
|
+
* (`foreign`, rebuilt from the file on every `load()`). The only write a non-owner
|
|
5948
|
+
* may make is the orphan verdict for a row whose owner is gone, judged inside the
|
|
5949
|
+
* very transaction that read the row. The two cross-plane bugs this family shipped
|
|
5950
|
+
* were projections of that one missing rule: 0.19.0 froze a foreign run as
|
|
5951
|
+
* `running` forever (a "known id" was never re-read), and 0.19.1 wrote a settled
|
|
5952
|
+
* foreign row back as `running` (memory overwrote every row it knew).
|
|
5953
|
+
* - **No timer.** A run whose process died is converged at READ time: the live rows
|
|
5954
|
+
* are in memory, so a `running` row in the file whose owner is not alive is an
|
|
5955
|
+
* orphan — the same "converge when read" posture the approval window uses.
|
|
5956
|
+
* - **One terminal state, and it is monotone.** `settle()` refuses to overwrite a row
|
|
5957
|
+
* that is already terminal (so a cancel that raced a failure cannot flip the answer
|
|
5958
|
+
* the operator already read), and a row that reached a terminal state never reads as
|
|
5959
|
+
* `running` again: only the owner that started it could make it running and
|
|
5960
|
+
* `settle()` is terminal-once, so a `running` claim for a settled row can only be a
|
|
5961
|
+
* stale writer's — refused on read, repaired on the next write.
|
|
5953
5962
|
*
|
|
5954
5963
|
* The store is PER INSTANCE (a factory, not module scope): one registry per
|
|
5955
5964
|
* mounted row, disposed with its fiber. There is no module-scope mutable state
|
|
@@ -6046,43 +6055,69 @@ function newRunRegistry(options) {
|
|
|
6046
6055
|
const maxRecords = clampedNumber(options.maxRecords ?? 50, 50, { min: 1 });
|
|
6047
6056
|
const warn = options.warn ?? (() => {});
|
|
6048
6057
|
const path = runsFile(options.home);
|
|
6049
|
-
|
|
6058
|
+
/** Rows THIS process started: its memory is the only truth for them. */
|
|
6059
|
+
const owned = /* @__PURE__ */ new Map();
|
|
6060
|
+
/** Rows read from the shared index: the file is the only truth for them. */
|
|
6061
|
+
const foreign = /* @__PURE__ */ new Map();
|
|
6062
|
+
/** Cancellation handles — "can this still be stopped", NOT ownership (`settle` drops one). */
|
|
6050
6063
|
const controllers = /* @__PURE__ */ new Map();
|
|
6051
|
-
|
|
6052
|
-
/** The ONE wording of the orphan verdict — read-time convergence says it in two places. */
|
|
6064
|
+
/** The ONE wording of the orphan verdict. */
|
|
6053
6065
|
const ORPHAN_FAILURE = "no live owner process — the run was interrupted before it settled (orphan)";
|
|
6054
|
-
/** A
|
|
6055
|
-
const
|
|
6056
|
-
|
|
6066
|
+
/** A row whose owner cannot be reached: `pid` absent (an older build) or not alive. */
|
|
6067
|
+
const ownerIsGone = (row) => row.pid === void 0 || !isProcessAlive(row.pid);
|
|
6068
|
+
/** The verdict is materialized ONCE, so its `endedAt` (and the duration a reader sees) is stable. */
|
|
6069
|
+
const orphanOf = (row) => ({
|
|
6070
|
+
...row,
|
|
6057
6071
|
state: "failed",
|
|
6058
6072
|
endedAt: now(),
|
|
6059
6073
|
failure: ORPHAN_FAILURE
|
|
6060
6074
|
});
|
|
6061
|
-
/**
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6075
|
+
/** Every row this process knows, newest first. ALL reads go through here. */
|
|
6076
|
+
const view = () => [...owned.values(), ...foreign.values()].sort((a, b) => b.startedAt - a.startedAt);
|
|
6077
|
+
/** Retained history: every running row plus the newest terminal ones (one cap, both planes). */
|
|
6078
|
+
const capped = (rows) => {
|
|
6079
|
+
const live = rows.filter((row) => row.state === "running");
|
|
6080
|
+
const done = rows.filter((row) => TERMINAL.has(row.state)).sort((a, b) => (b.endedAt ?? b.startedAt) - (a.endedAt ?? a.startedAt)).slice(0, maxRecords);
|
|
6081
|
+
return [...live, ...done].sort((a, b) => b.startedAt - a.startedAt);
|
|
6082
|
+
};
|
|
6083
|
+
/**
|
|
6084
|
+
* Update a surviving foreign row in place (identity stable), or add it.
|
|
6085
|
+
*
|
|
6086
|
+
* Monotone states (0.19.3): a row already known TERMINAL is never shown as `running`
|
|
6087
|
+
* again. Only the owner that started a run could make it running, and `settle()` is
|
|
6088
|
+
* terminal-once — so a `running` claim for a settled row can only come from a stale
|
|
6089
|
+
* writer (an older plane holding an outdated view), and adopting it would propagate the
|
|
6090
|
+
* resurrection to every other plane.
|
|
6091
|
+
* @returns true when the row was refused as exactly that regression.
|
|
6092
|
+
*/
|
|
6093
|
+
const adopt = (row) => {
|
|
6094
|
+
const existing = foreign.get(row.id);
|
|
6095
|
+
if (existing === void 0) {
|
|
6096
|
+
foreign.set(row.id, row);
|
|
6097
|
+
return false;
|
|
6098
|
+
}
|
|
6099
|
+
if (TERMINAL.has(existing.state) && !TERMINAL.has(row.state)) return true;
|
|
6100
|
+
delete existing.endedAt;
|
|
6101
|
+
delete existing.resultRef;
|
|
6102
|
+
delete existing.failure;
|
|
6103
|
+
Object.assign(existing, row);
|
|
6104
|
+
return false;
|
|
6069
6105
|
};
|
|
6070
6106
|
const persist = async () => {
|
|
6071
|
-
trim();
|
|
6072
6107
|
try {
|
|
6073
6108
|
await transactIo(options.io, path, (current) => {
|
|
6074
6109
|
const onDisk = parseIndex(current);
|
|
6075
6110
|
const merged = /* @__PURE__ */ new Map();
|
|
6076
|
-
for (const
|
|
6077
|
-
for (const
|
|
6078
|
-
|
|
6079
|
-
const
|
|
6080
|
-
if (
|
|
6081
|
-
merged.set(record.id, record);
|
|
6111
|
+
for (const row of onDisk.records) merged.set(row.id, row);
|
|
6112
|
+
for (const [id, row] of owned) merged.set(id, row);
|
|
6113
|
+
for (const [id, row] of foreign) {
|
|
6114
|
+
const onDiskRow = merged.get(id);
|
|
6115
|
+
if (TERMINAL.has(row.state) && onDiskRow !== void 0 && !TERMINAL.has(onDiskRow.state)) merged.set(id, row);
|
|
6082
6116
|
}
|
|
6083
|
-
const
|
|
6084
|
-
const
|
|
6085
|
-
const
|
|
6117
|
+
for (const [id, row] of merged) if (row.state === "running" && !owned.has(id) && ownerIsGone(row)) merged.set(id, orphanOf(row));
|
|
6118
|
+
const kept = capped([...merged.values()]);
|
|
6119
|
+
const live = kept.filter((row) => row.state === "running");
|
|
6120
|
+
const done = kept.filter((row) => TERMINAL.has(row.state));
|
|
6086
6121
|
return JSON.stringify({
|
|
6087
6122
|
schemaVersion: 1,
|
|
6088
6123
|
runs: [
|
|
@@ -6112,45 +6147,36 @@ function newRunRegistry(options) {
|
|
|
6112
6147
|
ok: false,
|
|
6113
6148
|
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)"
|
|
6114
6149
|
};
|
|
6150
|
+
const wasRunning = new Set([...foreign.values()].filter((row) => row.state === "running").map((row) => row.id));
|
|
6151
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6115
6152
|
let converged = 0;
|
|
6116
6153
|
let liveElsewhere = 0;
|
|
6117
|
-
let
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
if (
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
delete known.resultRef;
|
|
6126
|
-
delete known.failure;
|
|
6127
|
-
Object.assign(known, record);
|
|
6128
|
-
healed++;
|
|
6129
|
-
} else if (record.pid === void 0 || !isProcessAlive(record.pid)) {
|
|
6130
|
-
records[knownIndex] = orphanOf(record);
|
|
6131
|
-
converged++;
|
|
6132
|
-
}
|
|
6133
|
-
}
|
|
6134
|
-
continue;
|
|
6135
|
-
}
|
|
6136
|
-
if (record.state !== "running") {
|
|
6137
|
-
records.push(record);
|
|
6154
|
+
let followed = 0;
|
|
6155
|
+
let regressed = 0;
|
|
6156
|
+
for (const row of parsed.records) {
|
|
6157
|
+
if (owned.has(row.id)) continue;
|
|
6158
|
+
seen.add(row.id);
|
|
6159
|
+
if (row.state !== "running") {
|
|
6160
|
+
if (wasRunning.has(row.id)) followed++;
|
|
6161
|
+
if (adopt(row)) regressed++;
|
|
6138
6162
|
continue;
|
|
6139
6163
|
}
|
|
6140
|
-
if (
|
|
6141
|
-
|
|
6142
|
-
|
|
6164
|
+
if (ownerIsGone(row)) {
|
|
6165
|
+
converged++;
|
|
6166
|
+
adopt(orphanOf(row));
|
|
6143
6167
|
continue;
|
|
6144
6168
|
}
|
|
6145
|
-
|
|
6146
|
-
|
|
6169
|
+
liveElsewhere++;
|
|
6170
|
+
if (adopt(row)) regressed++;
|
|
6147
6171
|
}
|
|
6148
|
-
|
|
6172
|
+
for (const id of [...foreign.keys()]) if (!seen.has(id)) foreign.delete(id);
|
|
6173
|
+
if (converged > 0 || regressed > 0) await persist();
|
|
6149
6174
|
const notes = [
|
|
6150
6175
|
parsed.note,
|
|
6151
6176
|
converged > 0 ? `${converged} run(s) with no live owner were recorded as failed(orphan)` : void 0,
|
|
6152
6177
|
liveElsewhere > 0 ? `${liveElsewhere} run(s) are in flight in another process sharing this home` : void 0,
|
|
6153
|
-
|
|
6178
|
+
followed > 0 ? `${followed} run(s) settled by their owner were re-read from the index` : void 0,
|
|
6179
|
+
regressed > 0 ? `${regressed} settled run(s) stayed settled although the index claimed they were running` : void 0
|
|
6154
6180
|
].filter((note) => note !== void 0);
|
|
6155
6181
|
return {
|
|
6156
6182
|
ok: true,
|
|
@@ -6162,7 +6188,7 @@ function newRunRegistry(options) {
|
|
|
6162
6188
|
const startedAt = now();
|
|
6163
6189
|
const controller = new AbortController();
|
|
6164
6190
|
controllers.set(id, controller);
|
|
6165
|
-
|
|
6191
|
+
owned.set(id, {
|
|
6166
6192
|
id,
|
|
6167
6193
|
kind,
|
|
6168
6194
|
state: "running",
|
|
@@ -6178,37 +6204,37 @@ function newRunRegistry(options) {
|
|
|
6178
6204
|
};
|
|
6179
6205
|
},
|
|
6180
6206
|
async settle(id, outcome) {
|
|
6181
|
-
const
|
|
6182
|
-
if (
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
if (outcome.resultRef !== void 0)
|
|
6186
|
-
if (outcome.failure !== void 0)
|
|
6207
|
+
const row = owned.get(id);
|
|
6208
|
+
if (row === void 0 || TERMINAL.has(row.state)) return;
|
|
6209
|
+
row.state = outcome.state;
|
|
6210
|
+
row.endedAt = now();
|
|
6211
|
+
if (outcome.resultRef !== void 0) row.resultRef = outcome.resultRef;
|
|
6212
|
+
if (outcome.failure !== void 0) row.failure = outcome.failure;
|
|
6187
6213
|
controllers.delete(id);
|
|
6188
6214
|
await persist();
|
|
6189
6215
|
},
|
|
6190
6216
|
inFlight() {
|
|
6191
|
-
return
|
|
6217
|
+
return view().find((row) => row.state === "running");
|
|
6192
6218
|
},
|
|
6193
6219
|
lastSettledAt() {
|
|
6194
|
-
return
|
|
6220
|
+
return view().find((row) => TERMINAL.has(row.state))?.endedAt;
|
|
6195
6221
|
},
|
|
6196
6222
|
runs() {
|
|
6197
|
-
return
|
|
6223
|
+
return capped(view());
|
|
6198
6224
|
},
|
|
6199
6225
|
find(id) {
|
|
6200
|
-
return
|
|
6226
|
+
return owned.get(id) ?? foreign.get(id);
|
|
6201
6227
|
},
|
|
6202
6228
|
cancel(id) {
|
|
6203
6229
|
const controller = controllers.get(id);
|
|
6204
6230
|
if (controller === void 0) return false;
|
|
6205
|
-
const
|
|
6206
|
-
if (
|
|
6231
|
+
const row = owned.get(id);
|
|
6232
|
+
if (row === void 0 || row.state !== "running") return false;
|
|
6207
6233
|
controller.abort();
|
|
6208
6234
|
controllers.delete(id);
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6235
|
+
row.state = "cancelled";
|
|
6236
|
+
row.endedAt = now();
|
|
6237
|
+
row.failure = "cancelled by operator";
|
|
6212
6238
|
persist();
|
|
6213
6239
|
return true;
|
|
6214
6240
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 0.19.0 (S2): the family's run-lifecycle owner.
|
|
2
|
+
* 0.19.0 (S2) / 0.19.3 (restructure): the family's run-lifecycle owner.
|
|
3
3
|
*
|
|
4
4
|
* A "run" is work that can outlive one interaction — today the maintenance scan.
|
|
5
5
|
* Before this module the only record of such work was the requesting invocation:
|
|
@@ -12,16 +12,25 @@
|
|
|
12
12
|
* ended, and (c) WHERE its result lives (`resultRef` is a path, never the result
|
|
13
13
|
* itself — the report file is the one result home).
|
|
14
14
|
*
|
|
15
|
-
*
|
|
15
|
+
* Three disciplines are structural, not stylistic:
|
|
16
16
|
*
|
|
17
|
-
* - **
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
17
|
+
* - **One writer per row** (0.19.3). A run's row is authoritative in the memory of
|
|
18
|
+
* the process that STARTED it (`owned`) and in the shared index for everyone else
|
|
19
|
+
* (`foreign`, rebuilt from the file on every `load()`). The only write a non-owner
|
|
20
|
+
* may make is the orphan verdict for a row whose owner is gone, judged inside the
|
|
21
|
+
* very transaction that read the row. The two cross-plane bugs this family shipped
|
|
22
|
+
* were projections of that one missing rule: 0.19.0 froze a foreign run as
|
|
23
|
+
* `running` forever (a "known id" was never re-read), and 0.19.1 wrote a settled
|
|
24
|
+
* foreign row back as `running` (memory overwrote every row it knew).
|
|
25
|
+
* - **No timer.** A run whose process died is converged at READ time: the live rows
|
|
26
|
+
* are in memory, so a `running` row in the file whose owner is not alive is an
|
|
27
|
+
* orphan — the same "converge when read" posture the approval window uses.
|
|
28
|
+
* - **One terminal state, and it is monotone.** `settle()` refuses to overwrite a row
|
|
29
|
+
* that is already terminal (so a cancel that raced a failure cannot flip the answer
|
|
30
|
+
* the operator already read), and a row that reached a terminal state never reads as
|
|
31
|
+
* `running` again: only the owner that started it could make it running and
|
|
32
|
+
* `settle()` is terminal-once, so a `running` claim for a settled row can only be a
|
|
33
|
+
* stale writer's — refused on read, repaired on the next write.
|
|
25
34
|
*
|
|
26
35
|
* The store is PER INSTANCE (a factory, not module scope): one registry per
|
|
27
36
|
* mounted row, disposed with its fiber. There is no module-scope mutable state
|
|
@@ -66,9 +75,9 @@ export interface RunOutcome {
|
|
|
66
75
|
/** The registry surface the commands row drives. */
|
|
67
76
|
export interface RunRegistry {
|
|
68
77
|
/**
|
|
69
|
-
* Converge the persisted index; the commands row calls it before answering any read
|
|
70
|
-
* (0.19.
|
|
71
|
-
*
|
|
78
|
+
* Converge the persisted index; the commands row calls it before answering any read.
|
|
79
|
+
* The foreign view is rebuilt wholesale (0.19.3), so a row another plane settled is
|
|
80
|
+
* re-read here instead of being frozen at whatever this process first saw.
|
|
72
81
|
*/
|
|
73
82
|
load(): Promise<{
|
|
74
83
|
ok: boolean;
|
|
@@ -88,7 +97,7 @@ export interface RunRegistry {
|
|
|
88
97
|
inFlight(): RunRecord | undefined;
|
|
89
98
|
/** When the newest terminal run ended (`undefined` when none). */
|
|
90
99
|
lastSettledAt(): number | undefined;
|
|
91
|
-
/** Every tracked run, newest first. */
|
|
100
|
+
/** Every tracked run, newest first (retained history: cap applied). */
|
|
92
101
|
runs(): readonly RunRecord[];
|
|
93
102
|
/** One run by id, converged view included. */
|
|
94
103
|
find(id: string): RunRecord | undefined;
|
package/package.json
CHANGED