@llblab/pi-telegram 0.27.10 → 0.27.12
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/AGENTS.md +2 -2
- package/CHANGELOG.md +10 -0
- package/docs/multi-instance-bus.md +1 -1
- package/lib/bus-leader.ts +21 -6
- package/lib/locks.ts +17 -19
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -78,7 +78,7 @@ The core product loop is mobile continuation: start or supervise work in the ter
|
|
|
78
78
|
- The bridge is session-local, paired to one allowed Telegram user, and owns a local queue aligned with Pi lifecycle hooks
|
|
79
79
|
- The Telegram lock owns external Telegram transport, not local Pi execution. Local extension state and queue runtime are per Pi instance: losing exact `/telegram-connect` ownership must not clear accepted queue state or stop local Pi dispatch, but preview, final delivery, attachments, menus, and other Bot API mutations fail closed once another runtime owns transport. A replacement owner must never receive stale direct mutations merely to preserve old-runtime Telegram output. Bus leader lock entries carry a stable `leaderEpoch` minted on acquisition and preserved across heartbeat refreshes; leader-owned Thread Reconciler cleanup/provisioning actions stamp that epoch and skip destructive side effects if current leadership differs.
|
|
80
80
|
- Telegram transport ownership lives only in the extension-local `tmp/telegram/owners.json` store under `default` or named profile slots. Every mutation must serialize the complete cross-process read/check/write transaction through `owners.json.transaction` and fail closed on contention; atomic payload replacement alone does not make ownership linearizable. Refresh, release, takeover, and irreversible leader work additionally fence against the exact acquired owner and leader epoch. In classic/private-chat mode, transport ownership is the whole bridge ownership and `/telegram-connect` from another live Pi offers classic takeover confirmation. In Threaded Mode, transport ownership is the bus leader role and another live Pi registers as follower instead of offering takeover while the leader is live. If follower registration is unavailable, fall back to classic takeover.
|
|
81
|
-
- Polling checks exact `owners.json` ownership every second but refreshes the durable owner heartbeat only every two seconds; an eight-second stale threshold preserves bounded recovery headroom without turning every ownership check into an atomic disk rewrite. Ownership transactions publish a fully initialized private guard directory containing one exact generation-specific `owner.<generation>.json` through same-parent atomic rename. The owner filename and payload generation must match so stale observation cannot rename replacement metadata. Release verifies the owner and atomically renames the stable guard away before cleanup. Directory recovery first claims owner metadata inside the still-occupied guard so a crashed recoverer can itself be replaced without exposing the stable path; transient guard moves and rollback
|
|
81
|
+
- Polling checks exact `owners.json` ownership every second but refreshes the durable owner heartbeat only every two seconds; an eight-second stale threshold preserves bounded recovery headroom without turning every ownership check into an atomic disk rewrite. Ownership transactions publish a fully initialized private guard directory containing one exact generation-specific `owner.<generation>.json` through same-parent atomic rename. The owner filename and payload generation must match so stale observation cannot rename replacement metadata. Release verifies the owner and atomically renames the stable guard away before cleanup. Directory recovery first claims owner metadata inside the still-occupied guard so a crashed recoverer can itself be replaced without exposing the stable path; transient guard publication, recovery moves, and rollback all retry even when a competing path disappears before error classification, while inactive process-global claim generations remain recoverable after failed restoration. If secondary recovery cleanup fails after publishing a replacement main guard, release that exact replacement before propagating the failure. Guards must not depend on hard links, direct file rename-overwrite, incomplete create-then-write state, or age-only recovery; malformed or unsupported guards fail closed during ordinary mutation.
|
|
82
82
|
- `/telegram-connect` is the bounded liveness-recovery boundary for unclean-shutdown corruption in disposable `tmp/telegram` state. It may quarantine only classifier-approved malformed `owners.json`, active-profile `state*.json`, and `owners.json.transaction` after serializing recoverers and revalidating under ownership fencing; it must preserve `telegram.json`, diagnostics, unrelated artifacts, and every verifiable live owner/transaction holder. Stale heartbeats older than eight seconds do not gain protection through PID reuse. Require confirmed local polling suspension before quarantine mutation; a suspension failure blocks recovery, while an ownership-release failure may continue only into final guarded live-owner revalidation. Use the established bounded Windows sharing-violation rename retries. Retry polling exactly once after recovery, preserve unrelated startup errors, and replace blocked/repeated recovery failure with one explicit restart instruction rather than broad `tmp/` deletion or an automatic loop.
|
|
83
83
|
- Telegram profiles are parallel bot/runtime realities, not merely alternate credentials. Persist every bot/session field (`botToken`, `botUsername`, `botId`, `allowedUserId`, `lastUpdateId`) under `telegram.json` `profiles.<name>`, including `profiles.default`; keep shared handlers and assistant/voice/time/thread-lifecycle settings top-level. Bare and explicit `default` setup/connect commands address the same profile. Legacy root identity may normalize atomically only when unambiguous; conflicts fail closed without mutation. Any owner slot, IPC endpoint, state snapshot, diagnostics log, thread/bus binding, runtime cache, ownership map, or operator-visible status surface that could mix bot identities must be deliberately classified as global, session-local, or profile-scoped. Use the `default` owner slot for the default profile and validated profile names for named slots; give named profiles isolated observable surfaces unless there is an explicit top-level shared-config reason not to.
|
|
84
84
|
- Telegram private-chat Threaded Mode is the public switch for the local multi-instance bus. There is no separate public `telegram.json` bus switch: classic/private-chat singleton DM behavior is the base product mode, and when Telegram reports Threaded Mode available for the bot the bridge enables leader/follower bus behavior. If Telegram reports Threaded Mode unavailable or thread operations prove it unavailable, the bridge returns to classic singleton behavior as a first-class mode. During a live Threaded Mode downgrade, the current transport leader keeps the singleton classic polling role and followers disconnect instead of taking over or offering takeover while active thread bindings prove this is a downgrade from a live bus organism. Do not inject slot/thread rename guidance in classic mode. Blocked instances should not silently register as followers unless the effective runtime mode is Threaded Mode.
|
|
@@ -91,7 +91,7 @@ The core product loop is mobile continuation: start or supervise work in the ter
|
|
|
91
91
|
- Telegram extension work should not hold Pi's core agent lifecycle hostage once Pi has produced the semantic result. For Telegram-originated turns, final Telegram delivery, attachment upload, and transport cleanup are extension-owned side effects; schedule them off the critical `agent_end` path when ordering can still be preserved, record failures as runtime events, and keep dispatch of the next Telegram turn behind the delivery task when needed to avoid interleaving previews/finals. Treat a low-level error `agent_end` as retryable while its Telegram turn remains active; only a later successful `agent_end` or Pi's `agent_settled` boundary may finalize and release that turn. Public activity handlers run through isolated asynchronous per-handler queues; lifecycle hooks enqueue normalized events and never await consumer work. Core assistant-output projection always delivers each completed `intermediate` Telegram-originated text segment once to its immutable turn target before final delivery; final and terminal-partial segments remain with settlement to prevent duplication. Proactive push defaults enabled for local/autonomous work and requires explicit `assistant.proactivePush: false` to opt out of only that source class. Bind every admitted block to the exact target, profile/token transport stamp, direct leader epoch or follower registration generation, and session generation. Token deltas, hidden reasoning, tool traffic, unknown sources, and stale authority never enter projection.
|
|
92
92
|
- Core thinking/tool visibility is controlled by file-backed `assistant.activity: "quiet" | "thinking" | "tools" | "verbose"`; every Pi instance reloads the shared config at `agent-start` before admitting activity so process-local cache cannot broaden the selected class. It defaults to `verbose` when absent and remains separate from proactive public prose and assistant Markdown: provider-exposed thinking uses only a durable ordinary HTML expandable blockquote, while completed executed tools use native Rich Messages whose closed `<Tool>: <status>` root details replace snake-case separators with spaces, title-case each word, and preserve an uppercase initial two- or three-letter repeated prefix per word, then contain an open-by-default `arguments` child plus closed `update`, `result`, or `error` children with lowercase monospaced labels and JSON pre blocks; treat each label as one quote-free outer JSON key and add no icon, list marker, or heading emphasis. Thinking renders inline Markdown as Telegram HTML without spending a separate header row; `verbose` enables both classes, and every activity send/edit disables Telegram link previews while technical evidence also neutralizes HTTP(S) auto-link detection as a client-safe fallback. Keep it compact by default, exact-target/transport-generation fenced, start-order preserving, boundary-aware, memory/transport bounded, and non-blocking; final delivery waits for admitted technical activity only inside the extension-owned background task, and session replacement abandons queued old-generation work without waiting on old transport. Read legacy `assistant.activityVerbosity` only when `assistant.activity` is absent and remove it on the next Settings write. Never invent unavailable provider-private thinking or replay a possibly committed technical message.
|
|
93
93
|
- Telegram runtime state should be treated as event-driven reconciliation of extension assumptions against observable Telegram signals, not as a full Telegram bot read-model and not as a reason to query Telegram on every action. Keep a local assumption model for bot identity/capabilities, pairing assumptions, thread support, known thread lifecycle, bound instance targets, reservations, and transport health. Invalidate and reconcile the relevant slice on meaningful events: startup/reload, lifecycle service messages, stale-send/API errors, setup/pairing changes, follower register/prune, explicit status/debug requests, and bounded low-frequency leader health ticks. `lib/sync.ts` owns sync slices, observation intake, invalidation triggers, status/debug freshness, and reconciliation scheduling; it must not promise complete bot-state mirroring because Bot API lacks a complete topic listing surface. `lib/thread-reconciler.ts` owns pure Threaded Mode lifecycle planning and should become the only policy authority for destructive thread cleanup decisions. `threads` owns current Telegram UI thread/tab binding primitives and thread-name helpers mapped to Bot API topic transport, `status` renders projections, and `index.ts` only wires ports. `tmp/telegram/logs.jsonl` is a session-local redacted runtime evidence stream for race debugging and resets on extension start / runtime scope changes; it is diagnostics only, not routing/provisioning authority. `state.json` should be an observable/debug snapshot aligned with `/telegram-status` (extension runtime, bot capabilities including `bot.lastSlot`, runtime role, live roster projection, reservations, diagnostics), not routing/provisioning authority. Because every process on one Telegram profile shares this file, only the active transport lock owner may persist it; followers read current state and gain write authority only after promotion. Status-only writes must refresh disk-backed bindings before serialization so a stale loaded snapshot cannot erase newer leader state. Live bus/runtime state is authoritative; file reservations and fresh capability observations may be startup hints/collision guards only; stale snapshots must re-probe before suppressing bus/topic behavior. Do not persist stale/offline/failed thread target history as source of truth.
|
|
94
|
-
- Thread bindings are bidirectional. From the Pi-instance side, an instance knows and preserves its target, slot, thread name, lifecycle state, and direct-delivery defaults. From the Telegram-client/bot side, the bridge observes thread creation/close/reopen/stale-send signals and reconciles them into instance binding state. Lifecycle transitions that affect operator understanding should be Telegram-visible when a live transport remains: every
|
|
94
|
+
- Thread bindings are bidirectional. From the Pi-instance side, an instance knows and preserves its target, slot, thread name, lifecycle state, and direct-delivery defaults. From the Telegram-client/bot side, the bridge observes thread creation/close/reopen/stale-send signals and reconciles them into instance binding state. Lifecycle transitions that affect operator understanding should be Telegram-visible when a live transport remains: every fresh follower registration gets a compact connected notice in the assigned thread, while an exact immediate session handoff verifies the carried thread with a non-message chat action so it cannot duplicate that notice; heartbeat pruning stays silent because it is only liveness suspicion. These notices should use the instance thread name when known and fall back to the slot letter only while the thread is still unnamed. New unknown or command-created temporary threads receive the complete forward plus replace/restore control set and remain preserved until the user chooses one. Successful forward deletes the chooser and temporary thread; successful replace/restore always deletes the chooser, rebinds the temporary thread, and deletes only the replaced old thread. Any destructive cleanup must go through `thread-reconciler`. Unknown `forum_topic_created` service events are observations, not destructive cleanup proof.
|
|
95
95
|
- The multi-instance bus uses private bot DMs with Telegram private-chat Threaded Mode enabled for the bot. No supergroup, group, or forum configuration is needed; the thread chat is always the private chat with the paired owner. Threaded capability checks must use bot/private-chat evidence such as `getMe.has_topics_enabled`, incoming `message_thread_id`, and topic operation success/failure; do not use group chat metadata as the control-plane truth for Telegram private-chat Threaded Mode.
|
|
96
96
|
- Target-scoped behavior must preserve `{ chatId, threadId? }` through inbound messages/edits/callbacks/reactions, thread lifecycle service messages, media and split-text grouping, queue mutations, active-turn cleanup, previews, reply deduplication, menus, sections, generated buttons, voice, attachments, and direct delivery. Threaded Mode replies must keep Telegram's reply affordance to the triggering message just like classic DM mode. In Telegram private-chat Threaded Mode, mobile Telegram has been live-verified to render `sendMessage` replies that include top-level `message_thread_id` plus same-chat `reply_parameters.message_id`; Telegram Desktop may fail to show the visual reply header for the same correct payload, so do not treat Desktop-only absence as bridge failure without mobile/payload evidence.
|
|
97
97
|
- Local bus IPC is a local-only trust boundary: private socket directories/files are necessary but not the whole authorization model; never let arbitrary local processes inject prompts, callbacks, API sends, or artifact routes. Ordinary follower Bot API sends remain assigned-target scoped; runtime-marked `telegram_message` cross-target sends may address only a different thread in the same paired chat, and the leader must strip that internal authorization marker before Telegram transport.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.27.12: Windows Log Serialization Hotfix
|
|
4
|
+
|
|
5
|
+
- `Windows Log Serialization`: File-transaction guard publication now retries transient `EPERM`, `EBUSY`, and `EACCES` failures even when the competing guard disappears before the error is classified. Impact: concurrent runtime JSONL writers no longer drop one process's complete batch during the narrow Windows publication race, while permanent failures remain bounded and fail closed.
|
|
6
|
+
- `Transaction Coverage`: A deterministic regression injects a vanished-path `EPERM` during guard publication and requires the transaction operation to run exactly once after retry. Impact: cross-process diagnostics retain all lines without weakening guard ownership or recovery fencing.
|
|
7
|
+
|
|
8
|
+
## 0.27.11: Follower Connection Notice Hotfix
|
|
9
|
+
|
|
10
|
+
- `Follower Connection Notice`: An exact immediate follower session handoff now verifies its carried thread with a target-scoped chat action instead of sending the connected notice again. Impact: fresh follower startup still announces the assigned instance once, while the automatic session replacement no longer produces duplicate `Instance <name> connected` messages.
|
|
11
|
+
- `Handoff Coverage`: A regression now exercises fresh thread provisioning followed by exact session handoff and requires one visible connected notice plus a non-message visibility probe. Impact: target validation and stale-thread recovery remain intact without reintroducing duplicate chat bubbles.
|
|
12
|
+
|
|
3
13
|
## 0.27.10: Leader Thread Rebind Hotfix
|
|
4
14
|
|
|
5
15
|
- `Live Leader Target`: Reclaiming or replacing the current leader thread now updates the process-local leader identity immediately after the durable binding commits. Impact: live thread-name resolution no longer returns the deleted previous leader target, so another instance can address the rebound leader without receiving `message thread not found`.
|
|
@@ -388,7 +388,7 @@ All files containing routing, chat ids, thread ids, or process details use priva
|
|
|
388
388
|
- A missed heartbeat does not delete, close, mark offline, or send a disconnected notice for the follower's Telegram thread binding because the common cause may be leader reload, IPC handoff, or transient reconnect rather than a dead follower.
|
|
389
389
|
- Followers treat rejected/missing heartbeat acknowledgements as registration loss: retain the last known target locally, clear registered truth, try to re-register with the current leader, wait a short leader-reload grace window, and retry. They promote only after the exact leader lease becomes stale or inactive; a live owner with an unreachable endpoint leaves the follower disconnected/retrying rather than creating a competing poller.
|
|
390
390
|
- Persisted current manual-follower bindings survive abrupt process absence as restoration hints when Thread cleanup is disabled. When enabled, graceful Pi quit requests exact-generation teardown before lifecycle suspension; if that envelope is missed, stale pruning may delete only after the leader's OS confirms the exact registered PID has exited.
|
|
391
|
-
- Fresh registration sends
|
|
391
|
+
- Fresh registration sends one compact connected notice in the assigned thread. An exact immediate session handoff uses a target-scoped `sendChatAction` as its synchronous visibility probe, avoiding a duplicate notice while retaining stale/ambiguous recovery; other cross-session restoration keeps the connected notice as its probe.
|
|
392
392
|
- Registration requires a present generation, and explicit disconnect requires that same exact live generation. Leader-side registration and disconnect mutations serialize per durable follower profile across old and replacement runtime instance IDs, so a replacement registration cannot overtake awaited destructive cleanup and an old disconnect cannot remove its successor's routing authority.
|
|
393
393
|
- Successful forwarded updates and follower-originated API calls refresh liveness, so active followers are not pruned only because the interval heartbeat tick lagged.
|
|
394
394
|
- Destructive follower thread teardown belongs to confirmed `/telegram-disconnect`, graceful Pi quit, or confirmed reconciliation actions, not generic heartbeat pruning. Manual disconnect retains its destructive confirmation; quit performs the same action without prompting when Thread cleanup is enabled (default). Confirmed leader/follower teardown first persists an exact target/runtime-generation cleanup intent. The active leader attempts deletion under its current epoch; interruption preserves the intent so that leader or a successor can replay it under current authority, and confirmed deletion removes the binding plus intent in the same persisted state transition. If the graceful request is missed, stale heartbeat plus OS-confirmed absence of the exact registered PID may authorize the same cleanup while enabled; this action serializes ahead of replacement registration. Disabled cleanup, silence, heartbeat expiry alone, IPC/auth failure, and live or unknown process liveness remain non-destructive. Incomplete cleanup preserves durable intent for retry. A promoted leader uses its current owned leader epoch even when the inherited record still carries a historical `manual-follower` owner label.
|
package/lib/bus-leader.ts
CHANGED
|
@@ -523,6 +523,12 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
523
523
|
}
|
|
524
524
|
const probeRequiredRecord =
|
|
525
525
|
reconnectRecord?.status === "probe-required";
|
|
526
|
+
const exactSessionHandoff =
|
|
527
|
+
crossSessionReuse &&
|
|
528
|
+
!!requestedTarget &&
|
|
529
|
+
registration.previousInstanceId === reconnectRecord?.instanceId &&
|
|
530
|
+
requestedTarget.chatId === reconnectRecord.target.chatId &&
|
|
531
|
+
requestedTarget.threadId === reconnectRecord.target.threadId;
|
|
526
532
|
const requiresVisibilityProbe =
|
|
527
533
|
crossSessionReuse ||
|
|
528
534
|
probeRequiredRecord ||
|
|
@@ -538,12 +544,21 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
538
544
|
: undefined;
|
|
539
545
|
if (requiresVisibilityProbe && connectedAnnouncement) {
|
|
540
546
|
try {
|
|
541
|
-
await deps.callApi(
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
+
await deps.callApi(
|
|
548
|
+
exactSessionHandoff ? "sendChatAction" : "sendMessage",
|
|
549
|
+
exactSessionHandoff
|
|
550
|
+
? {
|
|
551
|
+
chat_id: connectedAnnouncement.target.chatId,
|
|
552
|
+
message_thread_id: connectedAnnouncement.target.threadId,
|
|
553
|
+
action: "typing",
|
|
554
|
+
}
|
|
555
|
+
: {
|
|
556
|
+
chat_id: connectedAnnouncement.target.chatId,
|
|
557
|
+
message_thread_id: connectedAnnouncement.target.threadId,
|
|
558
|
+
text: connectedAnnouncement.text,
|
|
559
|
+
parse_mode: connectedAnnouncement.parseMode,
|
|
560
|
+
},
|
|
561
|
+
);
|
|
547
562
|
if (recoverableTarget || probeRequiredRecord) {
|
|
548
563
|
const activatedRecord = deps.topicTargetStore.upsert({
|
|
549
564
|
...result.record,
|
package/lib/locks.ts
CHANGED
|
@@ -276,20 +276,15 @@ function createLockTransactionContentionError(path: string): Error {
|
|
|
276
276
|
);
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
-
function isLockTransactionContentionError(
|
|
280
|
-
error: unknown,
|
|
281
|
-
path: string,
|
|
282
|
-
): boolean {
|
|
279
|
+
function isLockTransactionContentionError(error: unknown): boolean {
|
|
283
280
|
const code = (error as { code?: unknown })?.code;
|
|
284
|
-
|
|
281
|
+
return (
|
|
285
282
|
code === "EEXIST" ||
|
|
286
283
|
code === "ENOTEMPTY" ||
|
|
287
284
|
code === "ENOTDIR" ||
|
|
288
|
-
code === "EISDIR"
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
return existsSync(path) && (code === "EPERM" || code === "EACCES");
|
|
285
|
+
code === "EISDIR" ||
|
|
286
|
+
isRetryableLockWriteError(error)
|
|
287
|
+
);
|
|
293
288
|
}
|
|
294
289
|
|
|
295
290
|
function removeLockTransactionGuard(path: string): void {
|
|
@@ -298,6 +293,7 @@ function removeLockTransactionGuard(path: string): void {
|
|
|
298
293
|
|
|
299
294
|
function createLockTransactionGuard(
|
|
300
295
|
path: string,
|
|
296
|
+
options: TelegramFileTransactionOptions = {},
|
|
301
297
|
): TelegramLockTransactionOwner {
|
|
302
298
|
const owner: TelegramLockTransactionOwner = {
|
|
303
299
|
pid: process.pid,
|
|
@@ -313,7 +309,7 @@ function createLockTransactionGuard(
|
|
|
313
309
|
{ encoding: "utf8", flag: "wx", mode: 0o600 },
|
|
314
310
|
);
|
|
315
311
|
if (existsSync(path)) throw createLockTransactionContentionError(path);
|
|
316
|
-
renameSync(stagedPath, path);
|
|
312
|
+
(options.publishRename ?? renameSync)(stagedPath, path);
|
|
317
313
|
return owner;
|
|
318
314
|
} finally {
|
|
319
315
|
try {
|
|
@@ -368,6 +364,7 @@ type TelegramTransactionGlobal = typeof globalThis & {
|
|
|
368
364
|
|
|
369
365
|
export interface TelegramFileTransactionOptions {
|
|
370
366
|
recoveryRename?: typeof renameSync;
|
|
367
|
+
publishRename?: typeof renameSync;
|
|
371
368
|
attempts?: number;
|
|
372
369
|
retryDelayMs?: number;
|
|
373
370
|
}
|
|
@@ -465,9 +462,9 @@ function acquireRecoverableDirectoryGuard(
|
|
|
465
462
|
): TelegramLockTransactionOwner | undefined {
|
|
466
463
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
467
464
|
try {
|
|
468
|
-
return createLockTransactionGuard(path);
|
|
465
|
+
return createLockTransactionGuard(path, options);
|
|
469
466
|
} catch (error) {
|
|
470
|
-
if (!isLockTransactionContentionError(error
|
|
467
|
+
if (!isLockTransactionContentionError(error)) throw error;
|
|
471
468
|
if (!reclaimAbandonedDirectoryGuard(path, options)) return undefined;
|
|
472
469
|
}
|
|
473
470
|
}
|
|
@@ -528,11 +525,12 @@ function acquireLegacyRecoveryGuard(
|
|
|
528
525
|
|
|
529
526
|
function createRecoveredLockTransactionGuard(
|
|
530
527
|
path: string,
|
|
528
|
+
options: TelegramFileTransactionOptions = {},
|
|
531
529
|
): TelegramLockTransactionOwner | undefined {
|
|
532
530
|
try {
|
|
533
|
-
return createLockTransactionGuard(path);
|
|
531
|
+
return createLockTransactionGuard(path, options);
|
|
534
532
|
} catch (error) {
|
|
535
|
-
if (isLockTransactionContentionError(error
|
|
533
|
+
if (isLockTransactionContentionError(error)) return undefined;
|
|
536
534
|
throw error;
|
|
537
535
|
}
|
|
538
536
|
}
|
|
@@ -550,7 +548,7 @@ function recoverAbandonedLockTransaction(
|
|
|
550
548
|
}
|
|
551
549
|
if (isDirectory) {
|
|
552
550
|
if (!reclaimAbandonedDirectoryGuard(path, options)) return undefined;
|
|
553
|
-
const recoveredOwner = createRecoveredLockTransactionGuard(path);
|
|
551
|
+
const recoveredOwner = createRecoveredLockTransactionGuard(path, options);
|
|
554
552
|
try {
|
|
555
553
|
reclaimAbandonedDirectoryGuard(`${path}.recovery`, options);
|
|
556
554
|
return recoveredOwner;
|
|
@@ -584,7 +582,7 @@ function recoverAbandonedLockTransaction(
|
|
|
584
582
|
} catch {
|
|
585
583
|
/* stale debris cannot retain transaction authority */
|
|
586
584
|
}
|
|
587
|
-
recoveredOwner = createRecoveredLockTransactionGuard(path);
|
|
585
|
+
recoveredOwner = createRecoveredLockTransactionGuard(path, options);
|
|
588
586
|
return recoveredOwner;
|
|
589
587
|
} finally {
|
|
590
588
|
try {
|
|
@@ -617,9 +615,9 @@ function acquireLockTransaction(
|
|
|
617
615
|
mkdirSync(dirname(path), { recursive: true });
|
|
618
616
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
619
617
|
try {
|
|
620
|
-
return createLockTransactionGuard(path);
|
|
618
|
+
return createLockTransactionGuard(path, options);
|
|
621
619
|
} catch (error) {
|
|
622
|
-
if (!isLockTransactionContentionError(error
|
|
620
|
+
if (!isLockTransactionContentionError(error)) throw error;
|
|
623
621
|
const recoveredOwner = recoverAbandonedLockTransaction(path, options);
|
|
624
622
|
if (recoveredOwner !== undefined) return recoveredOwner;
|
|
625
623
|
if (attempt === attempts - 1) {
|