From 44e44c38196b0fc35844d9c28bf6a81d6fe0533f Mon Sep 17 00:00:00 2001 From: smile <134200591+smileygames@users.noreply.github.com> Date: Mon, 16 Mar 2026 17:23:15 +0900 Subject: [PATCH] feat(purge): auto-purge processed events on mark_processed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mark_processed実行時にprocessed済み+保持期間超過のイベントを自動削除。 デフォルト7日、PURGE_AFTER_DAYS環境変数で設定可能。Node.js/Python両方対応。 Refs #29 --- main.py | 51 +++++++++++++++++++++++++++++--- mcp-server/server/event-store.js | 35 ++++++++++++++++++++-- mcp-server/server/index.js | 4 +-- 3 files changed, 81 insertions(+), 9 deletions(-) diff --git a/main.py b/main.py index bde838f..bd7fc02 100644 --- a/main.py +++ b/main.py @@ -33,6 +33,7 @@ def load_dotenv() -> bool: PRIMARY_ENCODING = "utf-8" LEGACY_ENCODINGS = ("utf-8-sig", "cp932", "shift_jis") NOTIFY_ONLY_EXIT_CODE = 86 +DEFAULT_PURGE_DAYS = 7 NOTIFICATION_EVENT_ACTIONS = { "issues": {"assigned", "closed", "opened", "reopened", "unassigned"}, "issue_comment": {"created"}, @@ -105,6 +106,37 @@ def update_event(event_id: str, **updates: Any) -> bool: return True return False +def _purge_days() -> int: + env = os.environ.get("PURGE_AFTER_DAYS") + if env is not None: + try: + n = int(env) + if n >= 0: + return n + except ValueError: + pass + return DEFAULT_PURGE_DAYS + +def purge_processed(events: list[dict]) -> tuple[list[dict], int]: + days = _purge_days() + if days < 0: + return events, 0 + cutoff = datetime.now(timezone.utc).timestamp() - days * 86400 + before = len(events) + kept = [] + for e in events: + if not e.get("processed"): + kept.append(e) + continue + try: + ts = datetime.fromisoformat(e["received_at"]).timestamp() + except (KeyError, ValueError): + kept.append(e) + continue + if ts > cutoff: + kept.append(e) + return kept, before - len(kept) + def _normalize_event_profile(profile: str) -> str: normalized = (profile or "all").strip().lower() if normalized not in {"all", "notifications"}: @@ -184,8 +216,19 @@ def get_event(event_id: str) -> dict | None: return event return None -def mark_done(event_id: str) -> bool: - return update_event(event_id, processed=True) +def mark_done(event_id: str) -> dict: + events = _load() + found = False + for event in events: + if event["id"] == event_id: + event["processed"] = True + found = True + break + if not found: + return {"success": False, "purged": 0} + kept, purged = purge_processed(events) + _save(kept) + return {"success": True, "purged": purged} # ── Direct Trigger Execution ─────────────────────────────────────────────────── @@ -551,11 +594,11 @@ async def call_tool( ] if name == "mark_processed": event_id = arguments.get("event_id", "") - ok = mark_done(event_id) + result = mark_done(event_id) return [ types.TextContent( type="text", - text=json.dumps({"success": ok, "event_id": event_id}), + text=json.dumps({"success": result["success"], "event_id": event_id, "purged": result["purged"]}), ) ] raise ValueError(f"Unknown tool: {name}") diff --git a/mcp-server/server/event-store.js b/mcp-server/server/event-store.js index 89b8141..4fdd10a 100644 --- a/mcp-server/server/event-store.js +++ b/mcp-server/server/event-store.js @@ -7,6 +7,16 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const DEFAULT_DATA_FILE = resolve(__dirname, "..", "..", "events.json"); const PRIMARY_ENCODING = "utf-8"; const LEGACY_ENCODINGS = ["utf-8", "cp932", "shift_jis"]; +const DEFAULT_PURGE_DAYS = 7; + +function purgeDays() { + const env = process.env.PURGE_AFTER_DAYS; + if (env !== undefined) { + const n = Number(env); + if (Number.isFinite(n) && n >= 0) return n; + } + return DEFAULT_PURGE_DAYS; +} function dataFilePath() { return process.env.EVENTS_JSON_PATH || DEFAULT_DATA_FILE; @@ -51,6 +61,21 @@ export function save(events) { writeFileSync(filePath, JSON.stringify(events, null, 2), PRIMARY_ENCODING); } +// ── Purge ────────────────────────────────────────────────────────────────── + +export function purgeProcessed(events) { + const days = purgeDays(); + if (days < 0) return { kept: events, purged: 0 }; + const cutoff = Date.now() - days * 86_400_000; + const before = events.length; + const kept = events.filter((e) => { + if (!e.processed) return true; + const ts = Date.parse(e.received_at); + return Number.isNaN(ts) || ts > cutoff; + }); + return { kept, purged: before - kept.length }; +} + // ── Query ─────────────────────────────────────────────────────────────────── export function getPending() { @@ -141,12 +166,16 @@ export function getPendingSummaries(limit = 20) { export function markDone(eventId) { const events = load(); + let found = false; for (const event of events) { if (event.id === eventId) { event.processed = true; - save(events); - return true; + found = true; + break; } } - return false; + if (!found) return { success: false, purged: 0 }; + const { kept, purged } = purgeProcessed(events); + save(kept); + return { success: true, purged }; } diff --git a/mcp-server/server/index.js b/mcp-server/server/index.js index cb79c06..f6c0b3f 100644 --- a/mcp-server/server/index.js +++ b/mcp-server/server/index.js @@ -92,10 +92,10 @@ server.tool( event_id: z.string().describe("The event ID to mark as processed"), }, async ({ event_id }) => { - const success = markDone(event_id); + const result = markDone(event_id); return { content: [ - { type: "text", text: JSON.stringify({ success, event_id }) }, + { type: "text", text: JSON.stringify({ success: result.success, event_id, purged: result.purged }) }, ], }; }