@remnic/plugin-openclaw 1.0.36 → 1.0.38
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/dist/{capsule-export-YPDWRB3C.js → capsule-export-DX53CPIT.js} +2 -2
- package/dist/{capsule-import-SWPOFG6F.js → capsule-import-4OXCPHOT.js} +2 -2
- package/dist/{capsule-merge-YXAF7ZJW.js → capsule-merge-25AUN33Q.js} +1 -1
- package/dist/{chunk-NNAN63QK.js → chunk-CEL5ZLKP.js} +1 -1
- package/dist/{chunk-E4RM7637.js → chunk-QCCP4RU5.js} +7 -2
- package/dist/{chunk-3IHGISUN.js → chunk-TXOEHSVP.js} +1 -1
- package/dist/index.js +10 -11
- package/dist/{types-7L34HYDW.js → types-MBUINTB2.js} +1 -1
- package/openclaw.plugin.json +148 -1
- package/package.json +2 -2
- package/scripts/faiss_index.py +66 -6
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
exportCapsule,
|
|
3
3
|
isValidCapsuleSince
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-TXOEHSVP.js";
|
|
5
5
|
import "./chunk-YKV4EFUI.js";
|
|
6
|
-
import "./chunk-
|
|
6
|
+
import "./chunk-QCCP4RU5.js";
|
|
7
7
|
import "./chunk-ZS6VABML.js";
|
|
8
8
|
import "./chunk-4XDQ3KEC.js";
|
|
9
9
|
import "./chunk-MXFJXUHC.js";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
importCapsule
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-CEL5ZLKP.js";
|
|
4
4
|
import "./chunk-YKV4EFUI.js";
|
|
5
|
-
import "./chunk-
|
|
5
|
+
import "./chunk-QCCP4RU5.js";
|
|
6
6
|
import "./chunk-ZS6VABML.js";
|
|
7
7
|
import "./chunk-4XDQ3KEC.js";
|
|
8
8
|
import "./chunk-MXFJXUHC.js";
|
|
@@ -34,6 +34,11 @@ var SemverLikeSchema = external_exports.string().min(1, "capsule.version must no
|
|
|
34
34
|
/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/,
|
|
35
35
|
"capsule.version must be a semver-like string (e.g. 1.0.0)"
|
|
36
36
|
);
|
|
37
|
+
function isSafePosixRelativePathPrefix(value) {
|
|
38
|
+
if (value.startsWith("/") || value.includes("\\")) return false;
|
|
39
|
+
const segments = value.split("/");
|
|
40
|
+
return segments.every((segment) => segment !== "" && segment !== "." && segment !== "..");
|
|
41
|
+
}
|
|
37
42
|
var CapsuleRetrievalPolicySchema = external_exports.object({
|
|
38
43
|
/**
|
|
39
44
|
* Per-tier weight overrides applied during recall when the capsule is the
|
|
@@ -66,8 +71,8 @@ var CapsuleParentSchema = external_exports.object({
|
|
|
66
71
|
* capsule's records were placed. Typically `forks/<parent-capsule-id>`.
|
|
67
72
|
* Must be a non-empty string with no leading slash.
|
|
68
73
|
*/
|
|
69
|
-
forkRoot: external_exports.string().min(1, "capsule.parent.forkRoot must not be empty").refine(
|
|
70
|
-
message: "capsule.parent.forkRoot must be a relative path
|
|
74
|
+
forkRoot: external_exports.string().min(1, "capsule.parent.forkRoot must not be empty").refine(isSafePosixRelativePathPrefix, {
|
|
75
|
+
message: "capsule.parent.forkRoot must be a normalized posix-relative path with no traversal segments"
|
|
71
76
|
})
|
|
72
77
|
});
|
|
73
78
|
var CapsuleBlockSchema = external_exports.object({
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import "./chunk-UZJ7EERS.js";
|
|
2
|
-
import "./chunk-
|
|
2
|
+
import "./chunk-CEL5ZLKP.js";
|
|
3
3
|
import {
|
|
4
4
|
isValidCapsuleSince
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-TXOEHSVP.js";
|
|
6
6
|
import "./chunk-YKV4EFUI.js";
|
|
7
7
|
import {
|
|
8
8
|
CAPSULE_ID_PATTERN
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-QCCP4RU5.js";
|
|
10
10
|
import "./chunk-ZS6VABML.js";
|
|
11
11
|
import "./chunk-QQXJODFL.js";
|
|
12
12
|
import "./chunk-BU5KJVWF.js";
|
|
@@ -6098,6 +6098,7 @@ function throwIfAborted(signal, message = "operation aborted") {
|
|
|
6098
6098
|
var QMD_UPDATE_BACKOFF_MS = 15 * 60 * 1e3;
|
|
6099
6099
|
var QMD_EMBED_BACKOFF_MS = 60 * 60 * 1e3;
|
|
6100
6100
|
var QMD_CLI_WARN_THROTTLE_MS = 15 * 60 * 1e3;
|
|
6101
|
+
var QMD_AUTO_UPGRADE_CHECK_INTERVAL_MS = 24 * 60 * 6e4;
|
|
6101
6102
|
var QMD_FALLBACK_PATHS = [
|
|
6102
6103
|
path8.join(os2.homedir(), ".bun", "bin", "qmd"),
|
|
6103
6104
|
"/usr/local/bin/qmd",
|
|
@@ -6694,6 +6695,7 @@ var recallRequestSchema = external_exports.object({
|
|
|
6694
6695
|
topK: external_exports.number().int().min(0).max(200).optional(),
|
|
6695
6696
|
mode: external_exports.enum(["auto", "no_recall", "minimal", "full", "graph_mode"]).optional(),
|
|
6696
6697
|
includeDebug: external_exports.boolean().optional(),
|
|
6698
|
+
idempotencyKey: idempotencyKeySchema,
|
|
6697
6699
|
disclosure: recallDisclosureSchema.optional(),
|
|
6698
6700
|
codingContext: codingContextSchema.optional(),
|
|
6699
6701
|
/** Working directory for auto git-context resolution (issue #569). */
|
|
@@ -7788,17 +7790,13 @@ async function syncHeartbeatSurfaceEntries(params) {
|
|
|
7788
7790
|
function escapeRegExp(value) {
|
|
7789
7791
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7790
7792
|
}
|
|
7791
|
-
function buildDelimitedBoundaryClass(
|
|
7792
|
-
|
|
7793
|
-
if (value.includes("-")) {
|
|
7794
|
-
parts.push("-");
|
|
7795
|
-
}
|
|
7796
|
-
return parts.join("");
|
|
7793
|
+
function buildDelimitedBoundaryClass() {
|
|
7794
|
+
return "a-z0-9-";
|
|
7797
7795
|
}
|
|
7798
7796
|
function compileDelimitedPhrasePattern(value) {
|
|
7799
7797
|
const normalized = value.trim().toLowerCase();
|
|
7800
7798
|
if (normalized.length === 0) return null;
|
|
7801
|
-
const boundaryClass = buildDelimitedBoundaryClass(
|
|
7799
|
+
const boundaryClass = buildDelimitedBoundaryClass();
|
|
7802
7800
|
return new RegExp(
|
|
7803
7801
|
`(^|[^${boundaryClass}])${escapeRegExp(normalized)}([^${boundaryClass}]|$)`
|
|
7804
7802
|
);
|
|
@@ -11324,7 +11322,8 @@ function detectBridgeMode() {
|
|
|
11324
11322
|
}
|
|
11325
11323
|
const daemonHost = readCompatEnv("REMNIC_HOST", "ENGRAM_HOST") ?? DEFAULT_HOST;
|
|
11326
11324
|
const daemonPort = readDaemonPort();
|
|
11327
|
-
|
|
11325
|
+
const hasDaemonPidHint = isDaemonRunning();
|
|
11326
|
+
if ((hasDaemonPidHint || shouldProbeDaemonHealth(daemonHost)) && checkDaemonHealthSync(daemonHost, daemonPort)) {
|
|
11328
11327
|
return {
|
|
11329
11328
|
mode: "delegate",
|
|
11330
11329
|
daemonHost,
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "openclaw-remnic",
|
|
3
3
|
"name": "Remnic OpenClaw Plugin",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.38",
|
|
5
5
|
"kind": "memory",
|
|
6
6
|
"description": "Local semantic memory for OpenClaw with bundled Remnic core runtime. Requires plugins.slots.memory set to this plugin id for hooks to fire.",
|
|
7
7
|
"setup": {
|
|
@@ -288,6 +288,83 @@
|
|
|
288
288
|
"default": false,
|
|
289
289
|
"description": "Allow automated cold-tier backfill jobs to repair parity gaps"
|
|
290
290
|
},
|
|
291
|
+
"qmdSupportedVersion": {
|
|
292
|
+
"type": "string",
|
|
293
|
+
"default": "2.5.1",
|
|
294
|
+
"description": "Highest QMD version this Remnic build will auto-install"
|
|
295
|
+
},
|
|
296
|
+
"qmdAutoUpgradeEnabled": {
|
|
297
|
+
"type": "boolean",
|
|
298
|
+
"default": false,
|
|
299
|
+
"description": "Opt-in auto-upgrade for PATH/fallback QMD installs"
|
|
300
|
+
},
|
|
301
|
+
"qmdAutoUpgradeCheckIntervalMs": {
|
|
302
|
+
"type": "number",
|
|
303
|
+
"minimum": 60000,
|
|
304
|
+
"default": 86400000,
|
|
305
|
+
"description": "Minimum interval between QMD auto-upgrade checks"
|
|
306
|
+
},
|
|
307
|
+
"qmdChunkStrategy": {
|
|
308
|
+
"type": "string",
|
|
309
|
+
"enum": [
|
|
310
|
+
"auto",
|
|
311
|
+
"regex"
|
|
312
|
+
],
|
|
313
|
+
"default": "auto",
|
|
314
|
+
"description": "QMD chunk strategy forwarded when the installed QMD supports it"
|
|
315
|
+
},
|
|
316
|
+
"qmdCandidateLimit": {
|
|
317
|
+
"type": "number",
|
|
318
|
+
"minimum": 1,
|
|
319
|
+
"description": "Optional candidate limit forwarded to supported QMD query paths"
|
|
320
|
+
},
|
|
321
|
+
"qmdQueryRerankEnabled": {
|
|
322
|
+
"type": "boolean",
|
|
323
|
+
"default": true,
|
|
324
|
+
"description": "When false, ask supported QMD versions to skip the built-in rerank step"
|
|
325
|
+
},
|
|
326
|
+
"qmdIndexName": {
|
|
327
|
+
"type": "string",
|
|
328
|
+
"description": "Optional QMD named index forwarded as qmd --index <name> when supported. Leave unset during upgrades unless existing QMD data already lives in that named index."
|
|
329
|
+
},
|
|
330
|
+
"qmdForceCpu": {
|
|
331
|
+
"type": "boolean",
|
|
332
|
+
"default": false,
|
|
333
|
+
"description": "Set QMD_FORCE_CPU=1 for QMD child processes to bypass GPU probing and run predictably on CPU"
|
|
334
|
+
},
|
|
335
|
+
"qmdGpuBackend": {
|
|
336
|
+
"type": [
|
|
337
|
+
"string",
|
|
338
|
+
"boolean"
|
|
339
|
+
],
|
|
340
|
+
"enum": [
|
|
341
|
+
"auto",
|
|
342
|
+
"metal",
|
|
343
|
+
"cuda",
|
|
344
|
+
"vulkan",
|
|
345
|
+
"false",
|
|
346
|
+
false
|
|
347
|
+
],
|
|
348
|
+
"description": "Optional QMD_LLAMA_GPU override for QMD child processes"
|
|
349
|
+
},
|
|
350
|
+
"qmdEmbedParallelism": {
|
|
351
|
+
"type": "number",
|
|
352
|
+
"minimum": 1,
|
|
353
|
+
"maximum": 8,
|
|
354
|
+
"description": "Optional QMD_EMBED_PARALLELISM override, clamped to 1-8"
|
|
355
|
+
},
|
|
356
|
+
"qmdEmbedModel": {
|
|
357
|
+
"type": "string",
|
|
358
|
+
"description": "Optional QMD_EMBED_MODEL override"
|
|
359
|
+
},
|
|
360
|
+
"qmdRerankModel": {
|
|
361
|
+
"type": "string",
|
|
362
|
+
"description": "Optional QMD_RERANK_MODEL override"
|
|
363
|
+
},
|
|
364
|
+
"qmdGenerateModel": {
|
|
365
|
+
"type": "string",
|
|
366
|
+
"description": "Optional QMD_GENERATE_MODEL override"
|
|
367
|
+
},
|
|
291
368
|
"embeddingFallbackEnabled": {
|
|
292
369
|
"type": "boolean",
|
|
293
370
|
"default": true,
|
|
@@ -303,6 +380,10 @@
|
|
|
303
380
|
"default": "auto",
|
|
304
381
|
"description": "Embedding provider selection for semantic fallback"
|
|
305
382
|
},
|
|
383
|
+
"embeddingFallbackModel": {
|
|
384
|
+
"type": "string",
|
|
385
|
+
"description": "Optional model identifier for embedding fallback requests. Defaults to localLlmModel for legacy installs."
|
|
386
|
+
},
|
|
306
387
|
"qmdPath": {
|
|
307
388
|
"type": "string",
|
|
308
389
|
"description": "Optional absolute path to qmd binary (bypasses PATH/fallback discovery)"
|
|
@@ -4869,6 +4950,66 @@
|
|
|
4869
4950
|
"advanced": true,
|
|
4870
4951
|
"help": "Enable automated cold-tier backfill runs for parity repair"
|
|
4871
4952
|
},
|
|
4953
|
+
"qmdSupportedVersion": {
|
|
4954
|
+
"label": "QMD Supported Version",
|
|
4955
|
+
"advanced": true,
|
|
4956
|
+
"placeholder": "2.5.1"
|
|
4957
|
+
},
|
|
4958
|
+
"qmdAutoUpgradeEnabled": {
|
|
4959
|
+
"label": "QMD Auto Upgrade",
|
|
4960
|
+
"advanced": true,
|
|
4961
|
+
"help": "Upgrade PATH/fallback QMD installs to the supported version; explicit qmdPath installs are skipped"
|
|
4962
|
+
},
|
|
4963
|
+
"qmdAutoUpgradeCheckIntervalMs": {
|
|
4964
|
+
"label": "QMD Auto Upgrade Interval",
|
|
4965
|
+
"advanced": true,
|
|
4966
|
+
"placeholder": "86400000"
|
|
4967
|
+
},
|
|
4968
|
+
"qmdChunkStrategy": {
|
|
4969
|
+
"label": "QMD Chunk Strategy",
|
|
4970
|
+
"advanced": true,
|
|
4971
|
+
"placeholder": "auto"
|
|
4972
|
+
},
|
|
4973
|
+
"qmdCandidateLimit": {
|
|
4974
|
+
"label": "QMD Candidate Limit",
|
|
4975
|
+
"advanced": true
|
|
4976
|
+
},
|
|
4977
|
+
"qmdQueryRerankEnabled": {
|
|
4978
|
+
"label": "QMD Query Rerank",
|
|
4979
|
+
"advanced": true
|
|
4980
|
+
},
|
|
4981
|
+
"qmdIndexName": {
|
|
4982
|
+
"label": "QMD Index Name",
|
|
4983
|
+
"advanced": true,
|
|
4984
|
+
"help": "Use a named QMD index when QMD 2.5+ supports --index. Changing this selects a different SQLite DB; keep it unset for existing default-index installs."
|
|
4985
|
+
},
|
|
4986
|
+
"qmdForceCpu": {
|
|
4987
|
+
"label": "QMD Force CPU",
|
|
4988
|
+
"advanced": true,
|
|
4989
|
+
"help": "Set QMD_FORCE_CPU=1 for QMD child processes"
|
|
4990
|
+
},
|
|
4991
|
+
"qmdGpuBackend": {
|
|
4992
|
+
"label": "QMD GPU Backend",
|
|
4993
|
+
"advanced": true,
|
|
4994
|
+
"placeholder": "metal"
|
|
4995
|
+
},
|
|
4996
|
+
"qmdEmbedParallelism": {
|
|
4997
|
+
"label": "QMD Embed Parallelism",
|
|
4998
|
+
"advanced": true,
|
|
4999
|
+
"placeholder": "4"
|
|
5000
|
+
},
|
|
5001
|
+
"qmdEmbedModel": {
|
|
5002
|
+
"label": "QMD Embed Model",
|
|
5003
|
+
"advanced": true
|
|
5004
|
+
},
|
|
5005
|
+
"qmdRerankModel": {
|
|
5006
|
+
"label": "QMD Rerank Model",
|
|
5007
|
+
"advanced": true
|
|
5008
|
+
},
|
|
5009
|
+
"qmdGenerateModel": {
|
|
5010
|
+
"label": "QMD Generate Model",
|
|
5011
|
+
"advanced": true
|
|
5012
|
+
},
|
|
4872
5013
|
"embeddingFallbackEnabled": {
|
|
4873
5014
|
"label": "Embedding Fallback",
|
|
4874
5015
|
"help": "Use semantic embeddings when QMD is unavailable or no results are found"
|
|
@@ -4878,6 +5019,12 @@
|
|
|
4878
5019
|
"advanced": true,
|
|
4879
5020
|
"placeholder": "auto"
|
|
4880
5021
|
},
|
|
5022
|
+
"embeddingFallbackModel": {
|
|
5023
|
+
"label": "Embedding Model",
|
|
5024
|
+
"advanced": true,
|
|
5025
|
+
"placeholder": "(defaults to local LLM model)",
|
|
5026
|
+
"help": "Optional embedding model for semantic fallback. Use this when the local chat model and embedding model are different."
|
|
5027
|
+
},
|
|
4881
5028
|
"qmdPath": {
|
|
4882
5029
|
"label": "QMD Path",
|
|
4883
5030
|
"advanced": true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remnic/plugin-openclaw",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.38",
|
|
4
4
|
"description": "OpenClaw adapter for Remnic memory with bundled @remnic/core runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
},
|
|
72
72
|
"dependencies": {
|
|
73
73
|
"openai": "^6.0.0",
|
|
74
|
-
"@remnic/core": "^1.1.
|
|
74
|
+
"@remnic/core": "^1.1.14"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
77
|
"openclaw": ">=2026.5.16-beta.1"
|
package/scripts/faiss_index.py
CHANGED
|
@@ -13,10 +13,12 @@ import os
|
|
|
13
13
|
import subprocess
|
|
14
14
|
import sys
|
|
15
15
|
import time
|
|
16
|
+
import uuid
|
|
16
17
|
from pathlib import Path
|
|
17
18
|
from typing import Any
|
|
18
19
|
|
|
19
20
|
MODEL_CACHE: dict[str, Any] = {}
|
|
21
|
+
LOCK_OWNERS: dict[str, str] = {}
|
|
20
22
|
HASH_EMBED_DIM = 128
|
|
21
23
|
LOCK_TIMEOUT_SECONDS = 10.0
|
|
22
24
|
LOCK_STALE_SECONDS = 120.0
|
|
@@ -372,20 +374,67 @@ def merge_rows(existing: list[dict[str, Any]], updates: list[dict[str, Any]]) ->
|
|
|
372
374
|
return [by_key[row_key] for row_key in order]
|
|
373
375
|
|
|
374
376
|
|
|
375
|
-
def
|
|
377
|
+
def lock_owner_key(lock_path: Path) -> str:
|
|
378
|
+
return str(lock_path.resolve())
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def make_lock_owner_token() -> str:
|
|
382
|
+
return f"{os.getpid()}:{uuid.uuid4().hex}"
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def read_lock_contents(lock_path: Path) -> str | None:
|
|
376
386
|
try:
|
|
377
387
|
raw = lock_path.read_text(encoding="utf-8").strip()
|
|
378
388
|
except Exception:
|
|
379
389
|
return None
|
|
380
390
|
if not raw:
|
|
381
391
|
return None
|
|
392
|
+
return raw
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def read_lock_owner_pid(lock_path: Path) -> int | None:
|
|
396
|
+
raw = read_lock_contents(lock_path)
|
|
397
|
+
if raw is None:
|
|
398
|
+
return None
|
|
399
|
+
pid_raw = raw.split(":", 1)[0].strip()
|
|
382
400
|
try:
|
|
383
|
-
pid = int(
|
|
401
|
+
pid = int(pid_raw)
|
|
384
402
|
except ValueError:
|
|
385
403
|
return None
|
|
386
404
|
return pid if pid > 0 else None
|
|
387
405
|
|
|
388
406
|
|
|
407
|
+
def lock_stat_matches(current_stat: os.stat_result, observed_stat: os.stat_result) -> bool:
|
|
408
|
+
if current_stat.st_size != observed_stat.st_size:
|
|
409
|
+
return False
|
|
410
|
+
if current_stat.st_mtime_ns != observed_stat.st_mtime_ns:
|
|
411
|
+
return False
|
|
412
|
+
if getattr(current_stat, "st_ino", 0) and getattr(observed_stat, "st_ino", 0):
|
|
413
|
+
return current_stat.st_ino == observed_stat.st_ino
|
|
414
|
+
return True
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def unlink_lock_if_unchanged(
|
|
418
|
+
lock_path: Path,
|
|
419
|
+
observed_owner_token: str | None,
|
|
420
|
+
observed_stat: os.stat_result,
|
|
421
|
+
) -> bool:
|
|
422
|
+
try:
|
|
423
|
+
current_stat = lock_path.stat()
|
|
424
|
+
except FileNotFoundError:
|
|
425
|
+
return False
|
|
426
|
+
current_owner_token = read_lock_contents(lock_path)
|
|
427
|
+
if current_owner_token != observed_owner_token:
|
|
428
|
+
return False
|
|
429
|
+
if not lock_stat_matches(current_stat, observed_stat):
|
|
430
|
+
return False
|
|
431
|
+
try:
|
|
432
|
+
lock_path.unlink()
|
|
433
|
+
except FileNotFoundError:
|
|
434
|
+
return False
|
|
435
|
+
return True
|
|
436
|
+
|
|
437
|
+
|
|
389
438
|
def is_process_alive(pid: int) -> bool:
|
|
390
439
|
if pid <= 0:
|
|
391
440
|
return False
|
|
@@ -420,6 +469,7 @@ def is_process_alive(pid: int) -> bool:
|
|
|
420
469
|
|
|
421
470
|
def acquire_lock(index_dir: Path, lock_name: str) -> Path:
|
|
422
471
|
lock_path = index_dir / lock_name
|
|
472
|
+
lock_owner_token = make_lock_owner_token()
|
|
423
473
|
lock_label = lock_name.strip(".")
|
|
424
474
|
if lock_label.endswith(".lock"):
|
|
425
475
|
lock_label = lock_label[: -len(".lock")]
|
|
@@ -430,19 +480,22 @@ def acquire_lock(index_dir: Path, lock_name: str) -> Path:
|
|
|
430
480
|
try:
|
|
431
481
|
fd = os.open(str(lock_path), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
|
432
482
|
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
433
|
-
handle.write(
|
|
483
|
+
handle.write(lock_owner_token)
|
|
484
|
+
LOCK_OWNERS[lock_owner_key(lock_path)] = lock_owner_token
|
|
434
485
|
return lock_path
|
|
435
486
|
except FileExistsError:
|
|
436
487
|
try:
|
|
437
|
-
|
|
488
|
+
observed_stat = lock_path.stat()
|
|
438
489
|
except FileNotFoundError:
|
|
439
490
|
continue
|
|
440
491
|
|
|
492
|
+
age = time.time() - observed_stat.st_mtime
|
|
493
|
+
observed_owner_token = read_lock_contents(lock_path)
|
|
441
494
|
owner_pid = read_lock_owner_pid(lock_path)
|
|
442
495
|
owner_alive = is_process_alive(owner_pid) if owner_pid is not None else False
|
|
443
496
|
|
|
444
497
|
if age > LOCK_STALE_SECONDS and not owner_alive:
|
|
445
|
-
lock_path
|
|
498
|
+
unlink_lock_if_unchanged(lock_path, observed_owner_token, observed_stat)
|
|
446
499
|
continue
|
|
447
500
|
|
|
448
501
|
if time.monotonic() >= deadline:
|
|
@@ -459,7 +512,14 @@ def acquire_writer_lock(index_dir: Path) -> Path:
|
|
|
459
512
|
|
|
460
513
|
|
|
461
514
|
def release_lock(lock_path: Path) -> None:
|
|
462
|
-
|
|
515
|
+
lock_owner_token = LOCK_OWNERS.pop(lock_owner_key(lock_path), None)
|
|
516
|
+
if lock_owner_token is None:
|
|
517
|
+
return
|
|
518
|
+
try:
|
|
519
|
+
observed_stat = lock_path.stat()
|
|
520
|
+
except FileNotFoundError:
|
|
521
|
+
return
|
|
522
|
+
unlink_lock_if_unchanged(lock_path, lock_owner_token, observed_stat)
|
|
463
523
|
|
|
464
524
|
|
|
465
525
|
def release_index_lock(lock_path: Path) -> None:
|