{"slug": "5-safety-guards-for-auto-archiving-another-ai-agent-s-conversations-into-my-a", "title": "5 Safety Guards for Auto-Archiving Another AI Agent's Conversations into My Vault: Designing a Manus API Bridge", "summary": "A developer built a Python bridge that polls the Manus API every five minutes and archives each task's messages into a shared knowledge vault's raw layer alongside Claude Code conversation logs. The script, run via launchd, uses five safety guards including reading its API key from the macOS Keychain on every run and tracking per-task revision timestamps in a state file to skip unchanged tasks. It only fetches and saves raw sources, leaving summarization to a separate reflection worker.", "body_md": "My Claude Code conversations were already saved to my knowledge vault automatically, but anything I gave to Manus stayed inside its browser UI. I couldn't search those tasks or find them later. Now a small Python bridge checks the Manus API every 5 minutes and saves each task to the same shelf as my Claude Code logs. It uses five safety guards to keep it quiet, self-healing and free of secrets.\n\nLast time, I wrote about [how a weekly batch job in my skills CLI had been failing silently](https://zenn.dev/bokuwalily/articles/skills-cli-weekly-silent-fail). This post continues with the same vault setup. It covers how I **automatically collect conversations from an AI agent other than Claude Code (Manus) through its API and store them as primary sources in a shared vault**.\n\nThe vault has a location called `raw/conversations/`, and Claude Code conversation logs pile up there automatically. Tasks I sent to Manus were different. The only way to see their contents was to open the task screen in the browser. When I later tried to remember what I had asked for in a task, it didn't show up in vault search or in the shared hot cache.\n\nManus has a public API that lets you read your own task list and messages. That was the starting point for this bridge: fetch those tasks on a schedule and put them on the same shelf (the raw layer) as the Claude Code conversation logs. `launchd` runs `~/Documents/claude-obsidian/bin/manus-task-archive.py` every 5 minutes, and the script writes its output to `~/Documents/my-knowledge-base/raw/manus-conversations/`.\n\n**Note**\n\nThis script **only fetches and saves**. A separate, safe reflection worker handles summaries and \"proposals to the vault.\" This script never does that work. A core rule of how I run the vault is to keep raw primary sources separate from the layer that interprets them.\n\nI don't want the API key in the code or in any file, so the script reads it from the macOS Keychain on every run.\n\n``` php\nKEYCHAIN_SERVICE = \"manus-vault-memory-archiver\"\n\ndef keychain_key() -> str:\n    result = subprocess.run(\n        [\"security\", \"find-generic-password\", \"-s\", KEYCHAIN_SERVICE, \"-w\"],\n        text=True,\n        capture_output=True,\n        check=False,\n    )\n    if result.returncode != 0 or not result.stdout.strip():\n        raise RuntimeError(\"Manus archive API key is unavailable in macOS Keychain\")\n    return result.stdout.strip()\n```\n\nThe docstring also states that this worker never passes the key to `print` or `write`. If the key can't be fetched, the function raises an exception. The `except` in `main()` logs it and the run stops. A broken key doesn't affect other jobs or conversations.\n\nThe script fetches Manus tasks by paging through `task.list` → `task.listMessages`. Fetching every message on every run would be wasteful. Instead, the script saves each task's update time as its \"revision\" in a state file and skips the task if that value hasn't changed.\n\n``` php\ndef task_revision(task: dict[str, Any]) -> str:\n    for key in (\"updated_at\", \"updatedAt\", \"modified_at\", \"timestamp\", \"created_at\", \"createdAt\"):\n        if task.get(key) not in (None, \"\"):\n            return str(task[key])\n    return \"unknown\"\nfor task in accessible:\n    ident = task_id(task)\n    if not ident:\n        continue\n    revision = task_revision(task)\n    if state[\"tasks\"].get(ident) == revision:\n        continue\n    messages = list_messages(credential, ident)\n    archive_task(task, messages)\n    state[\"tasks\"][ident] = revision\n```\n\nHere is the actual state file (`AI/.runtime/manus-task-archive-state.json`):\n\n```\n{\n  \"tasks\": {\n    \"rRkPkzEkUrImaYRXsI3sLY\": \"1790174319\",\n    \"7hBaAc4dhs9hycZaAnC8z5\": \"1789628528\",\n    \"5VXpxPKZkzify3yp9FA5LX\": \"1790077323\"\n  },\n  \"accessible_task_count\": 3\n}\n```\n\nThe launchd log (`~/.claude/logs/manus-task-archive.log`) shows that even though the job runs every 5 minutes, most runs find nothing new:\n\n```\n[2026-09-26 07:43:32 +0900] ok: accessible=3 updated=0\n[2026-09-26 07:48:33 +0900] ok: accessible=3 updated=0\n[2026-09-26 07:53:34 +0900] ok: accessible=3 updated=0\n[2026-09-26 07:58:51 +0900] ok: accessible=3 updated=0\n```\n\nA run of `accessible=3 updated=0` lines means the API returns 3 tasks each time, but none of their revisions changed, so nothing gets written. Of all my measurements, this was the clearest proof that the diff check works correctly.\n\nThe `launchd` `StartInterval` is fixed at 300 seconds (5 minutes), set in `~/Library/LaunchAgents/com.shun.manus-vault-archive.plist`.\n\n```\n<key>StartInterval</key>\n<integer>300</integer>\n<key>ThrottleInterval</key>\n<integer>60</integer>\n<key>LowPriorityIO</key>\n<true/>\n<key>Nice</key>\n<integer>12</integer>\n```\n\nOverlapping runs on a 5-minute schedule would cause trouble, so the script uses a lock directory created with `mkdir` to prevent more than one run at a time. The case I paid attention to was a previous run crashing and leaving its lock behind.\n\n``` php\ndef acquire_lock() -> bool:\n    STATE_DIR.mkdir(parents=True, exist_ok=True)\n    if LOCK_DIR.exists():\n        try:\n            age = time.time() - LOCK_DIR.stat().st_mtime\n            if age > 1800:\n                LOCK_DIR.rmdir()\n        except OSError:\n            pass\n    try:\n        LOCK_DIR.mkdir()\n        return True\n    except FileExistsError:\n        return False\n```\n\nIf a lock is older than 30 minutes (1800 seconds, or six 5-minute cycles), the next run removes it. The system recovers on a later cycle before a human notices and runs `rm`. Releasing the lock also sits in a `finally` block, so the lock is always removed, whether the run crashes with an exception or exits normally.\n\nThe log shows this working in practice:\n\n```\n[2026-09-26 06:42:45 +0900] ok: accessible=3 updated=0\n[2026-09-26 06:48:15 +0900] error: RuntimeError: API request failed: <urlopen error [Errno 8] nodename nor servname provided, or not known>\n[2026-09-26 06:53:16 +0900] ok: accessible=3 updated=0\n```\n\nAt 06:48, DNS resolution failed briefly and the run crashed with an exception. The `finally` block had already released the lock, so the next run 5 minutes later (06:53) finished normally with nothing blocking it. The lock design was meant to do exactly this: fix itself before a human notices.\n\nThis raw layer becomes \"primary source material for future AI agents to read.\" To keep secrets out of it, the script scrubs them with regular expressions.\n\n```\nSECRET_PATTERNS = [\n    (re.compile(r\"\\bsk-[A-Za-z0-9_\\-]{16,}\\b\"), \"[REDACTED_API_KEY]\"),\n    (re.compile(r\"(?i)\\b(?:api[_ -]?key|token|password|secret|bearer)\\s*[:=]\\s*[^\\s`'\\\"]{8,}\"), \"[REDACTED_SECRET]\"),\n    (re.compile(r\"https?://(?:meet\\.google\\.com|zoom\\.us/j|teams\\.microsoft\\.com)/[^\\s)\\]>]+\", re.I), \"[REDACTED_MEETING_LINK]\"),\n]\n```\n\nThe comment in the code explains the design intent:\n\n```\n# Deliberately conservative. The archive is a private raw source, but it must\n# not become an accidental secret store shared with future agents.\n```\n\nThe important part is the order of reasoning. I didn't start from \"this raw store is personal, so it can be a little loose.\" I started from \"future agents will read it, so keep it conservative.\" Zoom, Meet and Teams meeting URLs get the same treatment as API key patterns. Conversation logs often contain lines like \"join from this link,\" and I don't want those saved permanently in the primary sources.\n\nEach archived Markdown file's modification time (mtime) is set to **the time the task was last updated in Manus, not the time it was archived**.\n\n```\noutput = RAW_DIR / f\"manus_{ident}.md\"\natomic_write(output, \"\\n\".join(parts).rstrip() + \"\\n\")\nstamp = updated.timestamp()\nos.utime(output, (stamp, stamp))\n```\n\nWithout this, a task that finished 9 days ago but was archived for the first time today would appear as \"something that just happened\" in the shared hot cache and other indexes that sort by mtime. In fact, one of the files from the first archive run had an update date of 9/17:\n\n```\n# 会話ログ: Manus / Agent Manus / 7hBaAc4d\n日時: 2026-09-17 07:02 UTC\n更新: 2026-09-17 07:02 UTC\nTask ID: `7hBaAc4dhs9hycZaAnC8z5`\nStatus: `waiting`\n```\n\nThe file was written today, but its mtime stays at 9/17, so it doesn't appear in the list of \"today's events.\" Writes also go through `atomic_write`, which writes a temp file and then calls `os.replace`. If the process dies partway through, no half-written file is left behind.\n\nAfter writing, if at least one task was updated, the script also runs the shared hot-cache update script:\n\n```\nif updated_files and UPDATE_HOT.exists():\n    subprocess.run([sys.executable, str(UPDATE_HOT)], check=False,\n                    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n```\n\nThis way, Manus tasks reach the vault's \"recent sessions\" list through the same path as the other raw conversation logs.\n\n| Guard | What it protects against | Implementation | \n|---|---|---|\n| Keychain-only credential | Key leaks | Call `security find-generic-password` on every run; never write the key in code | \n| Incremental sync by revision diff | Wasted API calls and duplicate writes | Compare `state[\"tasks\"][ident]` with the revision | \n| Stale-lock self-healing | Permanent skips after a crash | Automatically `rmdir` a lock dir older than 1800 seconds on the next run | \n| Secret / meeting-link redaction | Secrets leaking into primary sources | Replace with 3 regex patterns | \n| mtime backdating | Breaking the index timeline | Pin the file to the task's update time with `os.utime` | \n\n`task.get(\"task_id\") or task.get(\"id\")`, and for revisions, check keys in order from `updated_at` through `createdAt`.` RuntimeError` once paging goes past `MAX_MESSAGE_PAGES` (100). I chose \"fail with an error I'll notice\" over an infinite loop.`urlopen error` failure (2026-09-26 06:48). Because lock release is in `finally`, the next 5-minute cycle recovered normally with no special handling.`raw/` shelf as Claude Code conversation logs, `accessible=3 updated=0` is the normal, most common result.`rmdir`\nNext time, I plan to write about how the reflection worker safely summarizes the Manus conversations in this raw layer and promotes them into the wiki.\n\nIf you archive conversations from more than one AI agent, how do you keep secrets and outdated timestamps out of the shared record?\n\n*Written by **Lily** — I ship iOS apps and automate my content stack with Claude Code.\n\nFollow along: [Portfolio](https://bokuwalily.com) · [X](https://x.com/bokuwalily) · [GitHub](https://github.com/bokuwalily)*", "url": "https://wpnews.pro/news/5-safety-guards-for-auto-archiving-another-ai-agent-s-conversations-into-my-a", "canonical_source": "https://dev.to/bokuwalily/5-safety-guards-for-auto-archiving-another-ai-agents-conversations-into-my-vault-designing-a-35gd", "published_at": "2026-09-27 00:00:03+00:00", "updated_at": "2026-09-27 00:30:50.468376+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools"], "entities": ["Manus", "Claude Code", "macOS Keychain", "launchd", "Python"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/5-safety-guards-for-auto-archiving-another-ai-agent-s-conversations-into-my-a", "markdown": "https://wpnews.pro/news/5-safety-guards-for-auto-archiving-another-ai-agent-s-conversations-into-my-a.md", "text": "https://wpnews.pro/news/5-safety-guards-for-auto-archiving-another-ai-agent-s-conversations-into-my-a.txt", "jsonld": "https://wpnews.pro/news/5-safety-guards-for-auto-archiving-another-ai-agent-s-conversations-into-my-a.jsonld"}}