@agfpd/iapeer 0.4.43 → 0.4.44
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/package.json +1 -1
- package/src/identity/index.ts +55 -1
- package/src/registry/index.ts +7 -1
package/package.json
CHANGED
package/src/identity/index.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// wipe description without an explicit new value). Path helpers moved to storage.
|
|
7
7
|
|
|
8
8
|
import { basename, join, resolve } from 'path'
|
|
9
|
-
import { existsSync, readFileSync, realpathSync } from 'fs'
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, rmdirSync, statSync } from 'fs'
|
|
10
10
|
import {
|
|
11
11
|
IAPEER_DIR,
|
|
12
12
|
PEER_PROFILE_FILE,
|
|
@@ -251,10 +251,64 @@ export function readPeerProfile(cwd: string = process.cwd()): PeerProfile | null
|
|
|
251
251
|
* The profile file (basename peer-profile.json) is allowed through
|
|
252
252
|
* storage.writeFileAtomic; only peers-profiles.json is guarded there.
|
|
253
253
|
*/
|
|
254
|
+
const PROFILE_LOCK_WAIT_MS = 2000
|
|
255
|
+
const PROFILE_LOCK_STALE_MS = 10_000
|
|
256
|
+
|
|
257
|
+
/** В31 — serialize the read-merge-write of a peer's local profile so concurrent FOUNDATION writers
|
|
258
|
+
* (verify --fix, the `default-runtime` verb, ensurePeerProfile at re-provision) don't lose each other's
|
|
259
|
+
* owner sections via a read-modify-write race. A sync mkdir-lock: writePeerProfileAtomic is sync and off
|
|
260
|
+
* the delivery hot path (rare operator calls). Best-effort — a stale lock is reclaimed, and on timeout it
|
|
261
|
+
* proceeds WITHOUT the lock so it can never deadlock. CROSS-PACKAGE CAVEAT: telegram-runtime writes this
|
|
262
|
+
* file with its OWN code and does not take this lock; full safety needs it to adopt the same lock (a
|
|
263
|
+
* separate cross-package contract — this closes the foundation-vs-foundation window). */
|
|
264
|
+
function withProfileLockSync<T>(cwd: string, fn: () => T): T {
|
|
265
|
+
const lockDir = join(cwd, '.iapeer', '.peer-profile.lock')
|
|
266
|
+
const deadline = Date.now() + PROFILE_LOCK_WAIT_MS
|
|
267
|
+
let held = false
|
|
268
|
+
for (;;) {
|
|
269
|
+
try {
|
|
270
|
+
mkdirSync(lockDir)
|
|
271
|
+
held = true
|
|
272
|
+
break
|
|
273
|
+
} catch (e) {
|
|
274
|
+
if ((e as NodeJS.ErrnoException).code !== 'EEXIST') break // fs issue → proceed unlocked (best-effort)
|
|
275
|
+
try {
|
|
276
|
+
if (Date.now() - statSync(lockDir).mtimeMs > PROFILE_LOCK_STALE_MS) {
|
|
277
|
+
rmdirSync(lockDir)
|
|
278
|
+
continue // reclaimed a crashed writer's stale lock → retry immediately
|
|
279
|
+
}
|
|
280
|
+
} catch {
|
|
281
|
+
/* the lock vanished between checks — retry */
|
|
282
|
+
}
|
|
283
|
+
if (Date.now() > deadline) break // give up waiting → proceed unlocked (never deadlock)
|
|
284
|
+
try {
|
|
285
|
+
;(globalThis as { Bun?: { sleepSync(ms: number): void } }).Bun?.sleepSync(15)
|
|
286
|
+
} catch {
|
|
287
|
+
/* */
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
try {
|
|
292
|
+
return fn()
|
|
293
|
+
} finally {
|
|
294
|
+
if (held)
|
|
295
|
+
try {
|
|
296
|
+
rmdirSync(lockDir)
|
|
297
|
+
} catch {
|
|
298
|
+
/* */
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
254
303
|
export function writePeerProfileAtomic(cwd: string, profile: PeerProfileWrite): void {
|
|
255
304
|
ensureLocalIapScaffold(cwd)
|
|
256
305
|
const path = peerProfilePath(cwd)
|
|
306
|
+
// В31 — the ENTIRE read-merge-write runs under the per-cwd lock so a concurrent foundation writer's
|
|
307
|
+
// owner sections are never clobbered by a stale-read merge.
|
|
308
|
+
withProfileLockSync(cwd, () => writePeerProfileMerged(path, profile))
|
|
309
|
+
}
|
|
257
310
|
|
|
311
|
+
function writePeerProfileMerged(path: string, profile: PeerProfileWrite): void {
|
|
258
312
|
let existing: Record<string, unknown> = {}
|
|
259
313
|
try {
|
|
260
314
|
const parsed = JSON.parse(readFileSync(path, 'utf8'))
|
package/src/registry/index.ts
CHANGED
|
@@ -490,7 +490,13 @@ export async function upsertPeer(
|
|
|
490
490
|
|
|
491
491
|
const record: PeerRecord = {
|
|
492
492
|
personality: args.personality,
|
|
493
|
-
|
|
493
|
+
// В34 — default_runtime is an H1 field: it INHERITS from existing, it does NOT flip to args.runtime.
|
|
494
|
+
// A re-provision / `init --runtime codex` on an existing claude-default peer ADDS codex to runtimes
|
|
495
|
+
// (below) but must NOT silently make codex the default — that mis-wakes the peer on the wrong
|
|
496
|
+
// runtime until the next reindex heals it (which never runs when addRuntime returns 'already'). The
|
|
497
|
+
// default changes ONLY via the explicit `default-runtime` verb (writes the local profile +
|
|
498
|
+
// reindexFromLocals). A brand-new peer (no existing) naturally takes args.runtime as its default.
|
|
499
|
+
runtime: existing ? existing.runtime : runtime,
|
|
494
500
|
runtimes,
|
|
495
501
|
description,
|
|
496
502
|
intelligence,
|