poi-plugin-mcp 0.2.24 → 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.
@@ -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 = 200
20
- const DEFAULT_MAX_TOTAL_RECORDING_BYTES = 64 * 1024 * 1024 * 1024
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
- if (recordingRoot.sessionCount >= maxStoredSessions) {
717
- const overflow = recordingRoot.sessionCount - maxStoredSessions + 1
718
- await pruneOldestSessions(recordingRoot.sessionDirectories, overflow, logger)
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
- const stack = [...directories]
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
- byteCount += (await fs.promises.stat(childPath)).size
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poi-plugin-mcp",
3
- "version": "0.2.24",
3
+ "version": "0.2.25",
4
4
  "description": "Poi data, WebView capture, and opt-in authenticated input bridge for local KanColle tools.",
5
5
  "main": "index.js",
6
6
  "keywords": [