poi-plugin-mcp 0.2.23 → 0.2.25
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/lib/poi-auto-interaction-recorder.js +23 -4
- package/lib/poi-interaction-recorder.js +72 -10
- package/lib/poi-webview-runtime.js +1238 -1181
- package/package.json +1 -1
|
@@ -13,6 +13,13 @@ function createPoiAutoInteractionRecorder(options = {}) {
|
|
|
13
13
|
const remote = loadElectronRemote()
|
|
14
14
|
return remote.webContents.fromId(webContentsId)
|
|
15
15
|
})
|
|
16
|
+
// Remote-proxy memoization (0909 leak fix): every getWebContents() across
|
|
17
|
+
// @electron/remote registers fresh objects in poi's main-process
|
|
18
|
+
// ObjectsRegistry; at ~1 call/sec the per-context Map hits V8's size cap
|
|
19
|
+
// (RangeError: Map maximum size exceeded) and ALL game writes die until
|
|
20
|
+
// poi restarts. Resolve by integer id (no registration) and cache the
|
|
21
|
+
// proxy — Electron ids are monotonic, never reused after destroy.
|
|
22
|
+
const webContentsProxyCache = new Map()
|
|
16
23
|
let captureScreenshot = options.captureScreenshot || null
|
|
17
24
|
const createSessionRecorder = options.createSessionRecorder
|
|
18
25
|
const eventTarget = options.eventTarget === undefined
|
|
@@ -63,12 +70,24 @@ function createPoiAutoInteractionRecorder(options = {}) {
|
|
|
63
70
|
function currentWebContents() {
|
|
64
71
|
const layout = getStore('layout.webview')
|
|
65
72
|
if (!layout || !layout.ref) return null
|
|
66
|
-
if (typeof layout.ref.getWebContents === 'function') {
|
|
67
|
-
return layout.ref.getWebContents()
|
|
68
|
-
}
|
|
69
73
|
if (typeof layout.ref.getWebContentsId === 'function') {
|
|
70
74
|
const id = layout.ref.getWebContentsId()
|
|
71
|
-
if (Number.isInteger(id) && id > 0)
|
|
75
|
+
if (Number.isInteger(id) && id > 0) {
|
|
76
|
+
const hit = webContentsProxyCache.get(id)
|
|
77
|
+
if (hit) return hit
|
|
78
|
+
const resolved = resolveWebContents(id)
|
|
79
|
+
if (resolved) {
|
|
80
|
+
webContentsProxyCache.set(id, resolved)
|
|
81
|
+
return resolved
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (typeof layout.ref.getWebContents === 'function') {
|
|
86
|
+
const webContents = layout.ref.getWebContents()
|
|
87
|
+
if (webContents) {
|
|
88
|
+
webContentsProxyCache.set('ref', webContents)
|
|
89
|
+
return webContents
|
|
90
|
+
}
|
|
72
91
|
}
|
|
73
92
|
return null
|
|
74
93
|
}
|
|
@@ -16,8 +16,17 @@ const DEFAULT_MAX_SESSION_BYTES = 8 * 1024 * 1024 * 1024
|
|
|
16
16
|
const DEFAULT_MAX_TIMELINE_EVENTS = 20000
|
|
17
17
|
const DEFAULT_MAX_SESSION_DURATION_MS = 4 * 60 * 60 * 1000
|
|
18
18
|
const DEFAULT_MAX_SCREENSHOT_BYTES = 16 * 1024 * 1024
|
|
19
|
-
const DEFAULT_MAX_STORED_SESSIONS =
|
|
20
|
-
|
|
19
|
+
const DEFAULT_MAX_STORED_SESSIONS = envPositiveInteger(
|
|
20
|
+
'POI_MCP_RECORDING_MAX_SESSIONS',
|
|
21
|
+
200,
|
|
22
|
+
)
|
|
23
|
+
const DEFAULT_MAX_TOTAL_RECORDING_BYTES = envPositiveInteger(
|
|
24
|
+
'POI_MCP_RECORDING_MAX_TOTAL_BYTES',
|
|
25
|
+
20 * 1024 * 1024 * 1024,
|
|
26
|
+
)
|
|
27
|
+
// Byte-gate headroom: evict oldest sessions until the root is below this
|
|
28
|
+
// fraction of the cap so a fresh session cannot immediately re-trip the gate.
|
|
29
|
+
const RECORDING_BYTE_HEADROOM_FRACTION = 0.9
|
|
21
30
|
const DEFAULT_FINAL_MANIFEST_RESERVE_BYTES = 64 * 1024
|
|
22
31
|
const DEFAULT_CHECKPOINT_DELAYS_MS = Object.freeze([0, 250, 1000])
|
|
23
32
|
const DEFAULT_OUTPUT_ROOT = process.platform === 'win32'
|
|
@@ -713,9 +722,17 @@ function createPoiInteractionRecorder(options = {}) {
|
|
|
713
722
|
if (running) return getStatus()
|
|
714
723
|
try {
|
|
715
724
|
const recordingRoot = await inspectRecordingRoot(outputRoot)
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
725
|
+
// Retention: both the session-count and byte gates evict oldest-first
|
|
726
|
+
// instead of refusing to start; the byte gate leaves headroom for the
|
|
727
|
+
// session that is about to begin.
|
|
728
|
+
const evictCount = recordingRetentionEvictionCount({
|
|
729
|
+
sessions: recordingRoot.sessions,
|
|
730
|
+
totalBytes: recordingRoot.byteCount,
|
|
731
|
+
maxStoredSessions,
|
|
732
|
+
maxTotalRecordingBytes,
|
|
733
|
+
})
|
|
734
|
+
if (evictCount > 0) {
|
|
735
|
+
await pruneOldestSessions(recordingRoot.sessionDirectories, evictCount, logger)
|
|
719
736
|
}
|
|
720
737
|
const afterPrune = await inspectRecordingRoot(outputRoot)
|
|
721
738
|
if (afterPrune.byteCount >= maxTotalRecordingBytes) {
|
|
@@ -950,7 +967,7 @@ async function inspectRecordingRoot(outputRoot) {
|
|
|
950
967
|
entries = await fs.promises.readdir(outputRoot, { withFileTypes: true })
|
|
951
968
|
} catch (error) {
|
|
952
969
|
if (error && error.code === 'ENOENT') {
|
|
953
|
-
return { sessionCount: 0, byteCount: 0, sessionDirectories: [] }
|
|
970
|
+
return { sessionCount: 0, byteCount: 0, sessionDirectories: [], sessions: [] }
|
|
954
971
|
}
|
|
955
972
|
throw error
|
|
956
973
|
}
|
|
@@ -969,9 +986,12 @@ async function inspectRecordingRoot(outputRoot) {
|
|
|
969
986
|
if (!error || error.code !== 'ENOENT') throw error
|
|
970
987
|
}
|
|
971
988
|
}
|
|
972
|
-
|
|
989
|
+
// Attribute every byte to its top-level directory so retention can rank
|
|
990
|
+
// sessions by size as well as age.
|
|
991
|
+
const rootBytes = new Map(directories.map((directory) => [directory, 0]))
|
|
992
|
+
const stack = directories.map((directory) => ({ directory, root: directory }))
|
|
973
993
|
while (stack.length > 0) {
|
|
974
|
-
const directory = stack.pop()
|
|
994
|
+
const { directory, root } = stack.pop()
|
|
975
995
|
let children
|
|
976
996
|
try {
|
|
977
997
|
children = await fs.promises.readdir(directory, { withFileTypes: true })
|
|
@@ -983,10 +1003,12 @@ async function inspectRecordingRoot(outputRoot) {
|
|
|
983
1003
|
if (child.isSymbolicLink()) continue
|
|
984
1004
|
const childPath = path.join(directory, child.name)
|
|
985
1005
|
if (child.isDirectory()) {
|
|
986
|
-
stack.push(childPath)
|
|
1006
|
+
stack.push({ directory: childPath, root })
|
|
987
1007
|
} else if (child.isFile()) {
|
|
988
1008
|
try {
|
|
989
|
-
|
|
1009
|
+
const size = (await fs.promises.stat(childPath)).size
|
|
1010
|
+
byteCount += size
|
|
1011
|
+
rootBytes.set(root, (rootBytes.get(root) || 0) + size)
|
|
990
1012
|
} catch (error) {
|
|
991
1013
|
if (!error || error.code !== 'ENOENT') throw error
|
|
992
1014
|
}
|
|
@@ -996,13 +1018,41 @@ async function inspectRecordingRoot(outputRoot) {
|
|
|
996
1018
|
const sessionDirectories = directories.filter((directory) =>
|
|
997
1019
|
SESSION_DIRECTORY_PATTERN.test(path.basename(directory)),
|
|
998
1020
|
)
|
|
1021
|
+
const sessions = sessionDirectories.map((directory) => ({
|
|
1022
|
+
directory,
|
|
1023
|
+
bytes: rootBytes.get(directory) || 0,
|
|
1024
|
+
}))
|
|
999
1025
|
return {
|
|
1000
1026
|
sessionCount: sessionDirectories.length,
|
|
1001
1027
|
byteCount,
|
|
1002
1028
|
sessionDirectories,
|
|
1029
|
+
sessions,
|
|
1003
1030
|
}
|
|
1004
1031
|
}
|
|
1005
1032
|
|
|
1033
|
+
// Oldest-first eviction count for the two retention gates. `sessions` must be
|
|
1034
|
+
// sorted oldest-first (inspectRecordingRoot guarantees this via name sort).
|
|
1035
|
+
function recordingRetentionEvictionCount({
|
|
1036
|
+
sessions,
|
|
1037
|
+
totalBytes,
|
|
1038
|
+
maxStoredSessions,
|
|
1039
|
+
maxTotalRecordingBytes,
|
|
1040
|
+
}) {
|
|
1041
|
+
if (!Array.isArray(sessions)) return 0
|
|
1042
|
+
const countOverflow = sessions.length >= maxStoredSessions
|
|
1043
|
+
? sessions.length - maxStoredSessions + 1
|
|
1044
|
+
: 0
|
|
1045
|
+
const byteTarget = Math.floor(maxTotalRecordingBytes * RECORDING_BYTE_HEADROOM_FRACTION)
|
|
1046
|
+
let byteEvictions = 0
|
|
1047
|
+
let recovered = 0
|
|
1048
|
+
for (const session of sessions) {
|
|
1049
|
+
if (totalBytes - recovered <= byteTarget) break
|
|
1050
|
+
recovered += Number.isFinite(session.bytes) ? session.bytes : 0
|
|
1051
|
+
byteEvictions += 1
|
|
1052
|
+
}
|
|
1053
|
+
return Math.max(countOverflow, byteEvictions)
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1006
1056
|
async function pruneOldestSessions(sessionDirectories, count, logger) {
|
|
1007
1057
|
if (count <= 0) return
|
|
1008
1058
|
for (const directory of sessionDirectories.slice(0, count)) {
|
|
@@ -1550,6 +1600,14 @@ function positiveInteger(value, fallback, name) {
|
|
|
1550
1600
|
return selected
|
|
1551
1601
|
}
|
|
1552
1602
|
|
|
1603
|
+
function envPositiveInteger(name, fallback) {
|
|
1604
|
+
const raw = process.env[name]
|
|
1605
|
+
if (raw === undefined || raw === '') return fallback
|
|
1606
|
+
const parsed = Number(raw)
|
|
1607
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) return fallback
|
|
1608
|
+
return parsed
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1553
1611
|
function positiveNumber(value, fallback, name) {
|
|
1554
1612
|
const selected = value == null ? fallback : value
|
|
1555
1613
|
if (!Number.isFinite(selected) || selected <= 0) {
|
|
@@ -1687,7 +1745,11 @@ module.exports = {
|
|
|
1687
1745
|
DEFAULT_MAX_STORED_SESSIONS,
|
|
1688
1746
|
DEFAULT_MAX_TOTAL_RECORDING_BYTES,
|
|
1689
1747
|
DEFAULT_OUTPUT_ROOT,
|
|
1748
|
+
RECORDING_BYTE_HEADROOM_FRACTION,
|
|
1690
1749
|
captureEquipmentUiStateFromWebContents: defaultCaptureEquipmentUiState,
|
|
1691
1750
|
captureWebStorageFromWebContents: defaultCaptureWebStorage,
|
|
1692
1751
|
createPoiInteractionRecorder,
|
|
1752
|
+
inspectRecordingRoot,
|
|
1753
|
+
pruneOldestSessions,
|
|
1754
|
+
recordingRetentionEvictionCount,
|
|
1693
1755
|
}
|