@lmzhen/dsh-evolution-core 0.19.1 → 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 +111 -78
- 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,29 +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
|
-
|
|
6053
|
-
|
|
6054
|
-
const
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
|
|
6064
|
+
/** The ONE wording of the orphan verdict. */
|
|
6065
|
+
const ORPHAN_FAILURE = "no live owner process — the run was interrupted before it settled (orphan)";
|
|
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,
|
|
6071
|
+
state: "failed",
|
|
6072
|
+
endedAt: now(),
|
|
6073
|
+
failure: ORPHAN_FAILURE
|
|
6074
|
+
});
|
|
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;
|
|
6060
6105
|
};
|
|
6061
6106
|
const persist = async () => {
|
|
6062
|
-
trim();
|
|
6063
6107
|
try {
|
|
6064
6108
|
await transactIo(options.io, path, (current) => {
|
|
6065
6109
|
const onDisk = parseIndex(current);
|
|
6066
6110
|
const merged = /* @__PURE__ */ new Map();
|
|
6067
|
-
for (const
|
|
6068
|
-
for (const
|
|
6069
|
-
const
|
|
6070
|
-
|
|
6071
|
-
|
|
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);
|
|
6116
|
+
}
|
|
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));
|
|
6072
6121
|
return JSON.stringify({
|
|
6073
6122
|
schemaVersion: 1,
|
|
6074
6123
|
runs: [
|
|
@@ -6098,52 +6147,36 @@ function newRunRegistry(options) {
|
|
|
6098
6147
|
ok: false,
|
|
6099
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)"
|
|
6100
6149
|
};
|
|
6150
|
+
const wasRunning = new Set([...foreign.values()].filter((row) => row.state === "running").map((row) => row.id));
|
|
6151
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6101
6152
|
let converged = 0;
|
|
6102
6153
|
let liveElsewhere = 0;
|
|
6103
|
-
let
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
if (
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
healed++;
|
|
6112
|
-
} else if (record.pid === void 0 || !isProcessAlive(record.pid)) {
|
|
6113
|
-
records[knownIndex] = {
|
|
6114
|
-
...record,
|
|
6115
|
-
state: "failed",
|
|
6116
|
-
endedAt: now(),
|
|
6117
|
-
failure: "no live owner process — the run was interrupted before it settled (orphan)"
|
|
6118
|
-
};
|
|
6119
|
-
converged++;
|
|
6120
|
-
}
|
|
6121
|
-
}
|
|
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++;
|
|
6122
6162
|
continue;
|
|
6123
6163
|
}
|
|
6124
|
-
if (
|
|
6125
|
-
|
|
6164
|
+
if (ownerIsGone(row)) {
|
|
6165
|
+
converged++;
|
|
6166
|
+
adopt(orphanOf(row));
|
|
6126
6167
|
continue;
|
|
6127
6168
|
}
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
records.push(record);
|
|
6131
|
-
continue;
|
|
6132
|
-
}
|
|
6133
|
-
converged++;
|
|
6134
|
-
records.push({
|
|
6135
|
-
...record,
|
|
6136
|
-
state: "failed",
|
|
6137
|
-
endedAt: now(),
|
|
6138
|
-
failure: "no live owner process — the run was interrupted before it settled (orphan)"
|
|
6139
|
-
});
|
|
6169
|
+
liveElsewhere++;
|
|
6170
|
+
if (adopt(row)) regressed++;
|
|
6140
6171
|
}
|
|
6141
|
-
|
|
6172
|
+
for (const id of [...foreign.keys()]) if (!seen.has(id)) foreign.delete(id);
|
|
6173
|
+
if (converged > 0 || regressed > 0) await persist();
|
|
6142
6174
|
const notes = [
|
|
6143
6175
|
parsed.note,
|
|
6144
6176
|
converged > 0 ? `${converged} run(s) with no live owner were recorded as failed(orphan)` : void 0,
|
|
6145
6177
|
liveElsewhere > 0 ? `${liveElsewhere} run(s) are in flight in another process sharing this home` : void 0,
|
|
6146
|
-
|
|
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
|
|
6147
6180
|
].filter((note) => note !== void 0);
|
|
6148
6181
|
return {
|
|
6149
6182
|
ok: true,
|
|
@@ -6155,7 +6188,7 @@ function newRunRegistry(options) {
|
|
|
6155
6188
|
const startedAt = now();
|
|
6156
6189
|
const controller = new AbortController();
|
|
6157
6190
|
controllers.set(id, controller);
|
|
6158
|
-
|
|
6191
|
+
owned.set(id, {
|
|
6159
6192
|
id,
|
|
6160
6193
|
kind,
|
|
6161
6194
|
state: "running",
|
|
@@ -6171,37 +6204,37 @@ function newRunRegistry(options) {
|
|
|
6171
6204
|
};
|
|
6172
6205
|
},
|
|
6173
6206
|
async settle(id, outcome) {
|
|
6174
|
-
const
|
|
6175
|
-
if (
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
if (outcome.resultRef !== void 0)
|
|
6179
|
-
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;
|
|
6180
6213
|
controllers.delete(id);
|
|
6181
6214
|
await persist();
|
|
6182
6215
|
},
|
|
6183
6216
|
inFlight() {
|
|
6184
|
-
return
|
|
6217
|
+
return view().find((row) => row.state === "running");
|
|
6185
6218
|
},
|
|
6186
6219
|
lastSettledAt() {
|
|
6187
|
-
return
|
|
6220
|
+
return view().find((row) => TERMINAL.has(row.state))?.endedAt;
|
|
6188
6221
|
},
|
|
6189
6222
|
runs() {
|
|
6190
|
-
return
|
|
6223
|
+
return capped(view());
|
|
6191
6224
|
},
|
|
6192
6225
|
find(id) {
|
|
6193
|
-
return
|
|
6226
|
+
return owned.get(id) ?? foreign.get(id);
|
|
6194
6227
|
},
|
|
6195
6228
|
cancel(id) {
|
|
6196
6229
|
const controller = controllers.get(id);
|
|
6197
6230
|
if (controller === void 0) return false;
|
|
6198
|
-
const
|
|
6199
|
-
if (
|
|
6231
|
+
const row = owned.get(id);
|
|
6232
|
+
if (row === void 0 || row.state !== "running") return false;
|
|
6200
6233
|
controller.abort();
|
|
6201
6234
|
controllers.delete(id);
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6235
|
+
row.state = "cancelled";
|
|
6236
|
+
row.endedAt = now();
|
|
6237
|
+
row.failure = "cancelled by operator";
|
|
6205
6238
|
persist();
|
|
6206
6239
|
return true;
|
|
6207
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