@agfpd/iapeer 0.4.30 → 0.4.31
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
CHANGED
|
@@ -11,12 +11,24 @@ import { openSync, readSync, closeSync, statSync } from 'fs'
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Content-timestamp (epoch ms) of the LAST jsonl entry carrying a `timestamp` field, read from the
|
|
14
|
-
* file's TAIL
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
14
|
+
* file's TAIL. Metadata records without a `timestamp` (claude `mode` / `ai-title` / `bridge-session`)
|
|
15
|
+
* are skipped. Returns null only when the file is unreadable / empty, or genuinely carries NO
|
|
16
|
+
* timestamped entry anywhere (the caller then floors at wokeAt).
|
|
17
|
+
*
|
|
18
|
+
* ADAPTIVE WINDOW (the giant-tail fix). The reader starts at the cheap `tailBytes` window and GROWS it
|
|
19
|
+
* (doubling, capped at file size) until a complete timestamped entry is captured. A single transcript
|
|
20
|
+
* record can dwarf the default window — a browser peer appends base64 screenshots, observed up to
|
|
21
|
+
* 368 KB (5.6× the 64 KB default). When such a record is the tail, a FIXED window holds only a fragment
|
|
22
|
+
* of one line: no complete JSON → the old code returned null, the caller floored at wokeAt, and a LIVE
|
|
23
|
+
* working session was idle-reaped (incident: mrbrowser killed mid-task, last turn 7 s before the reap).
|
|
24
|
+
* Growing guarantees we eventually read the full last record (worst case: the entire file), so a present
|
|
25
|
+
* timestamp is NEVER missed. The default window already covers the common case in one read; growth only
|
|
26
|
+
* kicks in for oversized tail records, and even then is bounded (≈log2(fileSize/tailBytes) reads).
|
|
27
|
+
*
|
|
28
|
+
* A partial LEADING line is never trusted: once the window starts past byte 0, the first split segment
|
|
29
|
+
* may be the truncated tail of an earlier record (which could mis-parse as valid JSON), so we scan only
|
|
30
|
+
* the newline-bounded segments after it. Reaching byte 0 makes every segment complete, including the
|
|
31
|
+
* first — and terminates the growth.
|
|
20
32
|
*/
|
|
21
33
|
export function lastTimestampedEntryMs(path: string, tailBytes = 65536): number | null {
|
|
22
34
|
let fd: number
|
|
@@ -27,35 +39,45 @@ export function lastTimestampedEntryMs(path: string, tailBytes = 65536): number
|
|
|
27
39
|
}
|
|
28
40
|
try {
|
|
29
41
|
const size = statSync(path).size
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
42
|
-
const line = lines[i].trim()
|
|
43
|
-
if (!line) continue
|
|
44
|
-
let obj: unknown
|
|
45
|
-
try {
|
|
46
|
-
obj = JSON.parse(line)
|
|
47
|
-
} catch {
|
|
48
|
-
continue // partial leading line / non-json → skip
|
|
42
|
+
if (size <= 0) return null
|
|
43
|
+
let window = Math.max(tailBytes, 1)
|
|
44
|
+
for (;;) {
|
|
45
|
+
const start = Math.max(0, size - window)
|
|
46
|
+
const len = size - start
|
|
47
|
+
const buf = Buffer.allocUnsafe(len)
|
|
48
|
+
let read = 0
|
|
49
|
+
while (read < len) {
|
|
50
|
+
const n = readSync(fd, buf, read, len - read, start + read)
|
|
51
|
+
if (n <= 0) break
|
|
52
|
+
read += n
|
|
49
53
|
}
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
const atFileStart = start === 0
|
|
55
|
+
const lines = buf.subarray(0, read).toString('utf8').split(/\r?\n/)
|
|
56
|
+
// Segment 0 is a complete record ONLY when the window reaches byte 0; otherwise it is the possibly
|
|
57
|
+
// truncated tail of an earlier record and must not be parsed (`floor` skips it).
|
|
58
|
+
const floor = atFileStart ? 0 : 1
|
|
59
|
+
for (let i = lines.length - 1; i >= floor; i--) {
|
|
60
|
+
const line = lines[i].trim()
|
|
61
|
+
if (!line) continue
|
|
62
|
+
let obj: unknown
|
|
63
|
+
try {
|
|
64
|
+
obj = JSON.parse(line)
|
|
65
|
+
} catch {
|
|
66
|
+
continue // metadata fragment / non-json → skip
|
|
67
|
+
}
|
|
68
|
+
const ts = (obj as { timestamp?: unknown })?.timestamp
|
|
69
|
+
if (typeof ts === 'string') {
|
|
70
|
+
const ms = Date.parse(ts)
|
|
71
|
+
if (!Number.isNaN(ms)) return ms
|
|
72
|
+
} else if (typeof ts === 'number' && Number.isFinite(ts)) {
|
|
73
|
+
return ts
|
|
74
|
+
}
|
|
56
75
|
}
|
|
76
|
+
// No complete timestamped entry in this window. If the whole file is read, there genuinely is none.
|
|
77
|
+
// Otherwise the tail record outgrows the window → grow and retry (terminates: window hits size → start 0).
|
|
78
|
+
if (atFileStart) return null
|
|
79
|
+
window *= 2
|
|
57
80
|
}
|
|
58
|
-
return null
|
|
59
81
|
} catch {
|
|
60
82
|
return null
|
|
61
83
|
} finally {
|