@lmzhen/dsh-evolution-approval 0.3.80 → 0.3.82
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 +50 -12
- package/lib/types/index.d.ts +31 -9
- package/package.json +8 -6
package/lib/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Service } from "@deepseek-ai/cordis";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { isProcessAlive, parseLockBody } from "@lmzhen/dsh-evolution-core";
|
|
3
4
|
import z from "@deepseek-ai/schemastery";
|
|
4
5
|
//#region lib/types/index.js
|
|
5
6
|
/**
|
|
@@ -135,14 +136,25 @@ var EvolutionApproval = class extends Service {
|
|
|
135
136
|
return await this.dedupe(`approve:${id}`, () => this.doApprove(id));
|
|
136
137
|
}
|
|
137
138
|
/** S2-P2-22 (0.3.80): return an ORPHANED executing record to the pending
|
|
138
|
-
* window. "Orphaned"
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
139
|
+
* window. "Orphaned" is DECIDED here, never assumed: the older rationale
|
|
140
|
+
* ("the family single-instance claim rules out any other live process") was
|
|
141
|
+
* wrong (v43 FLOW2-1) — that claim is a module-scope Map in
|
|
142
|
+
* core/instance-scope.ts, i.e. PER PROCESS, so a second process over one home
|
|
143
|
+
* is not excluded at all. Two checks stand in its place:
|
|
144
|
+
* 1. `inFlight` — an approve/reject running in THIS service instance;
|
|
145
|
+
* 2. the claim's holder pid. Claim ids are minted as `<pid>:<hex token>`, the
|
|
146
|
+
* same body shape the io write lock uses, so `parseLockBody` + the core
|
|
147
|
+
* `isProcessAlive` probe answer whether that holder can still be running: a
|
|
148
|
+
* LIVE FOREIGN pid REFUSES the release; our own pid is this process's
|
|
149
|
+
* leftover (a live in-process approve is what `inFlight` covers); a dead pid
|
|
150
|
+
* is a crash. The last two are releasable, which is the S2-P2-22 case.
|
|
151
|
+
* A claim carrying no pid (the bare-UUID claims minted before this change)
|
|
152
|
+
* cannot be probed: the release then degrades to an explicitly DESTRUCTIVE
|
|
153
|
+
* operator action and says so — re-approving replays the write, and that
|
|
154
|
+
* replay is NOT idempotent if the effect already landed. The operator is
|
|
155
|
+
* expected to verify the effect first; rejecting closes the record instead
|
|
156
|
+
* (the stored `claimedBy` rides along as the seam's release credential, so no
|
|
157
|
+
* state-seam expansion is needed). */
|
|
146
158
|
async release(id) {
|
|
147
159
|
const executing = (await this.state().listPending("executing")).find((item) => item.id === id);
|
|
148
160
|
if (!executing) {
|
|
@@ -164,15 +176,32 @@ var EvolutionApproval = class extends Service {
|
|
|
164
176
|
ok: false,
|
|
165
177
|
message: `Pending write "${id}" carries no claim to release (unexpected record shape) — reject it instead.`
|
|
166
178
|
};
|
|
167
|
-
|
|
179
|
+
const holderPid = parseLockBody(claimId);
|
|
180
|
+
if (holderPid !== null && holderPid !== process.pid && isProcessAlive(holderPid)) return {
|
|
181
|
+
ok: false,
|
|
182
|
+
message: `Pending write "${id}" is claimed by pid ${holderPid}, which is ALIVE — another process may be running this approve. Do not release it: verify the write effect instead and let that run finish (a completed approve resolves its own record).`
|
|
183
|
+
};
|
|
184
|
+
let released = false;
|
|
185
|
+
let failure = "";
|
|
186
|
+
try {
|
|
187
|
+
await this.state().releasePendingClaim(id, claimId);
|
|
188
|
+
released = (await this.state().listPending("pending")).some((item) => item.id === id);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
failure = error instanceof Error ? error.message : String(error);
|
|
191
|
+
this.ctx.logger.warn(error);
|
|
192
|
+
}
|
|
193
|
+
if (!released) return {
|
|
194
|
+
ok: false,
|
|
195
|
+
message: `Release of "${id}" did not take effect (${failure === "" ? "the claim no longer matches the record — a concurrent writer moved it" : failure}) — it stays EXECUTING with its claim unless it resolved, and the write effect is still unverified. Retry, or reject it to close the record without replaying.`
|
|
196
|
+
};
|
|
168
197
|
return {
|
|
169
198
|
ok: true,
|
|
170
|
-
message: `Released "${id}" back to the pending window. VERIFY the effect
|
|
199
|
+
message: `Released "${id}" back to the pending window.${holderPid === null ? " Its claim carries no pid, so liveness could NOT be verified: another process may still be running this approve." : ""} VERIFY the effect before approving: approve re-runs the write, and a non-idempotent replay duplicates an effect that already landed; reject closes the record instead.`
|
|
171
200
|
};
|
|
172
201
|
}
|
|
173
202
|
async reject(id) {
|
|
174
203
|
return await this.dedupe(`reject:${id}`, async () => {
|
|
175
|
-
const claimId =
|
|
204
|
+
const claimId = this.newClaimId();
|
|
176
205
|
if (!await this.state().claimPending(id, claimId)) {
|
|
177
206
|
if ((await this.state().listPending("executing")).find((item) => item.id === id)) return (await this.state().tryResolvePending(id, "rejected")).applied ? {
|
|
178
207
|
ok: true,
|
|
@@ -217,6 +246,15 @@ var EvolutionApproval = class extends Service {
|
|
|
217
246
|
if (override === "never" || override === "ask") return override;
|
|
218
247
|
return platformApproval.config?.policy ?? "ask";
|
|
219
248
|
}
|
|
249
|
+
/** v43 FLOW2-1: a claim id that CARRIES its holder pid — `parseLockBody`
|
|
250
|
+
* reads it back and `isProcessAlive` turns it into `release()`'s verifiable
|
|
251
|
+
* orphan criterion. The `<pid>:<hex token>` body is the io write lock's own
|
|
252
|
+
* shape, so one set of core helpers reads both; the token stays unique per
|
|
253
|
+
* call, so two approves of one record can never share a claim (the token is a
|
|
254
|
+
* dashed UUID stripped to hex because `LOCK_BODY_RE` accepts only hex). */
|
|
255
|
+
newClaimId() {
|
|
256
|
+
return `${process.pid}:${randomUUID().split("-").join("")}`;
|
|
257
|
+
}
|
|
220
258
|
dedupe(id, task) {
|
|
221
259
|
const existing = this.inFlight.get(id);
|
|
222
260
|
if (existing) return existing;
|
|
@@ -227,7 +265,7 @@ var EvolutionApproval = class extends Service {
|
|
|
227
265
|
return run;
|
|
228
266
|
}
|
|
229
267
|
async doApprove(id) {
|
|
230
|
-
const claimId =
|
|
268
|
+
const claimId = this.newClaimId();
|
|
231
269
|
const record = await this.state().claimPending(id, claimId);
|
|
232
270
|
if (!record) {
|
|
233
271
|
if ((await this.state().listPending("executing")).find((item) => item.id === id)) return {
|
package/lib/types/index.d.ts
CHANGED
|
@@ -97,7 +97,11 @@ export type ApprovalLike = {
|
|
|
97
97
|
}>;
|
|
98
98
|
/** S2-P2-22 (0.3.80): return an ORPHANED executing record (an approve that
|
|
99
99
|
* crashed mid-run) to the pending window, so it can be deliberately
|
|
100
|
-
* re-approved or rejected.
|
|
100
|
+
* re-approved or rejected. v43 FLOW2-1: "orphaned" is DECIDED — from this
|
|
101
|
+
* process's in-flight set AND the holder pid the record's claim carries — so a
|
|
102
|
+
* claim naming a live foreign pid is REFUSED, and a claim without a pid is
|
|
103
|
+
* released only as an explicitly destructive operator action. Optional: only
|
|
104
|
+
* the real service implements it. */
|
|
101
105
|
release?(id: string): Promise<{
|
|
102
106
|
ok: boolean;
|
|
103
107
|
message: string;
|
|
@@ -164,14 +168,25 @@ export declare class EvolutionApproval extends Service {
|
|
|
164
168
|
message: string;
|
|
165
169
|
}>;
|
|
166
170
|
/** S2-P2-22 (0.3.80): return an ORPHANED executing record to the pending
|
|
167
|
-
* window. "Orphaned"
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
171
|
+
* window. "Orphaned" is DECIDED here, never assumed: the older rationale
|
|
172
|
+
* ("the family single-instance claim rules out any other live process") was
|
|
173
|
+
* wrong (v43 FLOW2-1) — that claim is a module-scope Map in
|
|
174
|
+
* core/instance-scope.ts, i.e. PER PROCESS, so a second process over one home
|
|
175
|
+
* is not excluded at all. Two checks stand in its place:
|
|
176
|
+
* 1. `inFlight` — an approve/reject running in THIS service instance;
|
|
177
|
+
* 2. the claim's holder pid. Claim ids are minted as `<pid>:<hex token>`, the
|
|
178
|
+
* same body shape the io write lock uses, so `parseLockBody` + the core
|
|
179
|
+
* `isProcessAlive` probe answer whether that holder can still be running: a
|
|
180
|
+
* LIVE FOREIGN pid REFUSES the release; our own pid is this process's
|
|
181
|
+
* leftover (a live in-process approve is what `inFlight` covers); a dead pid
|
|
182
|
+
* is a crash. The last two are releasable, which is the S2-P2-22 case.
|
|
183
|
+
* A claim carrying no pid (the bare-UUID claims minted before this change)
|
|
184
|
+
* cannot be probed: the release then degrades to an explicitly DESTRUCTIVE
|
|
185
|
+
* operator action and says so — re-approving replays the write, and that
|
|
186
|
+
* replay is NOT idempotent if the effect already landed. The operator is
|
|
187
|
+
* expected to verify the effect first; rejecting closes the record instead
|
|
188
|
+
* (the stored `claimedBy` rides along as the seam's release credential, so no
|
|
189
|
+
* state-seam expansion is needed). */
|
|
175
190
|
release(id: string): Promise<{
|
|
176
191
|
ok: boolean;
|
|
177
192
|
message: string;
|
|
@@ -190,6 +205,13 @@ export declare class EvolutionApproval extends Service {
|
|
|
190
205
|
* check is lazy: the platform service can start before or after this plugin.
|
|
191
206
|
*/
|
|
192
207
|
private deriveSessionPolicy;
|
|
208
|
+
/** v43 FLOW2-1: a claim id that CARRIES its holder pid — `parseLockBody`
|
|
209
|
+
* reads it back and `isProcessAlive` turns it into `release()`'s verifiable
|
|
210
|
+
* orphan criterion. The `<pid>:<hex token>` body is the io write lock's own
|
|
211
|
+
* shape, so one set of core helpers reads both; the token stays unique per
|
|
212
|
+
* call, so two approves of one record can never share a claim (the token is a
|
|
213
|
+
* dashed UUID stripped to hex because `LOCK_BODY_RE` accepts only hex). */
|
|
214
|
+
private newClaimId;
|
|
193
215
|
private dedupe;
|
|
194
216
|
private doApprove;
|
|
195
217
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmzhen/dsh-evolution-approval",
|
|
3
3
|
"description": "Stage/pending approval service for Hermes-style self-evolution writes (community build)",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.82",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -26,15 +26,17 @@
|
|
|
26
26
|
],
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@deepseek-ai/schemastery": "^3.18.1"
|
|
29
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
30
|
+
"@lmzhen/dsh-evolution-core": "^0.3.82"
|
|
30
31
|
},
|
|
31
32
|
"peerDependencies": {
|
|
32
33
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
33
|
-
"@lmzhen/dsh-evolution-state-storage": "^0.3.
|
|
34
|
-
"@lmzhen/dsh-evolution-state": "^0.3.
|
|
34
|
+
"@lmzhen/dsh-evolution-state-storage": "^0.3.82",
|
|
35
|
+
"@lmzhen/dsh-evolution-state": "^0.3.82"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
|
-
"@lmzhen/dsh-evolution-
|
|
38
|
-
"@lmzhen/dsh-evolution-state": "^0.3.
|
|
38
|
+
"@lmzhen/dsh-evolution-core": "^0.3.82",
|
|
39
|
+
"@lmzhen/dsh-evolution-state-storage": "^0.3.82",
|
|
40
|
+
"@lmzhen/dsh-evolution-state": "^0.3.82"
|
|
39
41
|
}
|
|
40
42
|
}
|