@unbrained/pm-cli 2026.9.2 → 2026.9.4
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/.claude-plugin/marketplace.json +2 -2
- package/CHANGELOG.md +44 -0
- package/dist/cli/after-command-context-usage.js +16 -3
- package/dist/cli-bundle/bundle-manifest.json +151 -151
- package/dist/cli-bundle/chunks/chunk-6XSKPNCF.js +230 -0
- package/dist/cli-bundle/chunks/chunk-A24WAVE6.js +8 -0
- package/dist/cli-bundle/chunks/{chunk-TAG4VC22.js → chunk-HI6BHPCU.js} +27 -27
- package/dist/cli-bundle/chunks/{chunk-Y7VOMSZT.js → chunk-I2AS367G.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-X4RCGLTX.js → chunk-OGBVU67G.js} +5 -5
- package/dist/cli-bundle/chunks/{chunk-DTGWEL23.js → chunk-RI4XNOYS.js} +2 -2
- package/dist/cli-bundle/chunks/chunk-TXW6VL33.js +38 -0
- package/dist/cli-bundle/chunks/{chunk-FZ4Z2J4F.js → chunk-V72UV7QS.js} +76 -79
- package/dist/cli-bundle/chunks/chunk-VGLHECAP.js +9 -0
- package/dist/cli-bundle/chunks/{chunk-T6H5BZCP.js → chunk-X6CMGV6L.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-3WICXLVT.js → chunk-YW4QK2G7.js} +2 -2
- package/dist/cli-bundle/chunks/{register-list-query-XYNX4ASQ.js → register-list-query-Z5YYSJJG.js} +2 -2
- package/dist/cli-bundle/chunks/{register-mutation-WFQ7MJTX.js → register-mutation-ELSP6I4I.js} +2 -2
- package/dist/cli-bundle/chunks/{register-operations-JVTI7JPP.js → register-operations-OH7R3ZT4.js} +2 -2
- package/dist/cli-bundle/chunks/{register-setup-KVNC5KPL.js → register-setup-E2AE72QG.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-MEASX544.js → chunk-25UJBH4M.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-SVXJYD6G.js → chunk-3646BZ4W.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-4YRAVI43.js +12 -0
- package/dist/cli-bundle/focused-chunks/chunk-53IHPICX.js +3 -0
- package/dist/cli-bundle/focused-chunks/chunk-5P4HVOWG.js +176 -0
- package/dist/cli-bundle/focused-chunks/chunk-636VVQ7F.js +54 -0
- package/dist/cli-bundle/focused-chunks/{chunk-4O4CJAR6.js → chunk-6PWPQKDN.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-7N2TV7WZ.js +8 -0
- package/dist/cli-bundle/focused-chunks/chunk-AJR3OP2E.js +3 -0
- package/dist/cli-bundle/focused-chunks/chunk-B3A6MJYE.js +2 -0
- package/dist/cli-bundle/focused-chunks/chunk-BFDDJ2T2.js +2 -0
- package/dist/cli-bundle/focused-chunks/chunk-BHUIRZWQ.js +9 -0
- package/dist/cli-bundle/focused-chunks/chunk-JADHHPY6.js +6 -0
- package/dist/cli-bundle/focused-chunks/{chunk-KB5OFSFD.js → chunk-MHKUIPJW.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-NBMJD6MK.js +2 -0
- package/dist/cli-bundle/focused-chunks/chunk-NSKBLHVI.js +20 -0
- package/dist/cli-bundle/focused-chunks/{chunk-BEVHCEWG.js → chunk-P2GOAOQN.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-YARWJM2B.js +2 -0
- package/dist/cli-bundle/main.js +15 -15
- package/dist/cli-bundle/sdk-authoring.js +1 -1
- package/dist/cli-bundle/sdk-contracts.js +1 -1
- package/dist/cli-bundle/sdk-core.js +28 -31
- package/dist/cli-bundle/sdk-governance.js +1 -1
- package/dist/cli-bundle/sdk-graph.js +1 -1
- package/dist/cli-bundle/sdk-merge.js +43 -36
- package/dist/cli-bundle/sdk-query.js +1 -1
- package/dist/cli-bundle/sdk-runtime.js +1 -1
- package/dist/cli-bundle/sdk-testing.js +1 -1
- package/dist/cli-bundle/sdk.js +10 -10
- package/dist/core/diagnostics/remediation.js +4 -4
- package/dist/core/history/event-classification.d.ts +21 -0
- package/dist/core/history/event-classification.js +109 -0
- package/dist/core/history/event-index.d.ts +14 -3
- package/dist/core/history/event-index.js +543 -88
- package/dist/core/history/history.d.ts +37 -1
- package/dist/core/history/history.js +320 -71
- package/dist/core/history/replay.js +68 -34
- package/dist/core/history/workspace-history.js +7 -6
- package/dist/core/lock/lock.d.ts +32 -2
- package/dist/core/lock/lock.js +161 -20
- package/dist/core/output/output.js +31 -7
- package/dist/core/shared/author.d.ts +3 -1
- package/dist/core/shared/author.js +111 -23
- package/dist/core/shared/time.d.ts +2 -0
- package/dist/core/shared/time.js +8 -2
- package/dist/core/store/item-metadata-query-index.js +5 -5
- package/dist/sdk/context/recency.d.ts +9 -0
- package/dist/sdk/context/recency.js +34 -0
- package/dist/sdk/context/usage-egress.d.ts +9 -0
- package/dist/sdk/context/usage-egress.js +30 -0
- package/dist/sdk/context-intent-contracts.js +7 -3
- package/dist/sdk/context-relevance.d.ts +23 -0
- package/dist/sdk/context-relevance.js +94 -23
- package/dist/sdk/context-signal-store.d.ts +7 -3
- package/dist/sdk/context-signal-store.js +155 -42
- package/dist/sdk/context-usage.d.ts +54 -0
- package/dist/sdk/context-usage.js +245 -16
- package/dist/sdk/environment/agent-environment.d.ts +296 -0
- package/dist/sdk/environment/agent-environment.js +467 -0
- package/dist/sdk/environment/project-runtime-compatibility.d.ts +19 -1
- package/dist/sdk/environment/project-runtime-compatibility.js +118 -21
- package/dist/sdk/generated/generated-error-code-catalog-part-1.js +14 -2
- package/dist/sdk/generated/generated-error-code-catalog-part-2.js +14 -2
- package/dist/sdk/governance/health.js +13 -44
- package/dist/sdk/governance/merge-receipt-history.d.ts +62 -0
- package/dist/sdk/governance/merge-receipt-history.js +207 -0
- package/dist/sdk/governance/provenance-health.js +9 -3
- package/dist/sdk/history-compact.js +14 -5
- package/dist/sdk/history-redact.js +58 -23
- package/dist/sdk/history-repair.js +16 -6
- package/dist/sdk/index.d.ts +4 -0
- package/dist/sdk/index.js +6 -2
- package/dist/sdk/init.d.ts +2 -0
- package/dist/sdk/init.js +18 -9
- package/dist/sdk/merge/driver.d.ts +3 -1
- package/dist/sdk/merge/driver.js +9 -9
- package/dist/sdk/merge/index.d.ts +1 -1
- package/dist/sdk/merge/index.js +3 -3
- package/dist/sdk/merge/receipts.d.ts +17 -6
- package/dist/sdk/merge/receipts.js +246 -75
- package/dist/sdk/merge/reconcile.d.ts +9 -7
- package/dist/sdk/merge/reconcile.js +175 -12
- package/dist/sdk/merge/three-way.d.ts +22 -2
- package/dist/sdk/merge/three-way.js +92 -25
- package/dist/sdk/query/context.d.ts +3 -1
- package/dist/sdk/query/context.js +21 -9
- package/dist/sdk/query/next.js +18 -9
- package/dist/sdk/runtime-input.d.ts +2 -0
- package/dist/sdk/runtime-input.js +25 -2
- package/dist/sdk/runtime-primitives.d.ts +2 -2
- package/dist/sdk/runtime-primitives.js +4 -4
- package/dist/sdk/runtime.d.ts +1 -0
- package/dist/sdk/runtime.js +17 -26
- package/dist/types.d.ts +27 -0
- package/dist/types.js +2 -2
- package/docs/AGENT_ENVIRONMENTS.md +47 -0
- package/docs/AGENT_PROVENANCE_ADR.md +17 -2
- package/docs/CONTEXT_INTEGRITY_CONTRACTS.md +38 -1
- package/docs/CONTEXT_RELEVANCE.md +44 -1
- package/docs/MERGE_SAFETY.md +119 -23
- package/docs/PERFORMANCE.md +6 -3
- package/docs/README.md +1 -0
- package/docs/READ_OUTPUT_CONTRACTS.md +9 -1
- package/docs/SDK.md +11 -8
- package/docs/SDK_CONTEXT.md +7 -0
- package/docs/SDK_CONTEXT_EVIDENCE_CONTRACTS.md +24 -2
- package/docs/SDK_RUNTIME_BOUNDARIES.md +17 -1
- package/docs/examples/sdk-agent-environment/README.md +14 -0
- package/docs/examples/sdk-agent-environment/package.json +8 -0
- package/docs/examples/sdk-agent-environment/run-agent-environment.mjs +154 -0
- package/docs/generated/REFUSAL_CLOSURE_CENSUS.md +5 -3
- package/marketplace.json +2 -2
- package/package.json +5 -5
- package/packages/pm-beads/package.json +1 -1
- package/packages/pm-calendar/package.json +1 -1
- package/packages/pm-command-kit/package.json +1 -1
- package/packages/pm-digital-twin/package.json +1 -1
- package/packages/pm-governance-audit/package.json +1 -1
- package/packages/pm-guide-shell/package.json +1 -1
- package/packages/pm-kanban/package.json +1 -1
- package/packages/pm-lifecycle-hooks/package.json +1 -1
- package/packages/pm-linked-test-adapters/package.json +1 -1
- package/packages/pm-search-advanced/package.json +1 -1
- package/packages/pm-templates/package.json +1 -1
- package/packages/pm-todos/package.json +1 -1
- package/packages/pm-vcs/package.json +1 -1
- package/plugins/pm-claude/.claude-plugin/plugin.json +1 -1
- package/plugins/pm-codex/.codex-plugin/plugin.json +1 -1
- package/sdk/public-surface.json +773 -46
- package/dist/cli-bundle/chunks/chunk-GOACULJW.js +0 -13
- package/dist/cli-bundle/chunks/chunk-NOPL37X4.js +0 -205
- package/dist/cli-bundle/chunks/chunk-UDW64J2W.js +0 -8
- package/dist/cli-bundle/chunks/chunk-ZLJIFSOE.js +0 -35
- package/dist/cli-bundle/focused-chunks/chunk-242AXYB2.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-2FEZ5SKM.js +0 -3
- package/dist/cli-bundle/focused-chunks/chunk-6GCRSLPG.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-7VATS5QY.js +0 -16
- package/dist/cli-bundle/focused-chunks/chunk-JEFKLWMM.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-MKHG3T76.js +0 -20
- package/dist/cli-bundle/focused-chunks/chunk-O67VDLOT.js +0 -51
- package/dist/cli-bundle/focused-chunks/chunk-PF3E6Q36.js +0 -8
- package/dist/cli-bundle/focused-chunks/chunk-QDFJIRKN.js +0 -4
- package/dist/cli-bundle/focused-chunks/chunk-RMRCQPWU.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-S4U5CN5Q.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-T2LXXSOD.js +0 -158
- package/dist/cli-bundle/focused-chunks/chunk-Z2USIBR2.js +0 -5
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
* Implements append-only history and replay behavior for Replay.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
7
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="ce8dfbdf-8471-5843-8179-5608140330e1")}catch(e){}}();
|
|
8
8
|
import jsonPatch from "fast-json-patch";
|
|
9
9
|
import { ITEM_METADATA_KEY_ORDER } from "../shared/constants.js";
|
|
10
10
|
import { canonicalDocument } from "../item/item-format.js";
|
|
11
11
|
import { toItemRecord } from "../item/item-record.js";
|
|
12
12
|
import { orderObject, sha256Hex, stableStringify, } from "../shared/serialization.js";
|
|
13
|
-
import { CURRENT_HISTORY_ITEM_HASH_VERSION, SUPPORTED_HISTORY_ITEM_HASH_VERSIONS, hashDocumentVerificationCandidates, } from "./history.js";
|
|
13
|
+
import { CURRENT_HISTORY_ITEM_HASH_VERSION, SUPPORTED_HISTORY_ITEM_HASH_VERSIONS, hashDocumentVerificationCandidates, resealHistoryRewrite, verifyHistoryRecordHash, verifyHistoryRewriteEvidence, } from "./history.js";
|
|
14
14
|
/** Public contract for empty replay document, shared by SDK and presentation-layer consumers. */
|
|
15
15
|
export const EMPTY_REPLAY_DOCUMENT = {
|
|
16
16
|
metadata: {},
|
|
@@ -165,6 +165,31 @@ function historyEntryHashCandidates(explicitVersion, authoritativeVersion) {
|
|
|
165
165
|
? [...SUPPORTED_HISTORY_ITEM_HASH_VERSIONS]
|
|
166
166
|
: [authoritativeVersion];
|
|
167
167
|
}
|
|
168
|
+
/** Return the first record or retained-rewrite integrity failure for one entry. */
|
|
169
|
+
function historyEntryIntegrityError(entry) {
|
|
170
|
+
const recordVerification = verifyHistoryRecordHash(entry);
|
|
171
|
+
if (!recordVerification.ok)
|
|
172
|
+
return recordVerification.error;
|
|
173
|
+
const rewriteVerification = verifyHistoryRewriteEvidence(entry);
|
|
174
|
+
return rewriteVerification.ok ? undefined : rewriteVerification.error;
|
|
175
|
+
}
|
|
176
|
+
/** Validate immutable metadata and apply one patch for chain verification. */
|
|
177
|
+
function verifyHistoryEntryInput(entry, replay) {
|
|
178
|
+
const integrityError = historyEntryIntegrityError(entry);
|
|
179
|
+
if (integrityError)
|
|
180
|
+
return { ok: false, error: integrityError };
|
|
181
|
+
const applied = tryApplyReplayPatch(replay, entry.patch);
|
|
182
|
+
return applied.ok
|
|
183
|
+
? { ok: true, document: applied.document }
|
|
184
|
+
: { ok: false, error: "patch_apply_failed" };
|
|
185
|
+
}
|
|
186
|
+
/** Refuse any entry whose current or retained immutable envelope is invalid. */
|
|
187
|
+
function assertHistoryEntryIntegrity(entry, index) {
|
|
188
|
+
const integrityError = historyEntryIntegrityError(entry);
|
|
189
|
+
if (integrityError) {
|
|
190
|
+
throw new TypeError(`${integrityError}:entry_${index + 1}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
168
193
|
/** Deterministically verify a history chain: each entry's before_hash must equal the prior replayed after_hash, the patch must strictly apply, and the recorded after_hash must equal the replayed result. */
|
|
169
194
|
/** Verify a chain and report the explicit or auto-detected item hash epoch. */
|
|
170
195
|
export function verifyHistoryChainWithVersion(entries) {
|
|
@@ -179,18 +204,15 @@ export function verifyHistoryChainWithVersion(entries) {
|
|
|
179
204
|
if (unsupportedVersion) {
|
|
180
205
|
errors.push(`verify_failed:unsupported_item_hash_version:${String(explicitVersion)}:entry_${index + 1}`);
|
|
181
206
|
}
|
|
182
|
-
const
|
|
183
|
-
if (!
|
|
207
|
+
const input = verifyHistoryEntryInput(entry, replay);
|
|
208
|
+
if (!input.ok) {
|
|
184
209
|
return {
|
|
185
210
|
ok: false,
|
|
186
|
-
errors: [
|
|
187
|
-
...errors,
|
|
188
|
-
`verify_failed:patch_apply_failed:entry_${index + 1}`,
|
|
189
|
-
],
|
|
211
|
+
errors: [...errors, `verify_failed:${input.error}:entry_${index + 1}`],
|
|
190
212
|
};
|
|
191
213
|
}
|
|
192
214
|
if (unsupportedVersion) {
|
|
193
|
-
replay =
|
|
215
|
+
replay = input.document;
|
|
194
216
|
detectedVersion = undefined;
|
|
195
217
|
authoritativeExplicitVersion = undefined;
|
|
196
218
|
continue;
|
|
@@ -201,7 +223,7 @@ export function verifyHistoryChainWithVersion(entries) {
|
|
|
201
223
|
const candidates = historyEntryHashCandidates(explicitVersion, authoritativeExplicitVersion);
|
|
202
224
|
const candidateMatches = candidates.map((version) => ({
|
|
203
225
|
version,
|
|
204
|
-
match: matchReplayHashCandidates(replay,
|
|
226
|
+
match: matchReplayHashCandidates(replay, input.document, version, entry),
|
|
205
227
|
}));
|
|
206
228
|
const matchingVersion = candidateMatches.find(({ match }) => match.pairIndex >= 0);
|
|
207
229
|
if (matchingVersion === undefined) {
|
|
@@ -215,7 +237,7 @@ export function verifyHistoryChainWithVersion(entries) {
|
|
|
215
237
|
};
|
|
216
238
|
}
|
|
217
239
|
const version = matchingVersion.version;
|
|
218
|
-
replay =
|
|
240
|
+
replay = input.document;
|
|
219
241
|
detectedVersion = version;
|
|
220
242
|
authoritativeExplicitVersion =
|
|
221
243
|
explicitVersion ??
|
|
@@ -304,6 +326,27 @@ export function resolveHistoryRepairItemHashVersion(entries) {
|
|
|
304
326
|
}
|
|
305
327
|
return CURRENT_HISTORY_ITEM_HASH_VERSION;
|
|
306
328
|
}
|
|
329
|
+
/** Apply one patch strictly or return its deterministic lenient repair projection. */
|
|
330
|
+
function applyReanchorPatch(replay, entry) {
|
|
331
|
+
const strict = tryApplyReplayPatch(replay, entry.patch);
|
|
332
|
+
if (strict.ok) {
|
|
333
|
+
return {
|
|
334
|
+
next: strict.document,
|
|
335
|
+
patch: entry.patch,
|
|
336
|
+
repaired: false,
|
|
337
|
+
converted: 0,
|
|
338
|
+
skipped: 0,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
const lenient = lenientApplyReplayPatch(replay, entry.patch);
|
|
342
|
+
return {
|
|
343
|
+
next: lenient.document,
|
|
344
|
+
patch: jsonPatch.compare(replay, lenient.document),
|
|
345
|
+
repaired: true,
|
|
346
|
+
converted: lenient.convertedReplaceToAdd,
|
|
347
|
+
skipped: lenient.skippedOps,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
307
350
|
/** Re-anchor a drifted history chain: replay every entry from empty, recompute the before/after hashes, and only rewrite a patch when the original op set no longer strictly applies (legacy drift). Clean entries keep their patch verbatim so the on-disk diff stays minimal. The returned chain verifies via verifyHistoryChain. */
|
|
308
351
|
export function reanchorHistoryEntries(entries, itemHashVersion = resolveHistoryRepairItemHashVersion(entries), options = {}) {
|
|
309
352
|
const unsupportedIndex = entries.findIndex((entry) => isUnsupportedExplicitHistoryItemHashVersion(entry.item_hash_version));
|
|
@@ -322,27 +365,12 @@ export function reanchorHistoryEntries(entries, itemHashVersion = resolveHistory
|
|
|
322
365
|
entries.some((entry) => entry.item_hash_version !== undefined);
|
|
323
366
|
for (let index = 0; index < entries.length; index += 1) {
|
|
324
367
|
const entry = entries[index];
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if (strict.ok) {
|
|
332
|
-
next = strict.document;
|
|
333
|
-
outPatch = entry.patch;
|
|
334
|
-
}
|
|
335
|
-
else {
|
|
336
|
-
const lenient = lenientApplyReplayPatch(replay, entry.patch);
|
|
337
|
-
next = lenient.document;
|
|
338
|
-
outPatch = jsonPatch.compare(replay, next);
|
|
339
|
-
patchRepaired = true;
|
|
340
|
-
entryConverted = lenient.convertedReplaceToAdd;
|
|
341
|
-
entrySkipped = lenient.skippedOps;
|
|
342
|
-
convertedReplaceToAdd += entryConverted;
|
|
343
|
-
skippedOps += entrySkipped;
|
|
344
|
-
entriesPatchRepaired += 1;
|
|
345
|
-
}
|
|
368
|
+
assertHistoryEntryIntegrity(entry, index);
|
|
369
|
+
const patchApplication = applyReanchorPatch(replay, entry);
|
|
370
|
+
const { next, patch: outPatch, repaired: patchRepaired, converted: entryConverted, skipped: entrySkipped, } = patchApplication;
|
|
371
|
+
convertedReplaceToAdd += entryConverted;
|
|
372
|
+
skippedOps += entrySkipped;
|
|
373
|
+
entriesPatchRepaired += Number(patchRepaired);
|
|
346
374
|
const hashMatch = matchReplayHashCandidates(replay, next, itemHashVersion, entry);
|
|
347
375
|
if (semanticIndex === undefined || options.continuousHashSurface !== true) {
|
|
348
376
|
semanticIndex = [
|
|
@@ -361,7 +389,7 @@ export function reanchorHistoryEntries(entries, itemHashVersion = resolveHistory
|
|
|
361
389
|
if (rehashed) {
|
|
362
390
|
entriesRehashed += 1;
|
|
363
391
|
}
|
|
364
|
-
|
|
392
|
+
let rewrittenEntry = {
|
|
365
393
|
...entry,
|
|
366
394
|
patch: outPatch,
|
|
367
395
|
before_hash: beforeHash,
|
|
@@ -373,6 +401,12 @@ export function reanchorHistoryEntries(entries, itemHashVersion = resolveHistory
|
|
|
373
401
|
else {
|
|
374
402
|
delete rewrittenEntry.item_hash_version;
|
|
375
403
|
}
|
|
404
|
+
const versionChanged = rewrittenEntry.item_hash_version !== entry.item_hash_version;
|
|
405
|
+
if (rehashed || patchRepaired || versionChanged) {
|
|
406
|
+
rewrittenEntry = resealHistoryRewrite(entry, rewrittenEntry, {
|
|
407
|
+
retainOriginalPatch: patchRepaired,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
376
410
|
rewritten.push(rewrittenEntry);
|
|
377
411
|
details.push({
|
|
378
412
|
index: index + 1,
|
|
@@ -403,4 +437,4 @@ export function historyEntriesToRaw(entries) {
|
|
|
403
437
|
return `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`;
|
|
404
438
|
}
|
|
405
439
|
//# sourceMappingURL=replay.js.map
|
|
406
|
-
//# debugId=
|
|
440
|
+
//# debugId=ce8dfbdf-8471-5843-8179-5608140330e1
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* HistoryEntry format used by item mutations.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
8
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="4df21a17-43c0-5b63-adf3-657699e6a058")}catch(e){}}();
|
|
9
9
|
import fs from "node:fs/promises";
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import { readFileIfExists, writeFileAtomic } from "../fs/fs-utils.js";
|
|
@@ -13,6 +13,7 @@ import { acquireLock } from "../lock/lock.js";
|
|
|
13
13
|
import { EMPTY_CANONICAL_DOCUMENT, EXIT_CODE } from "../shared/constants.js";
|
|
14
14
|
import { PmCliError } from "../shared/errors.js";
|
|
15
15
|
import { stableStringify } from "../shared/serialization.js";
|
|
16
|
+
import { nowIso } from "../shared/time.js";
|
|
16
17
|
import { appendHistoryEntry, createHistoryEntry } from "./history.js";
|
|
17
18
|
import { readHistoryEntries } from "./read.js";
|
|
18
19
|
import { cloneEmptyReplayDocument, replayToItemDocument, tryApplyReplayPatch, verifyHistoryChain, } from "./replay.js";
|
|
@@ -153,7 +154,7 @@ async function appendWorkspaceHistoryChangeLocked(change) {
|
|
|
153
154
|
if (idempotentEntry) {
|
|
154
155
|
return { entry: idempotentEntry, historyPath };
|
|
155
156
|
}
|
|
156
|
-
const timestamp =
|
|
157
|
+
const timestamp = nowIso();
|
|
157
158
|
const beforeDocument = entries.length === 0
|
|
158
159
|
? EMPTY_CANONICAL_DOCUMENT
|
|
159
160
|
: replayWorkspaceEntries(entries);
|
|
@@ -222,7 +223,7 @@ export async function appendWorkspaceAuditEvent(options) {
|
|
|
222
223
|
? EMPTY_CANONICAL_DOCUMENT
|
|
223
224
|
: replayWorkspaceEntries(entries);
|
|
224
225
|
const entry = createHistoryEntry({
|
|
225
|
-
nowIso:
|
|
226
|
+
nowIso: nowIso(),
|
|
226
227
|
author: options.author,
|
|
227
228
|
op: options.op,
|
|
228
229
|
before: beforeDocument,
|
|
@@ -340,7 +341,7 @@ export async function reconcileWorkspaceJsonHistory(options) {
|
|
|
340
341
|
if (stableStringify(priorDocuments[documentPath]) === stableStringify(current)) {
|
|
341
342
|
return { changed: false, historyPath };
|
|
342
343
|
}
|
|
343
|
-
const timestamp =
|
|
344
|
+
const timestamp = nowIso();
|
|
344
345
|
const entry = createHistoryEntry({
|
|
345
346
|
nowIso: timestamp,
|
|
346
347
|
author: options.author,
|
|
@@ -393,7 +394,7 @@ export async function restoreWorkspaceJsonFromHistory(options) {
|
|
|
393
394
|
changed: false,
|
|
394
395
|
};
|
|
395
396
|
const beforeDocument = replayWorkspaceEntries(entries);
|
|
396
|
-
const timestamp =
|
|
397
|
+
const timestamp = nowIso();
|
|
397
398
|
const entry = createHistoryEntry({
|
|
398
399
|
nowIso: timestamp,
|
|
399
400
|
author: options.author,
|
|
@@ -430,4 +431,4 @@ export async function restoreWorkspaceJsonFromHistory(options) {
|
|
|
430
431
|
}
|
|
431
432
|
}
|
|
432
433
|
//# sourceMappingURL=workspace-history.js.map
|
|
433
|
-
//# debugId=
|
|
434
|
+
//# debugId=4df21a17-43c0-5b63-adf3-657699e6a058
|
package/dist/core/lock/lock.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ interface LockInfo {
|
|
|
2
2
|
id: string;
|
|
3
3
|
pid: number;
|
|
4
4
|
owner: string;
|
|
5
|
+
token?: string;
|
|
5
6
|
created_at: string;
|
|
6
7
|
ttl_seconds: number;
|
|
7
8
|
}
|
|
@@ -14,14 +15,40 @@ interface StaleLockRemovalResult {
|
|
|
14
15
|
shouldRetryCreate: boolean;
|
|
15
16
|
staleRemovalCounted: boolean;
|
|
16
17
|
}
|
|
18
|
+
/** Stable operating-system identity for a process when the host exposes one. */
|
|
19
|
+
export interface ProcessIdentity {
|
|
20
|
+
/** Operating-system process identifier captured with the owner record. */
|
|
21
|
+
pid: number;
|
|
22
|
+
/** Host process-start coordinate that distinguishes reuse of the same PID. */
|
|
23
|
+
process_start_identity?: string;
|
|
24
|
+
}
|
|
17
25
|
declare function parseLockInfo(raw: string): LockReadResult;
|
|
18
|
-
declare function readLockInfo(lockPath: string): Promise<LockReadResult>;
|
|
26
|
+
declare function readLockInfo(lockPath: string, emitReadHook?: boolean): Promise<LockReadResult>;
|
|
19
27
|
declare function isErrno(error: unknown, code: string): boolean;
|
|
20
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Classify exclusive-create failures that prove another lock lifecycle is in
|
|
30
|
+
* flight. Windows can report `EPERM`, `EACCES`, or `EBUSY` while a just-unlinked
|
|
31
|
+
* lock is still delete-pending; treating those as fatal makes bounded lock
|
|
32
|
+
* waiting fail nondeterministically. Existing paths are contention on every
|
|
33
|
+
* host. A missing or temporarily unstatable Windows path is contention only
|
|
34
|
+
* when its parent remains writable, preserving genuine permission failures.
|
|
35
|
+
*/
|
|
36
|
+
declare function isLockCreateContention(error: unknown, lockPath: string, platform?: NodeJS.Platform): Promise<boolean>;
|
|
37
|
+
declare function buildLockPayload(id: string, owner: string, ttlSeconds: number, token?: string): LockInfo;
|
|
21
38
|
declare function isStaleLock(info: LockInfo | null, ttlSeconds: number): boolean;
|
|
22
39
|
declare function lockOwnerSuffix(info: LockInfo | null): string;
|
|
23
40
|
/** Resolves the effective bounded wait budget for a contended lock: the PM_LOCK_WAIT_MS environment override wins when it parses as a non-negative integer, then the caller-provided budget (settings `locks.wait_ms`), then 0 (fail-fast, the pre-wait behavior). */
|
|
24
41
|
declare function resolveLockWaitMs(waitMs: number | undefined): number;
|
|
42
|
+
declare function parseLinuxProcessStartIdentity(raw: string): string | null;
|
|
43
|
+
/** Read the host process-start coordinate used to distinguish PID reuse. */
|
|
44
|
+
export declare function readProcessStartIdentity(pid: number): Promise<string | null>;
|
|
45
|
+
/** Capture the current PID plus a host process-start coordinate when available. */
|
|
46
|
+
export declare function captureProcessIdentity(pid?: number): Promise<ProcessIdentity>;
|
|
47
|
+
/**
|
|
48
|
+
* Verify that a captured process owner still exists, with a bounded grace
|
|
49
|
+
* period when the host cannot expose a reusable-PID-safe start coordinate.
|
|
50
|
+
*/
|
|
51
|
+
export declare function isProcessIdentityAlive(identity: ProcessIdentity, unverifiedAgeMs: number, maxUnverifiedAgeMs: number): Promise<boolean>;
|
|
25
52
|
declare function acquireStaleCleanupGate(lockPath: string, _id: string): Promise<(() => Promise<void>) | null>;
|
|
26
53
|
declare function removeConfirmedStaleLock(params: {
|
|
27
54
|
lockPath: string;
|
|
@@ -39,10 +66,13 @@ export declare const _testOnly: {
|
|
|
39
66
|
parseLockInfo: typeof parseLockInfo;
|
|
40
67
|
readLockInfo: typeof readLockInfo;
|
|
41
68
|
isErrno: typeof isErrno;
|
|
69
|
+
isLockCreateContention: typeof isLockCreateContention;
|
|
42
70
|
buildLockPayload: typeof buildLockPayload;
|
|
43
71
|
isStaleLock: typeof isStaleLock;
|
|
44
72
|
lockOwnerSuffix: typeof lockOwnerSuffix;
|
|
45
73
|
resolveLockWaitMs: typeof resolveLockWaitMs;
|
|
74
|
+
parseLinuxProcessStartIdentity: typeof parseLinuxProcessStartIdentity;
|
|
75
|
+
readProcessStartIdentity: typeof readProcessStartIdentity;
|
|
46
76
|
acquireStaleCleanupGate: typeof acquireStaleCleanupGate;
|
|
47
77
|
removeConfirmedStaleLock: typeof removeConfirmedStaleLock;
|
|
48
78
|
};
|
package/dist/core/lock/lock.js
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* Coordinates tracker lock ownership and cleanup for Lock.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
7
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="b21a63b8-9da9-5c7c-b288-3a2dae342bef")}catch(e){}}();
|
|
8
8
|
import { isFileMissingError } from "../fs/fs-utils.js";
|
|
9
|
+
import { constants as fsConstants } from "node:fs";
|
|
9
10
|
import fs from "node:fs/promises";
|
|
10
11
|
import path from "node:path";
|
|
11
12
|
import { randomUUID } from "node:crypto";
|
|
@@ -36,12 +37,15 @@ function parseLockInfo(raw) {
|
|
|
36
37
|
const id = candidate.id;
|
|
37
38
|
const pid = candidate.pid;
|
|
38
39
|
const owner = candidate.owner;
|
|
40
|
+
const token = candidate.token;
|
|
39
41
|
const createdAt = candidate.created_at;
|
|
40
42
|
const ttlSeconds = candidate.ttl_seconds;
|
|
41
43
|
if (typeof id !== "string" ||
|
|
42
44
|
typeof pid !== "number" ||
|
|
43
45
|
!Number.isFinite(pid) ||
|
|
44
46
|
typeof owner !== "string" ||
|
|
47
|
+
(token !== undefined &&
|
|
48
|
+
(typeof token !== "string" || token.trim().length === 0)) ||
|
|
45
49
|
typeof createdAt !== "string" ||
|
|
46
50
|
typeof ttlSeconds !== "number" ||
|
|
47
51
|
!Number.isFinite(ttlSeconds)) {
|
|
@@ -55,19 +59,22 @@ function parseLockInfo(raw) {
|
|
|
55
59
|
id,
|
|
56
60
|
pid,
|
|
57
61
|
owner,
|
|
62
|
+
...(typeof token === "string" ? { token } : {}),
|
|
58
63
|
created_at: createdAt,
|
|
59
64
|
ttl_seconds: ttlSeconds,
|
|
60
65
|
},
|
|
61
66
|
warnings: [],
|
|
62
67
|
};
|
|
63
68
|
}
|
|
64
|
-
async function readLockInfo(lockPath) {
|
|
69
|
+
async function readLockInfo(lockPath, emitReadHook = true) {
|
|
65
70
|
try {
|
|
66
71
|
const raw = await fs.readFile(lockPath, "utf8");
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
if (emitReadHook) {
|
|
73
|
+
await runActiveOnReadHooks({
|
|
74
|
+
path: lockPath,
|
|
75
|
+
scope: "project",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
71
78
|
return parseLockInfo(raw);
|
|
72
79
|
}
|
|
73
80
|
catch (error) {
|
|
@@ -89,6 +96,43 @@ function isErrno(error, code) {
|
|
|
89
96
|
"code" in error &&
|
|
90
97
|
error.code === code);
|
|
91
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Classify exclusive-create failures that prove another lock lifecycle is in
|
|
101
|
+
* flight. Windows can report `EPERM`, `EACCES`, or `EBUSY` while a just-unlinked
|
|
102
|
+
* lock is still delete-pending; treating those as fatal makes bounded lock
|
|
103
|
+
* waiting fail nondeterministically. Existing paths are contention on every
|
|
104
|
+
* host. A missing or temporarily unstatable Windows path is contention only
|
|
105
|
+
* when its parent remains writable, preserving genuine permission failures.
|
|
106
|
+
*/
|
|
107
|
+
async function isLockCreateContention(error, lockPath, platform = process.platform) {
|
|
108
|
+
if (isErrno(error, "EEXIST"))
|
|
109
|
+
return true;
|
|
110
|
+
if (!isErrno(error, "EPERM") &&
|
|
111
|
+
!isErrno(error, "EACCES") &&
|
|
112
|
+
!isErrno(error, "EBUSY")) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
await fs.lstat(lockPath);
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
catch (probeError) {
|
|
120
|
+
if (platform !== "win32" ||
|
|
121
|
+
(!isFileMissingError(probeError) &&
|
|
122
|
+
!isErrno(probeError, "EPERM") &&
|
|
123
|
+
!isErrno(probeError, "EACCES") &&
|
|
124
|
+
!isErrno(probeError, "EBUSY"))) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
await fs.access(path.dirname(lockPath), fsConstants.W_OK);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
92
136
|
async function emitLockWriteHook(lockPath, op) {
|
|
93
137
|
await runActiveOnWriteHooks({
|
|
94
138
|
path: lockPath,
|
|
@@ -96,20 +140,21 @@ async function emitLockWriteHook(lockPath, op) {
|
|
|
96
140
|
op,
|
|
97
141
|
});
|
|
98
142
|
}
|
|
99
|
-
function buildLockPayload(id, owner, ttlSeconds) {
|
|
143
|
+
function buildLockPayload(id, owner, ttlSeconds, token = randomUUID()) {
|
|
100
144
|
return {
|
|
101
145
|
id,
|
|
102
146
|
pid: process.pid,
|
|
103
147
|
owner,
|
|
148
|
+
token,
|
|
104
149
|
created_at: nowIso(),
|
|
105
150
|
ttl_seconds: ttlSeconds,
|
|
106
151
|
};
|
|
107
152
|
}
|
|
108
|
-
async function createLockFile(lockPath, id, owner, ttlSeconds) {
|
|
153
|
+
async function createLockFile(lockPath, id, owner, ttlSeconds, token) {
|
|
109
154
|
await fs.mkdir(path.dirname(lockPath), { recursive: true });
|
|
110
155
|
const handle = await fs.open(lockPath, "wx");
|
|
111
156
|
try {
|
|
112
|
-
await handle.writeFile(`${JSON.stringify(buildLockPayload(id, owner, ttlSeconds), null, 2)}\n`, "utf8");
|
|
157
|
+
await handle.writeFile(`${JSON.stringify(buildLockPayload(id, owner, ttlSeconds, token), null, 2)}\n`, "utf8");
|
|
113
158
|
}
|
|
114
159
|
finally {
|
|
115
160
|
await handle.close();
|
|
@@ -127,6 +172,41 @@ async function unlinkLockWithHook(lockPath, op) {
|
|
|
127
172
|
return false;
|
|
128
173
|
}
|
|
129
174
|
}
|
|
175
|
+
async function releaseOwnedLock(lockPath, id, owner, token) {
|
|
176
|
+
const startedAtMs = Date.now();
|
|
177
|
+
let backoffMs = LOCK_WAIT_INITIAL_DELAY_MS;
|
|
178
|
+
for (;;) {
|
|
179
|
+
const releaseCleanupGate = await acquireStaleCleanupGate(lockPath, id);
|
|
180
|
+
if (releaseCleanupGate !== null) {
|
|
181
|
+
try {
|
|
182
|
+
const current = await readLockInfo(lockPath, false);
|
|
183
|
+
if (isOwnedLock(current.info, id, owner, token)) {
|
|
184
|
+
await unlinkLockWithHook(lockPath, "lock:release");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
await releaseCleanupGate();
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const current = await readLockInfo(lockPath, false);
|
|
193
|
+
if (!isOwnedLock(current.info, id, owner, token)) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const elapsedMs = Date.now() - startedAtMs;
|
|
197
|
+
if (elapsedMs >= LOCK_RELEASE_GATE_WAIT_MS) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
await sleepWithJitter(Math.min(backoffMs, LOCK_RELEASE_GATE_WAIT_MS - elapsedMs));
|
|
201
|
+
backoffMs = Math.min(backoffMs * 2, LOCK_WAIT_MAX_DELAY_MS);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function isOwnedLock(info, id, owner, token) {
|
|
205
|
+
return (info?.id === id &&
|
|
206
|
+
info.pid === process.pid &&
|
|
207
|
+
info.owner === owner &&
|
|
208
|
+
info.token === token);
|
|
209
|
+
}
|
|
130
210
|
function isStaleLock(info, ttlSeconds) {
|
|
131
211
|
const createdAtMs = info?.created_at
|
|
132
212
|
? Date.parse(info.created_at)
|
|
@@ -146,8 +226,8 @@ const MAX_STALE_LOCK_REMOVALS = 3;
|
|
|
146
226
|
const STALE_CLEANUP_GATE_SUFFIX = ".stale-cleanup";
|
|
147
227
|
const STALE_CLEANUP_GATE_OWNER_FILE = "owner.json";
|
|
148
228
|
const STALE_CLEANUP_GATE_STALE_MS = 10_000;
|
|
149
|
-
|
|
150
|
-
const
|
|
229
|
+
const LOCK_RELEASE_GATE_WAIT_MS = STALE_CLEANUP_GATE_STALE_MS + LOCK_WAIT_MAX_DELAY_MS;
|
|
230
|
+
const STALE_CLEANUP_GATE_MAX_UNVERIFIED_MS = 60 * 60_000;
|
|
151
231
|
function parseNonNegativeIntegerWaitMs(value) {
|
|
152
232
|
if (typeof value === "string") {
|
|
153
233
|
const trimmed = value.trim();
|
|
@@ -220,14 +300,24 @@ function parseStaleCleanupGateOwner(raw) {
|
|
|
220
300
|
const candidate = parsed;
|
|
221
301
|
const pid = candidate.pid;
|
|
222
302
|
const token = candidate.token;
|
|
303
|
+
const processStartIdentity = candidate.process_start_identity;
|
|
223
304
|
if (typeof pid !== "number" ||
|
|
224
305
|
!Number.isInteger(pid) ||
|
|
225
306
|
pid <= 0 ||
|
|
226
307
|
typeof token !== "string" ||
|
|
227
|
-
token.length === 0
|
|
308
|
+
token.length === 0 ||
|
|
309
|
+
(processStartIdentity !== undefined &&
|
|
310
|
+
(typeof processStartIdentity !== "string" ||
|
|
311
|
+
!/^\d+$/u.test(processStartIdentity)))) {
|
|
228
312
|
return null;
|
|
229
313
|
}
|
|
230
|
-
return {
|
|
314
|
+
return {
|
|
315
|
+
pid,
|
|
316
|
+
token,
|
|
317
|
+
...(typeof processStartIdentity === "string"
|
|
318
|
+
? { process_start_identity: processStartIdentity }
|
|
319
|
+
: {}),
|
|
320
|
+
};
|
|
231
321
|
}
|
|
232
322
|
async function readStaleCleanupGateOwner(gatePath) {
|
|
233
323
|
try {
|
|
@@ -238,6 +328,8 @@ async function readStaleCleanupGateOwner(gatePath) {
|
|
|
238
328
|
}
|
|
239
329
|
}
|
|
240
330
|
function isProcessAlive(pid) {
|
|
331
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
332
|
+
return false;
|
|
241
333
|
try {
|
|
242
334
|
process.kill(pid, 0);
|
|
243
335
|
return true;
|
|
@@ -249,6 +341,49 @@ function isProcessAlive(pid) {
|
|
|
249
341
|
return true;
|
|
250
342
|
}
|
|
251
343
|
}
|
|
344
|
+
function parseLinuxProcessStartIdentity(raw) {
|
|
345
|
+
const closeParenIndex = raw.lastIndexOf(")");
|
|
346
|
+
if (closeParenIndex < 0)
|
|
347
|
+
return null;
|
|
348
|
+
const startTime = raw
|
|
349
|
+
.slice(closeParenIndex + 2)
|
|
350
|
+
.trim()
|
|
351
|
+
.split(/\s+/u)[19];
|
|
352
|
+
return startTime !== undefined && /^\d+$/u.test(startTime) ? startTime : null;
|
|
353
|
+
}
|
|
354
|
+
/** Read the host process-start coordinate used to distinguish PID reuse. */
|
|
355
|
+
export async function readProcessStartIdentity(pid) {
|
|
356
|
+
try {
|
|
357
|
+
return parseLinuxProcessStartIdentity(await fs.readFile(`/proc/${pid}/stat`, "utf8"));
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
/** Capture the current PID plus a host process-start coordinate when available. */
|
|
364
|
+
export async function captureProcessIdentity(pid = process.pid) {
|
|
365
|
+
const processStartIdentity = await readProcessStartIdentity(pid);
|
|
366
|
+
return {
|
|
367
|
+
pid,
|
|
368
|
+
...(processStartIdentity === null
|
|
369
|
+
? {}
|
|
370
|
+
: { process_start_identity: processStartIdentity }),
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Verify that a captured process owner still exists, with a bounded grace
|
|
375
|
+
* period when the host cannot expose a reusable-PID-safe start coordinate.
|
|
376
|
+
*/
|
|
377
|
+
export async function isProcessIdentityAlive(identity, unverifiedAgeMs, maxUnverifiedAgeMs) {
|
|
378
|
+
if (!isProcessAlive(identity.pid))
|
|
379
|
+
return false;
|
|
380
|
+
const currentStartIdentity = await readProcessStartIdentity(identity.pid);
|
|
381
|
+
if (identity.process_start_identity !== undefined &&
|
|
382
|
+
currentStartIdentity !== null) {
|
|
383
|
+
return currentStartIdentity === identity.process_start_identity;
|
|
384
|
+
}
|
|
385
|
+
return unverifiedAgeMs <= maxUnverifiedAgeMs;
|
|
386
|
+
}
|
|
252
387
|
async function removeStaleCleanupGateDirectory(gatePath) {
|
|
253
388
|
try {
|
|
254
389
|
await fs.rm(gatePath, { recursive: true, force: true });
|
|
@@ -275,7 +410,10 @@ async function tryCreateStaleCleanupGate(gatePath) {
|
|
|
275
410
|
}
|
|
276
411
|
return null;
|
|
277
412
|
}
|
|
278
|
-
const owner = {
|
|
413
|
+
const owner = {
|
|
414
|
+
...(await captureProcessIdentity()),
|
|
415
|
+
token: randomUUID(),
|
|
416
|
+
};
|
|
279
417
|
try {
|
|
280
418
|
await fs.writeFile(path.join(gatePath, STALE_CLEANUP_GATE_OWNER_FILE), `${JSON.stringify(owner)}\n`, "utf8");
|
|
281
419
|
}
|
|
@@ -299,8 +437,7 @@ async function removeExpiredStaleCleanupGate(gatePath) {
|
|
|
299
437
|
}
|
|
300
438
|
const owner = await readStaleCleanupGateOwner(gatePath);
|
|
301
439
|
if (owner !== null &&
|
|
302
|
-
|
|
303
|
-
gateAgeMs <= STALE_CLEANUP_GATE_MAX_ACTIVE_MS) {
|
|
440
|
+
(await isProcessIdentityAlive(owner, gateAgeMs, STALE_CLEANUP_GATE_MAX_UNVERIFIED_MS))) {
|
|
304
441
|
return false;
|
|
305
442
|
}
|
|
306
443
|
return removeStaleCleanupGateDirectory(gatePath);
|
|
@@ -365,10 +502,13 @@ export const _testOnly = {
|
|
|
365
502
|
parseLockInfo,
|
|
366
503
|
readLockInfo,
|
|
367
504
|
isErrno,
|
|
505
|
+
isLockCreateContention,
|
|
368
506
|
buildLockPayload,
|
|
369
507
|
isStaleLock,
|
|
370
508
|
lockOwnerSuffix,
|
|
371
509
|
resolveLockWaitMs,
|
|
510
|
+
parseLinuxProcessStartIdentity,
|
|
511
|
+
readProcessStartIdentity,
|
|
372
512
|
acquireStaleCleanupGate,
|
|
373
513
|
removeConfirmedStaleLock,
|
|
374
514
|
};
|
|
@@ -400,11 +540,12 @@ export async function acquireLock(pmRoot, id, ttlSeconds, owner, force = false,
|
|
|
400
540
|
const startedAtMs = Date.now();
|
|
401
541
|
let staleRemovals = 0;
|
|
402
542
|
let backoffMs = LOCK_WAIT_INITIAL_DELAY_MS;
|
|
543
|
+
const token = randomUUID();
|
|
403
544
|
for (;;) {
|
|
404
545
|
try {
|
|
405
|
-
await createLockFile(lockPath, id, owner, ttlSeconds);
|
|
546
|
+
await createLockFile(lockPath, id, owner, ttlSeconds, token);
|
|
406
547
|
return async () => {
|
|
407
|
-
await
|
|
548
|
+
await releaseOwnedLock(lockPath, id, owner, token);
|
|
408
549
|
await runActiveServiceOverride("lock_release", {
|
|
409
550
|
pm_root: pmRoot,
|
|
410
551
|
id,
|
|
@@ -413,7 +554,7 @@ export async function acquireLock(pmRoot, id, ttlSeconds, owner, force = false,
|
|
|
413
554
|
};
|
|
414
555
|
}
|
|
415
556
|
catch (error) {
|
|
416
|
-
if (!
|
|
557
|
+
if (!(await isLockCreateContention(error, lockPath))) {
|
|
417
558
|
throw new PmCliError(`Failed to acquire lock for ${id}: ${toErrorMessage(error)}`, EXIT_CODE.GENERIC_FAILURE);
|
|
418
559
|
}
|
|
419
560
|
let lockInfo = await readLockInfo(lockPath);
|
|
@@ -447,4 +588,4 @@ export async function acquireLock(pmRoot, id, ttlSeconds, owner, force = false,
|
|
|
447
588
|
}
|
|
448
589
|
}
|
|
449
590
|
//# sourceMappingURL=lock.js.map
|
|
450
|
-
//# debugId=
|
|
591
|
+
//# debugId=b21a63b8-9da9-5c7c-b288-3a2dae342bef
|