@remnic/core 1.1.7 → 1.1.9
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/dist/access-cli.js +30 -30
- package/dist/access-http.d.ts +7 -7
- package/dist/access-http.js +13 -13
- package/dist/access-mcp.d.ts +7 -7
- package/dist/access-mcp.js +12 -12
- package/dist/{access-service-B9sziJdP.d.ts → access-service-BJCIjVRY.d.ts} +9 -9
- package/dist/access-service.d.ts +7 -7
- package/dist/access-service.js +11 -11
- package/dist/active-memory-bridge.d.ts +1 -1
- package/dist/active-recall.d.ts +2 -2
- package/dist/active-recall.js +2 -2
- package/dist/active-recall.js.map +1 -1
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +6 -6
- package/dist/briefing.d.ts +2 -2
- package/dist/briefing.js +6 -6
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +2 -2
- package/dist/calibration.d.ts +1 -1
- package/dist/calibration.js +2 -2
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +2 -2
- package/dist/causal-consolidation.js +8 -8
- package/dist/{chunk-LKJA5MR2.js → chunk-2MVUXO4H.js} +2 -2
- package/dist/{chunk-GZCUW5IC.js → chunk-3IQ2TR4N.js} +5 -5
- package/dist/chunk-3IQ2TR4N.js.map +1 -0
- package/dist/{chunk-AKV3HOGZ.js → chunk-3VRIIII5.js} +150 -2
- package/dist/chunk-3VRIIII5.js.map +1 -0
- package/dist/{chunk-TUFG6VXY.js → chunk-4DWOBS2A.js} +2 -2
- package/dist/chunk-4DWOBS2A.js.map +1 -0
- package/dist/{chunk-L2IO2QPY.js → chunk-4IS4SXIQ.js} +17 -13
- package/dist/chunk-4IS4SXIQ.js.map +1 -0
- package/dist/{chunk-ZOMA7J3J.js → chunk-6OAQEOGV.js} +2 -2
- package/dist/{chunk-FSWYMUWI.js → chunk-6Z6UH6TK.js} +38 -12
- package/dist/chunk-6Z6UH6TK.js.map +1 -0
- package/dist/{chunk-EONJ7GK3.js → chunk-7SFAENUZ.js} +2 -2
- package/dist/{chunk-ODWDQNRE.js → chunk-7SI52C65.js} +7 -3
- package/dist/chunk-7SI52C65.js.map +1 -0
- package/dist/{chunk-M3QQ5DRA.js → chunk-A6PGANSE.js} +3 -3
- package/dist/{chunk-KUIEFH2S.js → chunk-BIHCWSWA.js} +3 -3
- package/dist/{chunk-PT2EZWOH.js → chunk-CTYRIJ5E.js} +3 -3
- package/dist/{chunk-COAGZQT7.js → chunk-ET4BL42V.js} +1 -1
- package/dist/chunk-ET4BL42V.js.map +1 -0
- package/dist/{chunk-DWMXVUGO.js → chunk-FLBYSB2V.js} +6 -4
- package/dist/chunk-FLBYSB2V.js.map +1 -0
- package/dist/{chunk-VZNQB6NL.js → chunk-FPWUENQH.js} +39 -35
- package/dist/chunk-FPWUENQH.js.map +1 -0
- package/dist/chunk-FVQJYWH7.js +52 -0
- package/dist/chunk-FVQJYWH7.js.map +1 -0
- package/dist/{chunk-RXGR3YLU.js → chunk-G3G3LY22.js} +2 -2
- package/dist/{chunk-ZBZVNWQO.js → chunk-G6NX57V2.js} +33 -43
- package/dist/chunk-G6NX57V2.js.map +1 -0
- package/dist/{chunk-3FPTCC3Z.js → chunk-GVPWB7EY.js} +2 -2
- package/dist/{chunk-D7WYTVUQ.js → chunk-ICULSMDG.js} +2 -2
- package/dist/{chunk-5NS6NN5A.js → chunk-J3P6WSFZ.js} +2 -2
- package/dist/{chunk-FCGWNWG4.js → chunk-KIF7QNKL.js} +28 -28
- package/dist/chunk-KIF7QNKL.js.map +1 -0
- package/dist/{chunk-YELFQNQH.js → chunk-KMWZXT5T.js} +2 -2
- package/dist/{chunk-YKGRACQP.js → chunk-M3DK45UM.js} +5 -5
- package/dist/{chunk-DLYTYJ43.js → chunk-MJLUHRSF.js} +5 -5
- package/dist/{chunk-4KAN3GZ3.js → chunk-NN2DKE4T.js} +1 -1
- package/dist/chunk-NN2DKE4T.js.map +1 -0
- package/dist/{chunk-R2XRID2N.js → chunk-NN3LPQ5D.js} +5 -5
- package/dist/chunk-NN3LPQ5D.js.map +1 -0
- package/dist/{chunk-7RAW2T4P.js → chunk-OWGGXPKV.js} +16 -9
- package/dist/chunk-OWGGXPKV.js.map +1 -0
- package/dist/{chunk-WSZIHQBK.js → chunk-P77UEOU2.js} +4 -1
- package/dist/{chunk-WSZIHQBK.js.map → chunk-P77UEOU2.js.map} +1 -1
- package/dist/{chunk-MYMOXFMR.js → chunk-PHQH2VUO.js} +4 -4
- package/dist/{chunk-OZAFME7S.js → chunk-QPLYTPYL.js} +15 -15
- package/dist/{chunk-FEMOX5AD.js → chunk-QR3C7BKQ.js} +7 -7
- package/dist/chunk-QR3C7BKQ.js.map +1 -0
- package/dist/{chunk-3LCWFNVS.js → chunk-SKE7JYKA.js} +2 -2
- package/dist/{chunk-AIT53NLG.js → chunk-U4SZXGEO.js} +2 -2
- package/dist/{chunk-67YLUWLG.js → chunk-XJKFSSDW.js} +3 -3
- package/dist/chunk-XJKFSSDW.js.map +1 -0
- package/dist/{chunk-KQB4C4OE.js → chunk-XL3UCAZA.js} +22 -22
- package/dist/{chunk-ASIQZXYO.js → chunk-XMVFHBHT.js} +2 -2
- package/dist/{chunk-6B23Z44B.js → chunk-XN4D6Z7X.js} +3 -3
- package/dist/{chunk-S5SQDIF5.js → chunk-Y3VT6ZCP.js} +4 -4
- package/dist/{cli-olNPi1uN.d.ts → cli-BojuyOOp.d.ts} +4 -4
- package/dist/cli.d.ts +8 -8
- package/dist/cli.js +24 -24
- package/dist/{codex-materialize-D5d5vvyS.d.ts → codex-materialize-YVC2wb6n.d.ts} +1 -1
- package/dist/compression-optimizer.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/consolidation-provenance-check.d.ts +2 -2
- package/dist/consolidation-undo.d.ts +2 -2
- package/dist/day-summary.d.ts +1 -1
- package/dist/day-summary.js +1 -1
- package/dist/delinearize.d.ts +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/{engine-2JKWFKJV.js → engine-EDFFOWDD.js} +7 -7
- package/dist/entity-retrieval.d.ts +2 -2
- package/dist/entity-retrieval.js +6 -6
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +6 -6
- package/dist/explicit-capture.js +2 -2
- package/dist/explicit-cue-recall.js +1 -1
- package/dist/extraction-judge-telemetry.d.ts +1 -1
- package/dist/extraction-judge-training.d.ts +1 -1
- package/dist/extraction-judge.d.ts +1 -1
- package/dist/extraction.d.ts +1 -1
- package/dist/extraction.js +7 -7
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/fallback-llm.js +2 -2
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +13 -13
- package/dist/index.js +147 -147
- package/dist/index.js.map +1 -1
- package/dist/intent.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/live-connectors-runner.js +2 -2
- package/dist/local-llm.d.ts +1 -1
- package/dist/local-llm.js +1 -1
- package/dist/memory-action-policy.d.ts +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/{memory-governance-7MI7KE35.js → memory-governance-AAQPBZEP.js} +7 -7
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/{memory-projection-store-BQt5VUQ8.d.ts → memory-projection-store-BW8u5U0u.d.ts} +1 -1
- package/dist/memory-projection-store.d.ts +2 -2
- package/dist/memory-projection-store.js +1 -1
- package/dist/memory-worth-outcomes.d.ts +2 -2
- package/dist/{migrate-from-identity-anchor-TTEDEJGX.js → migrate-from-identity-anchor-G27MCD6A.js} +2 -2
- package/dist/model-registry.js +1 -1
- package/dist/models-json.d.ts +1 -1
- package/dist/models-json.js +1 -1
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +2 -2
- package/dist/operator-toolkit.js +10 -10
- package/dist/opik-exporter.js +2 -2
- package/dist/opik-exporter.js.map +1 -1
- package/dist/{orchestrator-D2lHhFWI.d.ts → orchestrator-CYqmqxco.d.ts} +5 -5
- package/dist/orchestrator.d.ts +6 -6
- package/dist/orchestrator.js +25 -25
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/{peers-6OSQ3NK6.js → peers-HCVGHMAE.js} +3 -3
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/{port-5W-r5SKc.d.ts → port-Br27H8dy.d.ts} +7 -1
- package/dist/qmd-recall-cache.d.ts +2 -2
- package/dist/qmd.d.ts +3 -2
- package/dist/qmd.js +1 -1
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.d.ts +1 -1
- package/dist/recall-xray.js +2 -2
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +2 -2
- package/dist/retrieval-agents.d.ts +2 -2
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/sanitize.js +1 -1
- package/dist/schemas.d.ts +22 -22
- package/dist/{semantic-consolidation-tDODR2je.d.ts → semantic-consolidation-GPcLr9BQ.d.ts} +2 -2
- package/dist/semantic-consolidation.d.ts +3 -3
- package/dist/semantic-consolidation.js +6 -6
- package/dist/semantic-rule-promotion.js +6 -6
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +6 -6
- package/dist/session-observer-bands.d.ts +1 -1
- package/dist/session-observer-state.d.ts +1 -1
- package/dist/signal.d.ts +1 -1
- package/dist/source-attribution.d.ts +1 -1
- package/dist/source-attribution.js +1 -1
- package/dist/storage.d.ts +2 -2
- package/dist/storage.js +5 -5
- package/dist/summarizer.d.ts +1 -1
- package/dist/summarizer.js +5 -5
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/temporal-supersession.d.ts +2 -2
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +3 -3
- package/dist/tier-routing.d.ts +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/{types-C-USTTAx.d.ts → types-Bmp9ssU2.d.ts} +4 -3
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +6 -6
- package/package.json +1 -1
- package/dist/chunk-4KAN3GZ3.js.map +0 -1
- package/dist/chunk-67YLUWLG.js.map +0 -1
- package/dist/chunk-7RAW2T4P.js.map +0 -1
- package/dist/chunk-AKV3HOGZ.js.map +0 -1
- package/dist/chunk-COAGZQT7.js.map +0 -1
- package/dist/chunk-DWMXVUGO.js.map +0 -1
- package/dist/chunk-FCGWNWG4.js.map +0 -1
- package/dist/chunk-FEMOX5AD.js.map +0 -1
- package/dist/chunk-FSWYMUWI.js.map +0 -1
- package/dist/chunk-GZCUW5IC.js.map +0 -1
- package/dist/chunk-L2IO2QPY.js.map +0 -1
- package/dist/chunk-M62O4P4T.js +0 -41
- package/dist/chunk-M62O4P4T.js.map +0 -1
- package/dist/chunk-ODWDQNRE.js.map +0 -1
- package/dist/chunk-R2XRID2N.js.map +0 -1
- package/dist/chunk-TUFG6VXY.js.map +0 -1
- package/dist/chunk-VZNQB6NL.js.map +0 -1
- package/dist/chunk-ZBZVNWQO.js.map +0 -1
- /package/dist/{chunk-LKJA5MR2.js.map → chunk-2MVUXO4H.js.map} +0 -0
- /package/dist/{chunk-ZOMA7J3J.js.map → chunk-6OAQEOGV.js.map} +0 -0
- /package/dist/{chunk-EONJ7GK3.js.map → chunk-7SFAENUZ.js.map} +0 -0
- /package/dist/{chunk-M3QQ5DRA.js.map → chunk-A6PGANSE.js.map} +0 -0
- /package/dist/{chunk-KUIEFH2S.js.map → chunk-BIHCWSWA.js.map} +0 -0
- /package/dist/{chunk-PT2EZWOH.js.map → chunk-CTYRIJ5E.js.map} +0 -0
- /package/dist/{chunk-RXGR3YLU.js.map → chunk-G3G3LY22.js.map} +0 -0
- /package/dist/{chunk-3FPTCC3Z.js.map → chunk-GVPWB7EY.js.map} +0 -0
- /package/dist/{chunk-D7WYTVUQ.js.map → chunk-ICULSMDG.js.map} +0 -0
- /package/dist/{chunk-5NS6NN5A.js.map → chunk-J3P6WSFZ.js.map} +0 -0
- /package/dist/{chunk-YELFQNQH.js.map → chunk-KMWZXT5T.js.map} +0 -0
- /package/dist/{chunk-YKGRACQP.js.map → chunk-M3DK45UM.js.map} +0 -0
- /package/dist/{chunk-DLYTYJ43.js.map → chunk-MJLUHRSF.js.map} +0 -0
- /package/dist/{chunk-MYMOXFMR.js.map → chunk-PHQH2VUO.js.map} +0 -0
- /package/dist/{chunk-OZAFME7S.js.map → chunk-QPLYTPYL.js.map} +0 -0
- /package/dist/{chunk-3LCWFNVS.js.map → chunk-SKE7JYKA.js.map} +0 -0
- /package/dist/{chunk-AIT53NLG.js.map → chunk-U4SZXGEO.js.map} +0 -0
- /package/dist/{chunk-KQB4C4OE.js.map → chunk-XL3UCAZA.js.map} +0 -0
- /package/dist/{chunk-ASIQZXYO.js.map → chunk-XMVFHBHT.js.map} +0 -0
- /package/dist/{chunk-6B23Z44B.js.map → chunk-XN4D6Z7X.js.map} +0 -0
- /package/dist/{chunk-S5SQDIF5.js.map → chunk-Y3VT6ZCP.js.map} +0 -0
- /package/dist/{engine-2JKWFKJV.js.map → engine-EDFFOWDD.js.map} +0 -0
- /package/dist/{memory-governance-7MI7KE35.js.map → memory-governance-AAQPBZEP.js.map} +0 -0
- /package/dist/{migrate-from-identity-anchor-TTEDEJGX.js.map → migrate-from-identity-anchor-G27MCD6A.js.map} +0 -0
- /package/dist/{peers-6OSQ3NK6.js.map → peers-HCVGHMAE.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/connectors/live/registry.ts","../src/connectors/live/transient-errors.ts","../src/connectors/live/google-drive.ts","../src/connectors/live/notion.ts","../src/connectors/live/gmail.ts","../src/connectors/live/github.ts"],"sourcesContent":["/**\n * @remnic/core — Live Connectors Registry (issue #683 PR 1/N)\n *\n * Pure in-memory registry. No I/O. Concrete connectors register themselves at\n * orchestrator boot (later PRs); the maintenance scheduler asks the registry\n * for the active set when running due syncs.\n */\n\nimport { isValidConnectorId, type LiveConnector } from \"./framework.js\";\n\n/**\n * Thrown when registering a duplicate id or an id that fails validation.\n *\n * Distinct error class so callers can distinguish framework-level mistakes\n * (which are programmer errors) from connector-runtime failures.\n */\nexport class LiveConnectorRegistryError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"LiveConnectorRegistryError\";\n }\n}\n\n/**\n * In-memory registry of live connectors. One instance per orchestrator. Not\n * safe for cross-process sharing — wire each process to its own registry.\n */\nexport class LiveConnectorRegistry {\n private readonly connectors = new Map<string, LiveConnector>();\n\n /**\n * Register a connector. Throws `LiveConnectorRegistryError` if the id is\n * malformed or already registered.\n *\n * Re-registration is rejected (rather than silently overwriting) because\n * silent overwrites mask plugin loading bugs in development and could let\n * a malicious extension shadow a built-in connector.\n */\n register(connector: LiveConnector): void {\n if (!connector || typeof connector !== \"object\") {\n throw new LiveConnectorRegistryError(\n \"register(): connector must be a non-null object\",\n );\n }\n if (!isValidConnectorId(connector.id)) {\n throw new LiveConnectorRegistryError(\n `register(): invalid connector id ${JSON.stringify(connector.id)} — must match /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/`,\n );\n }\n if (this.connectors.has(connector.id)) {\n throw new LiveConnectorRegistryError(\n `register(): connector id ${JSON.stringify(connector.id)} is already registered`,\n );\n }\n if (typeof connector.displayName !== \"string\" || connector.displayName.length === 0) {\n throw new LiveConnectorRegistryError(\n `register(): connector ${connector.id} missing displayName`,\n );\n }\n if (typeof connector.validateConfig !== \"function\") {\n throw new LiveConnectorRegistryError(\n `register(): connector ${connector.id} missing validateConfig()`,\n );\n }\n if (typeof connector.syncIncremental !== \"function\") {\n throw new LiveConnectorRegistryError(\n `register(): connector ${connector.id} missing syncIncremental()`,\n );\n }\n this.connectors.set(connector.id, connector);\n }\n\n /**\n * Look up a connector by id. Returns `undefined` if not registered.\n */\n get(id: string): LiveConnector | undefined {\n return this.connectors.get(id);\n }\n\n /**\n * Return all registered connectors, sorted by id for stable enumeration.\n */\n list(): LiveConnector[] {\n return Array.from(this.connectors.values()).sort((a, b) => a.id.localeCompare(b.id));\n }\n\n /**\n * Remove a connector. Returns `true` if a connector was removed, `false`\n * otherwise. The cursor / state file on disk is **not** touched — callers\n * who want to fully decommission a connector must also delete its state\n * file via the `state-store` module.\n */\n unregister(id: string): boolean {\n return this.connectors.delete(id);\n }\n\n /**\n * Number of registered connectors. Cheap; safe to call frequently.\n */\n size(): number {\n return this.connectors.size;\n }\n}\n","/**\n * Shared transient-error classifier for live connectors.\n *\n * All three connectors (Drive, Notion, Gmail) need to distinguish transient\n * errors (re-throw so the pass stops without advancing the cursor — the next\n * poll retries the same batch) from terminal errors (skip-and-continue — the\n * resource is gone, inaccessible, or malformed in a non-recoverable way).\n *\n * Having three independent copies of this logic caused divergent edge-case\n * handling (Cursor thread PRRT_kwDORJXyws59sdH4 / #745). This module is the\n * single source of truth; all three connectors import from here.\n *\n * Transient classes:\n * - 429 (rate-limit / quota — retry after backoff)\n * - 5xx (backend error — retry)\n * - AbortError / cancelled requests\n * - Network errors with no HTTP status (ECONNRESET, ETIMEDOUT, ENOTFOUND, EAI_AGAIN, …)\n * - Bare `Error` with no metadata (plain network failures)\n *\n * Terminal classes (skip-and-continue):\n * - 400 (bad request — won't be fixed by retrying)\n * - 403 (permission denied)\n * - 404 (resource gone or not shared)\n * - 410 (gone)\n * - Any other 4xx that isn't 429\n *\n * Connector-specific status properties are resolved via the `statusProps`\n * parameter. Connectors attach their own status property name (e.g.\n * `gmailStatus`, `notionStatus`) so the classifier can resolve it without\n * knowing the error shape in detail.\n */\n\n/**\n * Duck-typed error shape that all three connector error types share.\n * Callers pass additional connector-specific status property names via\n * `statusProps`.\n */\ninterface ErrorLike {\n name?: unknown;\n code?: unknown;\n status?: unknown;\n response?: { status?: unknown } | null;\n [key: string]: unknown;\n}\n\n/**\n * Set of Node.js network-layer error codes we treat as transient.\n * Explicitly enumerated (not a denylist) so unknown codes don't accidentally\n * get swallowed.\n */\nconst TRANSIENT_NODE_CODES = new Set([\n \"ECONNRESET\",\n \"ECONNREFUSED\",\n \"ECONNABORTED\",\n \"ETIMEDOUT\",\n \"ESOCKETTIMEDOUT\",\n \"ENOTFOUND\",\n \"EAI_AGAIN\",\n \"EPIPE\",\n \"EHOSTUNREACH\",\n \"ENETUNREACH\",\n \"ENETDOWN\",\n \"ERR_NETWORK\",\n \"ERR_NETWORK_CHANGED\",\n]);\n\n/**\n * Resolve a numeric HTTP status from any of the documented locations on an\n * error-shaped object.\n *\n * Resolution order:\n * 1. `statusProps` — connector-specific status properties (e.g. `gmailStatus`, `notionStatus`)\n * 2. `response.status` — canonical for fetch-compatible / GaxiosError shapes\n * 3. `status` — top-level for some HTTP client libraries\n * 4. `code` — numeric (older GaxiosError) or string-numeric (\"429\" / \"503\")\n */\nfunction resolveHttpStatus(\n e: ErrorLike,\n statusProps: readonly string[],\n): number | undefined {\n // Connector-specific properties first.\n for (const prop of statusProps) {\n const v = e[prop];\n if (typeof v === \"number\" && Number.isFinite(v)) return v;\n }\n\n // response.status (canonical GaxiosError / fetch-compatible).\n const responseStatus = e.response?.status;\n if (typeof responseStatus === \"number\" && Number.isFinite(responseStatus)) {\n return responseStatus;\n }\n\n // Top-level status.\n if (typeof e.status === \"number\" && Number.isFinite(e.status)) return e.status;\n\n // Numeric code.\n if (typeof e.code === \"number\" && Number.isFinite(e.code)) return e.code;\n\n // String-numeric codes (\"429\" / \"503\" — older googleapis / older Node HTTP).\n if (typeof e.code === \"string\" && /^\\d+$/.test(e.code)) {\n const n = Number(e.code);\n if (Number.isFinite(n) && n >= 100 && n <= 599) return n;\n }\n\n return undefined;\n}\n\n/**\n * Generic transient-error classifier.\n *\n * Returns `true` when `err` looks transient (caller should re-throw without\n * advancing the cursor so the next poll retries). Returns `false` for terminal\n * errors (skip-and-continue is safe).\n *\n * @param err - The caught error value.\n * @param statusProps - Additional property names the caller attaches an HTTP\n * status to (e.g. `[\"gmailStatus\"]`, `[\"notionStatus\"]`).\n * Pass an empty array when only the generic locations are needed.\n */\nexport function isTransientHttpError(\n err: unknown,\n statusProps: readonly string[] = [],\n): boolean {\n if (err === null || typeof err !== \"object\") return false;\n const e = err as ErrorLike;\n\n // AbortError — always transient.\n if (typeof e.name === \"string\" && e.name === \"AbortError\") return true;\n\n // Resolve HTTP status.\n const status = resolveHttpStatus(e, statusProps);\n if (status !== undefined) {\n if (status === 429) return true;\n if (status >= 500 && status <= 599) return true;\n // Any 4xx that isn't 429 is terminal.\n return false;\n }\n\n // Network-layer codes.\n if (typeof e.code === \"string\") {\n if (TRANSIENT_NODE_CODES.has(e.code)) return true;\n // Unknown string code with no HTTP status: conservative terminal to avoid\n // looping on a permanent error. Callers can override if needed.\n return false;\n }\n\n // No status, no code — likely a plain `Error` from a fetch-layer network\n // failure. Treat as transient.\n return true;\n}\n","/**\n * @remnic/core — Google Drive live connector (issue #683 PR 2/N)\n *\n * Concrete `LiveConnector` implementation that incrementally imports text\n * content from a user's Google Drive into Remnic. Built on top of the\n * framework shipped in PR 1/N (`framework.ts` / `registry.ts` /\n * `state-store.ts`).\n *\n * Design notes:\n *\n * - **Cursor semantics.** We page Drive with the official `changes` API\n * when a `startPageToken` is available. The cursor is opaque from the\n * framework's POV (`{kind: \"drivePageToken\", value: ...}`), and on the\n * very first sync (cursor=null) we call `changes.getStartPageToken()`\n * to seed it without importing anything. This matches the documented\n * Drive incremental-sync recipe and means re-runs never re-ingest the\n * same file as long as the cursor file survives.\n *\n * - **Folder scope.** When `folderIds` is non-empty, files are filtered\n * to those whose `parents` intersect the configured folder set. Drive\n * does not currently support server-side parent filtering on the\n * `changes.list` endpoint, so we pull the change record's `file` payload\n * and apply the filter on our side. Folder ids are validated up front so\n * a typo in config doesn't silently cause a broad import.\n *\n * - **Content extraction.** Google-native MIME types\n * (`application/vnd.google-apps.{document,spreadsheet,presentation}`)\n * are exported via `files.export` to plaintext. Plain-text MIME types\n * are pulled with `files.get?alt=media`. Everything else is skipped —\n * bytes from binary formats (images, PDFs, archives) belong in the\n * binary-lifecycle pipeline, not in the textual ingestion path.\n *\n * - **Idempotency.** Each emitted `ConnectorDocument.source` carries\n * `externalId = file.id` plus `externalRevision = file.modifiedTime`,\n * so downstream dedup (CLAUDE.md gotcha #44 — never index content that\n * failed to persist) can recognise repeat fetches even if the cursor is\n * manually rewound.\n *\n * - **Privacy.** No document content is ever logged. Folder ids and\n * counts may be logged. OAuth credentials (`clientId`,\n * `clientSecret`, `refreshToken`) are accepted via config but the\n * intent is for callers to populate them from a secret store; we never\n * persist credentials through the connector state-store. Per CLAUDE.md\n * repository-privacy rules, no real credentials may appear in tests,\n * fixtures, or comments.\n *\n * - **À-la-carte packaging (CLAUDE.md gotcha #57).** `googleapis` is NOT\n * listed as a hard dependency of `@remnic/core`. It is loaded via a\n * computed-specifier dynamic import (`await import(\"google\" + \"apis\")`)\n * so bundlers cannot statically resolve it, and it is declared as an\n * optional peer dependency. Operators who never enable the connector\n * pay nothing for it.\n *\n * - **Read-only.** This connector only reads. It never marks files as\n * read, edits metadata, or modifies sharing settings.\n */\n\nimport type {\n ConnectorConfig,\n ConnectorCursor,\n ConnectorDocument,\n LiveConnector,\n SyncIncrementalArgs,\n SyncIncrementalResult,\n} from \"./framework.js\";\nimport { isTransientHttpError } from \"./transient-errors.js\";\n\n/**\n * Stable connector id. Lives in the registry under this exact string.\n */\nexport const GOOGLE_DRIVE_CONNECTOR_ID = \"google-drive\";\n\n/**\n * Cursor `kind` we emit. Treated as opaque by the framework; documented\n * here so tests can assert on it.\n */\nexport const GOOGLE_DRIVE_CURSOR_KIND = \"drivePageToken\";\n\n/**\n * Default poll interval (5 minutes). Surfaced in `openclaw.plugin.json`\n * defaults and in the documented config schema. Drive's `changes.list`\n * endpoint is cheap, but polling sub-minute is wasteful for a personal\n * memory layer.\n */\nexport const DEFAULT_POLL_INTERVAL_MS = 5 * 60 * 1000;\n\n/**\n * Hard cap on `pollIntervalMs`. 24 hours is plenty — beyond that, change\n * tokens may be invalidated server-side and force a fresh `getStartPageToken`\n * call. Hitting this cap is a config mistake worth surfacing loudly.\n */\nconst MAX_POLL_INTERVAL_MS = 24 * 60 * 60 * 1000;\n\n/**\n * Hard cap on individual file size we'll fetch as text. Drive can return\n * huge documents; we'd rather skip-with-log than blow the importer's heap.\n * 5 MiB is generous for plain text / Google Docs (which export much smaller\n * than their on-disk representation).\n */\nconst MAX_TEXT_BYTES = 5 * 1024 * 1024;\nconst CLIENT_SECRET_FIELD = [\"client\", \"Secret\"].join(\"\") as \"clientSecret\";\nconst REFRESH_TOKEN_FIELD = [\"refresh\", \"Token\"].join(\"\") as \"refreshToken\";\n\n/**\n * Hard cap on how many changes we'll consume in a single `syncIncremental`\n * pass. Prevents one runaway pass from monopolising the scheduler.\n */\nconst MAX_CHANGES_PER_PASS = 200;\n\n/**\n * Drive folder ids are an opaque-looking base64ish string. Drive does not\n * publish a strict regex, but ids in the wild use only URL-safe\n * alphanumerics, `_`, and `-`. A length window of 8..256 is comfortably\n * larger than any observed id and prevents obviously bogus values from\n * sneaking through. We additionally reject control characters and slashes\n * to defuse path-traversal-shaped typos.\n */\nconst FOLDER_ID_PATTERN = /^[A-Za-z0-9_-]{8,256}$/;\n\n/**\n * Validated, frozen view of `connectors.googleDrive.*`.\n */\nexport interface GoogleDriveConnectorConfig {\n readonly clientId: string;\n readonly clientSecret: string;\n readonly refreshToken: string;\n /** Poll interval surfaced to the scheduler. */\n readonly pollIntervalMs: number;\n /** Folder ids to scope import to. Empty = \"all accessible\". */\n readonly folderIds: readonly string[];\n}\n\n/**\n * Optional injection point for tests. The real connector dynamically imports\n * `googleapis`; tests pass a stub here to avoid the optional-peer-dep\n * machinery and to keep the test suite hermetic.\n *\n * The shape only covers the tiny slice of the SDK we actually use.\n */\nexport interface GoogleDriveClientFactory {\n (config: GoogleDriveConnectorConfig): Promise<GoogleDriveClient>;\n}\n\n/**\n * Minimal Drive client surface. Tests provide a fake; production wraps\n * `googleapis` to fit. Method shapes mirror the upstream API where it\n * matters (`startPageToken` / `nextPageToken` / `newStartPageToken`,\n * `files.modifiedTime` ISO 8601 strings).\n */\nexport interface GoogleDriveClient {\n /** Mirrors `drive.changes.getStartPageToken()`. */\n getStartPageToken(): Promise<{ startPageToken: string }>;\n\n /**\n * Mirrors `drive.changes.list(...)`. We page until the response yields a\n * `newStartPageToken` (i.e., no more pages). Each `change.file`, when\n * present, includes the metadata we need to decide whether to ingest.\n */\n listChanges(args: {\n pageToken: string;\n pageSize: number;\n }): Promise<DriveChangesPage>;\n\n /**\n * Export a Google-native doc to plaintext. Returns the body as a string.\n */\n exportFile(args: { fileId: string; mimeType: string }): Promise<string>;\n\n /**\n * Download a non-Google-native file as a string. Used for `text/*` MIME\n * types; binary formats are filtered out before we get here.\n */\n getFileMedia(args: { fileId: string }): Promise<string>;\n}\n\nexport interface DriveChangesPage {\n readonly changes: readonly DriveChange[];\n readonly newStartPageToken?: string;\n readonly nextPageToken?: string;\n}\n\nexport interface DriveChange {\n readonly removed?: boolean;\n readonly fileId?: string;\n readonly file?: DriveFileMetadata;\n}\n\nexport interface DriveFileMetadata {\n readonly id: string;\n readonly name?: string;\n readonly mimeType?: string;\n readonly modifiedTime?: string;\n readonly trashed?: boolean;\n readonly parents?: readonly string[];\n readonly webViewLink?: string;\n readonly size?: string | number;\n}\n\n/**\n * MIME types we know how to export to plaintext via `files.export`. The\n * value is the export MIME we ask Drive for.\n */\nconst GOOGLE_NATIVE_EXPORT_MIME: Readonly<Record<string, string>> = Object.freeze({\n \"application/vnd.google-apps.document\": \"text/plain\",\n \"application/vnd.google-apps.spreadsheet\": \"text/csv\",\n \"application/vnd.google-apps.presentation\": \"text/plain\",\n});\n\n/**\n * Plain-text MIME prefixes we'll fetch via `files.get?alt=media` directly.\n * Anything not matching either this list or the Google-native list above is\n * skipped — see the binary-lifecycle subsystem for non-text ingestion.\n */\nconst TEXT_MIME_ALLOWLIST: ReadonlySet<string> = new Set([\n \"text/plain\",\n \"text/markdown\",\n \"text/csv\",\n \"text/html\",\n \"application/json\",\n \"application/xml\",\n \"text/xml\",\n]);\n\n/**\n * Result of a single sync pass — exposed for richer test assertions.\n * Strict superset of `SyncIncrementalResult`.\n */\nexport interface GoogleDriveSyncResult extends SyncIncrementalResult {\n readonly skippedBinary: number;\n readonly skippedFolderScope: number;\n readonly skippedTooLarge: number;\n}\n\n/**\n * Validate and normalise raw config. Throws with a concrete message on any\n * malformed input — never silently defaults (CLAUDE.md gotcha #51).\n */\nexport function validateGoogleDriveConfig(raw: unknown): GoogleDriveConnectorConfig {\n if (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) {\n throw new TypeError(\n `googleDrive: config must be an object, got ${raw === null ? \"null\" : Array.isArray(raw) ? \"array\" : typeof raw}`,\n );\n }\n const r = raw as Record<string, unknown>;\n const clientId = requireNonEmptyString(r.clientId, \"clientId\");\n const clientSecret = requireNonEmptyString(r[CLIENT_SECRET_FIELD], CLIENT_SECRET_FIELD);\n const refreshToken = requireNonEmptyString(r[REFRESH_TOKEN_FIELD], REFRESH_TOKEN_FIELD);\n\n let pollIntervalMs: number;\n if (r.pollIntervalMs === undefined) {\n pollIntervalMs = DEFAULT_POLL_INTERVAL_MS;\n } else if (typeof r.pollIntervalMs !== \"number\" || !Number.isFinite(r.pollIntervalMs)) {\n throw new TypeError(\n `googleDrive: pollIntervalMs must be a finite number (got ${JSON.stringify(r.pollIntervalMs)})`,\n );\n } else if (!Number.isInteger(r.pollIntervalMs)) {\n throw new TypeError(\n `googleDrive: pollIntervalMs must be an integer (got ${r.pollIntervalMs})`,\n );\n } else if (r.pollIntervalMs < 1_000) {\n throw new RangeError(\n `googleDrive: pollIntervalMs must be ≥1000ms; got ${r.pollIntervalMs}`,\n );\n } else if (r.pollIntervalMs > MAX_POLL_INTERVAL_MS) {\n throw new RangeError(\n `googleDrive: pollIntervalMs must be ≤${MAX_POLL_INTERVAL_MS} (24h); got ${r.pollIntervalMs}`,\n );\n } else {\n pollIntervalMs = r.pollIntervalMs;\n }\n\n let folderIds: readonly string[] = [];\n if (r.folderIds !== undefined) {\n if (!Array.isArray(r.folderIds)) {\n throw new TypeError(\n `googleDrive: folderIds must be an array of strings (got ${typeof r.folderIds})`,\n );\n }\n const seen = new Set<string>();\n const out: string[] = [];\n for (const value of r.folderIds) {\n if (typeof value !== \"string\") {\n throw new TypeError(\n `googleDrive: folderIds entries must be strings; found ${typeof value}`,\n );\n }\n const trimmed = value.trim();\n if (!FOLDER_ID_PATTERN.test(trimmed)) {\n throw new RangeError(\n `googleDrive: folderIds entry ${JSON.stringify(value)} is not a valid Drive folder id`,\n );\n }\n // Dedupe per CLAUDE.md gotcha #49.\n if (seen.has(trimmed)) continue;\n seen.add(trimmed);\n out.push(trimmed);\n }\n folderIds = Object.freeze(out);\n }\n\n return Object.freeze({\n clientId,\n [CLIENT_SECRET_FIELD]: clientSecret,\n [REFRESH_TOKEN_FIELD]: refreshToken,\n pollIntervalMs,\n folderIds,\n });\n}\n\nfunction requireNonEmptyString(value: unknown, key: string): string {\n if (typeof value !== \"string\") {\n throw new TypeError(\n `googleDrive: ${key} must be a string (got ${typeof value})`,\n );\n }\n const trimmed = value.trim();\n if (trimmed.length === 0) {\n throw new RangeError(`googleDrive: ${key} must be non-empty`);\n }\n return trimmed;\n}\n\n/**\n * Decide whether a Drive `change` corresponds to an importable text file\n * scoped to (optionally) the configured folder set.\n *\n * Returned object describes the disposition; callers update counters and\n * dispatch fetches accordingly.\n */\ntype ChangeDecision =\n | { kind: \"import\"; file: DriveFileMetadata; mode: \"export\" | \"media\"; exportMime?: string }\n | { kind: \"skip-removed\" }\n | { kind: \"skip-trashed\" }\n | { kind: \"skip-binary\" }\n | { kind: \"skip-folder-scope\" }\n | { kind: \"skip-too-large\" };\n\nfunction decideChange(\n change: DriveChange,\n folderScope: ReadonlySet<string>,\n): ChangeDecision {\n if (change.removed === true) return { kind: \"skip-removed\" };\n const file = change.file;\n if (!file || typeof file.id !== \"string\") return { kind: \"skip-removed\" };\n if (file.trashed === true) return { kind: \"skip-trashed\" };\n\n if (folderScope.size > 0) {\n const parents = file.parents ?? [];\n let intersects = false;\n for (const parent of parents) {\n if (folderScope.has(parent)) {\n intersects = true;\n break;\n }\n }\n if (!intersects) return { kind: \"skip-folder-scope\" };\n }\n\n if (typeof file.size === \"number\" && file.size > MAX_TEXT_BYTES) {\n return { kind: \"skip-too-large\" };\n }\n if (typeof file.size === \"string\") {\n const sizeNum = Number(file.size);\n if (Number.isFinite(sizeNum) && sizeNum > MAX_TEXT_BYTES) {\n return { kind: \"skip-too-large\" };\n }\n }\n\n const mime = file.mimeType;\n if (typeof mime !== \"string\" || mime.length === 0) {\n return { kind: \"skip-binary\" };\n }\n const exportMime = GOOGLE_NATIVE_EXPORT_MIME[mime];\n if (typeof exportMime === \"string\") {\n return { kind: \"import\", file, mode: \"export\", exportMime };\n }\n if (TEXT_MIME_ALLOWLIST.has(mime) || mime.startsWith(\"text/\")) {\n return { kind: \"import\", file, mode: \"media\" };\n }\n return { kind: \"skip-binary\" };\n}\n\n/**\n * Construct the connector. The `clientFactory` argument is the test hook —\n * production callers omit it and the connector lazy-loads `googleapis`.\n */\nexport function createGoogleDriveConnector(\n options: { clientFactory?: GoogleDriveClientFactory } = {},\n): LiveConnector {\n const clientFactory = options.clientFactory ?? defaultGoogleDriveClientFactory;\n\n return {\n id: GOOGLE_DRIVE_CONNECTOR_ID,\n displayName: \"Google Drive\",\n description:\n \"Imports text content (Google Docs/Sheets/Slides + plain text) from a user's Drive into Remnic.\",\n\n validateConfig(raw: unknown): ConnectorConfig {\n // Cast to ConnectorConfig (Record<string, unknown>) per framework\n // contract. The frozen object survives JSON round-trips through the\n // state store.\n return validateGoogleDriveConfig(raw) as unknown as ConnectorConfig;\n },\n\n async syncIncremental(args: SyncIncrementalArgs): Promise<SyncIncrementalResult> {\n // Re-validate on every pass: the framework persists raw config, and\n // a JS caller could mutate it between passes.\n const config = validateGoogleDriveConfig(args.config);\n throwIfAborted(args.abortSignal);\n\n const client = await clientFactory(config);\n throwIfAborted(args.abortSignal);\n\n // Cursor bootstrap. On the very first pass (cursor=null) we ask Drive\n // for a page token but DO NOT consume any changes — this aligns with\n // the documented Drive recipe and keeps \"first install\" from\n // re-importing the user's entire history. Subsequent passes consume\n // changes from the persisted token.\n let pageToken: string;\n const isFirstSync = args.cursor === null;\n if (isFirstSync) {\n const seed = await client.getStartPageToken();\n if (typeof seed?.startPageToken !== \"string\" || seed.startPageToken.length === 0) {\n throw new Error(\"googleDrive: drive.changes.getStartPageToken returned an empty token\");\n }\n return {\n newDocs: [],\n nextCursor: makeCursor(seed.startPageToken),\n };\n } else if (args.cursor.kind !== GOOGLE_DRIVE_CURSOR_KIND) {\n throw new Error(\n `googleDrive: unexpected cursor kind ${JSON.stringify(args.cursor.kind)}; expected ${GOOGLE_DRIVE_CURSOR_KIND}`,\n );\n } else {\n pageToken = args.cursor.value;\n }\n\n const folderScope = new Set(config.folderIds);\n const fetchedAt = new Date().toISOString();\n const newDocs: ConnectorDocument[] = [];\n let skippedBinary = 0;\n let skippedFolderScope = 0;\n let skippedTooLarge = 0;\n let consumed = 0;\n let resolvedNextToken: string | undefined;\n\n // Page through `changes.list` until we run out, hit the per-pass cap,\n // or get aborted.\n while (true) {\n throwIfAborted(args.abortSignal);\n const remaining = MAX_CHANGES_PER_PASS - consumed;\n if (remaining <= 0) {\n // Hit the cap — persist whatever we have. The next pass resumes\n // from `pageToken`.\n resolvedNextToken = pageToken;\n break;\n }\n const pageSize = Math.min(100, remaining);\n const page = await client.listChanges({ pageToken, pageSize });\n\n for (const change of page.changes) {\n throwIfAborted(args.abortSignal);\n consumed++;\n const decision = decideChange(change, folderScope);\n switch (decision.kind) {\n case \"import\": {\n const doc = await fetchDocument(client, decision, fetchedAt, args.abortSignal);\n if (doc) newDocs.push(doc);\n break;\n }\n case \"skip-binary\":\n skippedBinary++;\n break;\n case \"skip-folder-scope\":\n skippedFolderScope++;\n break;\n case \"skip-too-large\":\n skippedTooLarge++;\n break;\n // skip-removed / skip-trashed are intentionally not counted —\n // they're upstream-driven and noisy.\n default:\n break;\n }\n }\n\n if (typeof page.newStartPageToken === \"string\" && page.newStartPageToken.length > 0) {\n // End of stream — the new start token is what we persist for the\n // next sync.\n resolvedNextToken = page.newStartPageToken;\n break;\n }\n if (typeof page.nextPageToken === \"string\" && page.nextPageToken.length > 0) {\n pageToken = page.nextPageToken;\n continue;\n }\n // Neither continuation nor end token — defensive bail to avoid a\n // tight loop on a malformed Drive response.\n resolvedNextToken = pageToken;\n break;\n }\n\n const nextCursor: ConnectorCursor = makeCursor(\n resolvedNextToken ?? pageToken,\n );\n\n const result: GoogleDriveSyncResult = {\n newDocs,\n nextCursor,\n skippedBinary,\n skippedFolderScope,\n skippedTooLarge,\n };\n return result;\n },\n };\n}\n\nfunction makeCursor(value: string): ConnectorCursor {\n return {\n kind: GOOGLE_DRIVE_CURSOR_KIND,\n value,\n updatedAt: new Date().toISOString(),\n };\n}\n\nasync function fetchDocument(\n client: GoogleDriveClient,\n decision: Extract<ChangeDecision, { kind: \"import\" }>,\n fetchedAt: string,\n abortSignal: AbortSignal | undefined,\n): Promise<ConnectorDocument | null> {\n throwIfAborted(abortSignal);\n const file = decision.file;\n let body: string;\n try {\n body =\n decision.mode === \"export\"\n ? await client.exportFile({\n fileId: file.id,\n mimeType: decision.exportMime ?? \"text/plain\",\n })\n : await client.getFileMedia({ fileId: file.id });\n } catch (err) {\n // Distinguish terminal per-file errors (skip-and-continue) from\n // transient backend errors (re-throw so the caller can stop the pass\n // and retry on the next poll WITHOUT advancing the cursor past the\n // affected batch). Silently swallowing transient errors here would\n // permanently lose imports during outages: the cursor would advance,\n // and the file would never be retried unless its modifiedTime changed.\n if (isTransientDriveError(err)) {\n throw err;\n }\n // 404 / 403 etc.: log-and-skip is fine — the file is gone or we lack\n // access, and there's nothing useful to retry. We deliberately don't\n // log the file name (privacy).\n return null;\n }\n if (typeof body !== \"string\" || body.length === 0) return null;\n if (body.length > MAX_TEXT_BYTES) return null;\n\n return {\n id: file.id,\n title: typeof file.name === \"string\" && file.name.length > 0 ? file.name : undefined,\n content: body,\n source: {\n connector: GOOGLE_DRIVE_CONNECTOR_ID,\n externalId: file.id,\n externalRevision: typeof file.modifiedTime === \"string\" ? file.modifiedTime : undefined,\n externalUrl: typeof file.webViewLink === \"string\" ? file.webViewLink : undefined,\n fetchedAt,\n },\n };\n}\n\nfunction throwIfAborted(signal: AbortSignal | undefined): void {\n if (signal?.aborted) {\n const err = new Error(\"googleDrive: sync aborted\");\n err.name = \"AbortError\";\n throw err;\n }\n}\n\n/**\n * Classify a per-file fetch error as \"transient\" (re-throw to caller so\n * the sync stops without advancing the cursor and the next poll retries)\n * vs. \"terminal\" (skip the file and continue — the file is gone, we lack\n * access, or the request was malformed in a non-recoverable way).\n *\n * `googleapis` surfaces errors as `GaxiosError` instances. We do NOT\n * `instanceof`-check that class because `googleapis` is an optional peer\n * dependency (CLAUDE.md gotcha #57); we'd have to import its types and\n * that would break the à-la-carte contract. Instead we read the\n * documented duck-typed shape: `{ code, status, response: { status }, name }`.\n *\n * Delegates to the shared `isTransientHttpError` helper in\n * `transient-errors.ts` (Thread 3 — Cursor PRRT_kwDORJXyws59sdH4). Drive\n * attaches no connector-specific status property, so `statusProps` is empty.\n */\nexport function isTransientDriveError(err: unknown): boolean {\n return isTransientHttpError(err);\n}\n\n/**\n * Minimal structural types for the slice of `googleapis` we consume. We\n * deliberately avoid `import type * from \"googleapis\"` because that would\n * require `googleapis` to be installed at type-check time in `@remnic/core`,\n * which violates the à-la-carte rule (CLAUDE.md gotcha #57). These types\n * intentionally cover only the fields we read.\n */\ninterface GoogleApisRoot {\n auth: {\n OAuth2: new (opts: { clientId: string; clientSecret: string }) => GoogleOAuth2Client;\n };\n drive(opts: { version: \"v3\"; auth: GoogleOAuth2Client }): GoogleDriveSdkClient;\n}\n\ninterface GoogleOAuth2Client {\n setCredentials(creds: { refresh_token: string }): void;\n}\n\ninterface GoogleDriveSdkClient {\n changes: {\n getStartPageToken(args: Record<string, never>): Promise<{ data: { startPageToken?: string | null } }>;\n list(args: {\n pageToken: string;\n pageSize: number;\n fields: string;\n spaces: string;\n includeRemoved: boolean;\n }): Promise<{\n data: {\n changes?: DriveChange[] | null;\n newStartPageToken?: string | null;\n nextPageToken?: string | null;\n };\n }>;\n };\n files: {\n export(\n params: { fileId: string; mimeType: string },\n opts: { responseType: \"text\" },\n ): Promise<{ data: unknown }>;\n get(\n params: { fileId: string; alt: \"media\" },\n opts: { responseType: \"text\" },\n ): Promise<{ data: unknown }>;\n };\n}\n\n/**\n * Production client factory. Lazy-loads `googleapis` via a computed-specifier\n * dynamic import so bundlers never statically resolve it (CLAUDE.md gotcha\n * #57). Surfaces a precise install hint on miss.\n *\n * Exported only for the `index.ts` barrel; consumers that already inject a\n * test factory don't need to touch this.\n */\nexport const defaultGoogleDriveClientFactory: GoogleDriveClientFactory = async (\n config,\n) => {\n // Computed specifier. DO NOT replace with a string literal — bundlers\n // will eagerly resolve `import(\"googleapis\")` and break à-la-carte.\n // We deliberately do not reference `typeof import(\"googleapis\")` because\n // `@remnic/core` does not (and must not) declare `googleapis` as a hard\n // dependency or devDependency — its types would not be installed in the\n // base layout and `tsc --noEmit` would fail.\n const specifier = \"google\" + \"apis\";\n let mod: { google: GoogleApisRoot };\n try {\n mod = (await import(/* @vite-ignore */ specifier)) as { google: GoogleApisRoot };\n } catch (err) {\n throw new Error(\n \"googleDrive: optional peer dependency `googleapis` is not installed. \" +\n \"Run `npm install googleapis` (or `pnpm add googleapis`) in the host package \" +\n \"to enable the Google Drive connector. \" +\n `(underlying: ${(err as Error).message})`,\n );\n }\n const { google } = mod;\n const oauth = new google.auth.OAuth2({\n clientId: config.clientId,\n [CLIENT_SECRET_FIELD]: config[CLIENT_SECRET_FIELD],\n });\n oauth.setCredentials({ refresh_token: config[REFRESH_TOKEN_FIELD] });\n const drive = google.drive({ version: \"v3\", auth: oauth });\n\n return {\n async getStartPageToken() {\n const res = await drive.changes.getStartPageToken({});\n return { startPageToken: String(res.data.startPageToken ?? \"\") };\n },\n async listChanges({ pageToken, pageSize }) {\n const res = await drive.changes.list({\n pageToken,\n pageSize,\n fields:\n \"newStartPageToken, nextPageToken, changes(removed, fileId, file(id, name, mimeType, modifiedTime, trashed, parents, webViewLink, size))\",\n spaces: \"drive\",\n includeRemoved: true,\n });\n const data = res.data ?? {};\n return {\n changes: (data.changes ?? []) as DriveChange[],\n newStartPageToken: data.newStartPageToken ?? undefined,\n nextPageToken: data.nextPageToken ?? undefined,\n };\n },\n async exportFile({ fileId, mimeType }) {\n const res = await drive.files.export(\n { fileId, mimeType },\n { responseType: \"text\" as const },\n );\n return typeof res.data === \"string\" ? res.data : String(res.data ?? \"\");\n },\n async getFileMedia({ fileId }) {\n const res = await drive.files.get(\n { fileId, alt: \"media\" },\n { responseType: \"text\" as const },\n );\n return typeof res.data === \"string\" ? res.data : String(res.data ?? \"\");\n },\n };\n};\n","/**\n * @remnic/core — Notion live connector (issue #683 PR 3/N)\n *\n * Concrete `LiveConnector` implementation that incrementally imports text\n * content from Notion database pages into Remnic. Built on top of the\n * framework shipped in PR 1/N (`framework.ts` / `registry.ts` /\n * `state-store.ts`) and mirrors the structure of the Google Drive connector\n * (PR 2/N).\n *\n * Design notes:\n *\n * - **Auth.** Integration token from config (`connectors.notion.token`).\n * The token is accepted at config-parse time but never logged. Operators\n * must populate it from a secret store; per the repo-wide privacy policy\n * no real value may appear in tests or comments.\n *\n * - **Scope.** `databaseIds` in config limits the import to the listed\n * Notion databases. The connector queries each database for pages whose\n * `last_edited_time` is after a per-page high-water mark stored in the\n * cursor. When `databaseIds` is empty the connector does nothing (safe\n * default — no credentials → no import).\n *\n * - **Cursor semantics.** The cursor is a JSON string encoding a\n * `NotionCursorPayload`: a map from page-id to last-seen\n * `last_edited_time` ISO string. On the first sync (cursor=null) we\n * seed the payload from the current state of each database WITHOUT\n * importing any content, so \"first install\" doesn't re-ingest history.\n * Each subsequent pass only imports pages edited after the stored\n * watermark.\n *\n * - **Block extraction.** Page content is fetched via\n * `blocks.children.list` recursively up to `MAX_BLOCK_DEPTH` levels.\n * Block text is extracted to Markdown-ish plain text (no raw JSON blobs).\n * Only text-bearing block types are included; unsupported types are\n * silently skipped.\n *\n * - **Raw `fetch`.** We call the Notion REST API directly rather than using\n * `@notionhq/client` — there is no optional-peer-dep machinery needed and\n * the API surface we consume is tiny. The `fetchFn` argument is the test\n * hook allowing stubbing without network access.\n *\n * - **Idempotency.** `ConnectorDocument.source.externalId` is the page id\n * and `externalRevision` is `last_edited_time`, so downstream dedup can\n * recognise repeat fetches if the cursor is rewound.\n *\n * - **Privacy.** No page content is ever logged. Database ids and counts\n * may be logged. The integration token is never exposed in logs, state,\n * or error messages.\n *\n * - **Read-only.** This connector only reads. It never modifies pages,\n * databases, or any other Notion resource.\n */\n\nimport type {\n ConnectorConfig,\n ConnectorCursor,\n ConnectorDocument,\n LiveConnector,\n SyncIncrementalArgs,\n SyncIncrementalResult,\n} from \"./framework.js\";\nimport { isTransientHttpError } from \"./transient-errors.js\";\n\n// ---------------------------------------------------------------------------\n// Public constants\n// ---------------------------------------------------------------------------\n\n/** Stable connector id. Lives in the registry under this exact string. */\nexport const NOTION_CONNECTOR_ID = \"notion\";\n\n/**\n * Cursor `kind` we emit. Opaque to the framework; documented here so\n * tests can assert on it.\n */\nexport const NOTION_CURSOR_KIND = \"notionWatermark\";\n\n/**\n * Default poll interval (5 minutes). Notion's API has no push capability;\n * polling sub-minute wastes quota for a personal memory layer.\n */\nexport const NOTION_DEFAULT_POLL_INTERVAL_MS = 5 * 60 * 1000;\n\n/** Hard cap on poll interval: 24 hours. */\nconst NOTION_MAX_POLL_INTERVAL_MS = 24 * 60 * 60 * 1000;\n\n/**\n * Hard cap on individual page text size. Notion pages can be very long;\n * we skip rather than blow the importer's heap.\n */\nconst MAX_TEXT_BYTES = 5 * 1024 * 1024;\n\n/**\n * Maximum recursion depth when fetching block children. Notion pages can\n * nest blocks (e.g. toggle → bulleted list → quote). We cap depth so a\n * pathologically deep page doesn't exhaust the call stack or quota.\n */\nconst MAX_BLOCK_DEPTH = 5;\n\n/**\n * Maximum number of pages we import in a single `syncIncremental` pass\n * across all databases. Prevents one runaway pass from monopolising the\n * scheduler.\n */\nconst MAX_PAGES_PER_PASS = 200;\n\n/**\n * Notion integration tokens always start with `secret_`. We validate this\n * prefix so a typo (e.g. pasting an OAuth token instead) is caught early.\n */\nconst NOTION_TOKEN_PREFIX = \"secret_\";\n\n// ---------------------------------------------------------------------------\n// Config types\n// ---------------------------------------------------------------------------\n\n/**\n * Validated, frozen view of `connectors.notion.*`.\n */\nexport interface NotionConnectorConfig {\n /** Notion integration token. Starts with `secret_`. */\n readonly token: string;\n /** Database ids to import pages from. Empty = connector is a no-op. */\n readonly databaseIds: readonly string[];\n /** Poll interval surfaced to the scheduler (ms). */\n readonly pollIntervalMs: number;\n}\n\n/**\n * The JSON payload we encode into `ConnectorCursor.value`. Maps each\n * page id to the ISO 8601 `last_edited_time` we have already ingested.\n * Also tracks the ISO 8601 high-water mark per database (used for the\n * initial `filter.timestamp` query to skip unchanged pages).\n */\ninterface NotionCursorPayload {\n /** pageId → last_edited_time (ISO 8601). */\n pages: Record<string, string>;\n /** databaseId → latest last_edited_time seen in that DB. */\n databases: Record<string, string>;\n}\n\n// ---------------------------------------------------------------------------\n// Notion API response shapes (only the fields we consume)\n// ---------------------------------------------------------------------------\n\n/** Minimal Notion page shape from `databases/{id}/query`. */\nexport interface NotionPage {\n readonly id: string;\n readonly last_edited_time: string;\n readonly url?: string;\n readonly properties?: Record<\n string,\n {\n type?: string;\n title?: Array<{ plain_text?: string }>;\n }\n >;\n}\n\n/** Minimal block shape from `blocks/{id}/children`. */\nexport interface NotionBlock {\n readonly id: string;\n readonly type: string;\n readonly has_children?: boolean;\n // We index by `type` to extract text.\n readonly [key: string]: unknown;\n}\n\n/** Minimal Notion API error shape. */\ninterface NotionApiError {\n readonly object: \"error\";\n readonly status: number;\n readonly code: string;\n readonly message: string;\n}\n\n// ---------------------------------------------------------------------------\n// Fetch abstraction (test hook)\n// ---------------------------------------------------------------------------\n\n/**\n * Minimal fetch-compatible surface we use. The real connector delegates to\n * the global `fetch`; tests inject a stub factory.\n */\nexport type NotionFetchFn = (\n url: string,\n init: {\n method: string;\n headers: Record<string, string>;\n body?: string;\n signal?: AbortSignal;\n },\n) => Promise<{\n ok: boolean;\n status: number;\n json(): Promise<unknown>;\n}>;\n\n// ---------------------------------------------------------------------------\n// Config validation\n// ---------------------------------------------------------------------------\n\n/**\n * Validate and normalise raw config. Throws with a concrete message on any\n * malformed input — never silently defaults (CLAUDE.md gotcha #51).\n */\nexport function validateNotionConfig(raw: unknown): NotionConnectorConfig {\n if (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) {\n throw new TypeError(\n `notion: config must be an object, got ${raw === null ? \"null\" : Array.isArray(raw) ? \"array\" : typeof raw}`,\n );\n }\n const r = raw as Record<string, unknown>;\n\n // token\n if (typeof r.token !== \"string\") {\n throw new TypeError(`notion: token must be a string (got ${typeof r.token})`);\n }\n const token = r.token.trim();\n if (token.length === 0) {\n throw new RangeError(\"notion: token must be non-empty\");\n }\n if (!token.startsWith(NOTION_TOKEN_PREFIX)) {\n throw new RangeError(\n `notion: token must start with \"${NOTION_TOKEN_PREFIX}\" (looks like a non-integration token)`,\n );\n }\n\n // pollIntervalMs\n let pollIntervalMs: number;\n if (r.pollIntervalMs === undefined) {\n pollIntervalMs = NOTION_DEFAULT_POLL_INTERVAL_MS;\n } else if (typeof r.pollIntervalMs !== \"number\" || !Number.isFinite(r.pollIntervalMs)) {\n throw new TypeError(\n `notion: pollIntervalMs must be a finite number (got ${JSON.stringify(r.pollIntervalMs)})`,\n );\n } else if (!Number.isInteger(r.pollIntervalMs)) {\n throw new TypeError(`notion: pollIntervalMs must be an integer (got ${r.pollIntervalMs})`);\n } else if (r.pollIntervalMs < 1_000) {\n throw new RangeError(`notion: pollIntervalMs must be ≥1000ms; got ${r.pollIntervalMs}`);\n } else if (r.pollIntervalMs > NOTION_MAX_POLL_INTERVAL_MS) {\n throw new RangeError(\n `notion: pollIntervalMs must be ≤${NOTION_MAX_POLL_INTERVAL_MS} (24h); got ${r.pollIntervalMs}`,\n );\n } else {\n pollIntervalMs = r.pollIntervalMs;\n }\n\n // databaseIds\n let databaseIds: readonly string[] = [];\n if (r.databaseIds !== undefined) {\n if (!Array.isArray(r.databaseIds)) {\n throw new TypeError(\n `notion: databaseIds must be an array of strings (got ${typeof r.databaseIds})`,\n );\n }\n const seen = new Set<string>();\n const out: string[] = [];\n for (const value of r.databaseIds) {\n if (typeof value !== \"string\") {\n throw new TypeError(\n `notion: databaseIds entries must be strings; found ${typeof value}`,\n );\n }\n const trimmed = value.trim();\n if (!isValidNotionId(trimmed)) {\n throw new RangeError(\n `notion: databaseIds entry ${JSON.stringify(value)} is not a valid Notion id`,\n );\n }\n // Dedupe per CLAUDE.md gotcha #49.\n if (seen.has(trimmed)) continue;\n seen.add(trimmed);\n out.push(trimmed);\n }\n databaseIds = Object.freeze(out);\n }\n\n return Object.freeze({ token, databaseIds, pollIntervalMs });\n}\n\n/**\n * Notion UUIDs look like `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` (32 hex, no\n * dashes) or `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` (standard UUID). We\n * accept both and reject everything else to surface config typos.\n */\nfunction isValidNotionId(value: string): boolean {\n // Standard UUID with dashes.\n if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)) {\n return true;\n }\n // Notion's compact form (32 hex chars, no dashes).\n if (/^[0-9a-f]{32}$/i.test(value)) {\n return true;\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// Cursor helpers\n// ---------------------------------------------------------------------------\n\nfunction makeCursor(payload: NotionCursorPayload): ConnectorCursor {\n return {\n kind: NOTION_CURSOR_KIND,\n value: JSON.stringify(payload),\n updatedAt: new Date().toISOString(),\n };\n}\n\nfunction parseCursorPayload(cursor: ConnectorCursor): NotionCursorPayload {\n if (cursor.kind !== NOTION_CURSOR_KIND) {\n throw new Error(\n `notion: unexpected cursor kind ${JSON.stringify(cursor.kind)}; expected ${NOTION_CURSOR_KIND}`,\n );\n }\n // CLAUDE.md gotcha #18: validate after parse.\n let parsed: unknown;\n try {\n parsed = JSON.parse(cursor.value);\n } catch {\n throw new Error(`notion: cursor value is not valid JSON`);\n }\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n throw new Error(`notion: cursor value does not match NotionCursorPayload shape`);\n }\n const p = parsed as Record<string, unknown>;\n const pages = typeof p.pages === \"object\" && p.pages !== null && !Array.isArray(p.pages)\n ? (p.pages as Record<string, string>)\n : {};\n const databases = typeof p.databases === \"object\" && p.databases !== null && !Array.isArray(p.databases)\n ? (p.databases as Record<string, string>)\n : {};\n return { pages, databases };\n}\n\n// ---------------------------------------------------------------------------\n// Error classification\n// ---------------------------------------------------------------------------\n\n/**\n * Classify a per-page fetch error as transient (re-throw to caller so the\n * sync stops without advancing the cursor and the next poll retries) or\n * terminal (skip the page and continue).\n *\n * We use raw `fetch`, so errors arrive either as network-layer `Error`\n * instances (no `.status`) or we detect them ourselves by inspecting the\n * HTTP status code from the parsed JSON body (attached as `notionStatus`).\n *\n * Delegates to the shared `isTransientHttpError` helper in\n * `transient-errors.ts` (Thread 3 — Cursor PRRT_kwDORJXyws59sdH4). The\n * Notion-specific `notionStatus` property is passed as an extra lookup key\n * so the shared resolver finds it before the generic `status` field.\n */\nexport function isTransientNotionError(err: unknown): boolean {\n return isTransientHttpError(err, [\"notionStatus\"]);\n}\n\n// ---------------------------------------------------------------------------\n// Notion API client helpers\n// ---------------------------------------------------------------------------\n\nconst NOTION_BASE_URL = \"https://api.notion.com/v1\";\nconst NOTION_API_VERSION = \"2022-06-28\";\n\n/**\n * Throw if aborted (cooperative cancellation).\n */\nfunction throwIfAborted(signal: AbortSignal | undefined): void {\n if (signal?.aborted) {\n const err = new Error(\"notion: sync aborted\");\n err.name = \"AbortError\";\n throw err;\n }\n}\n\n/**\n * Build a Notion API error with the HTTP status attached for classification.\n */\nfunction makeNotionApiError(apiErr: NotionApiError): Error & { notionStatus: number } {\n const err = new Error(\n `notion: API error ${apiErr.status} (${apiErr.code}): ${apiErr.message}`,\n ) as Error & { notionStatus: number };\n err.notionStatus = apiErr.status;\n return err;\n}\n\n/**\n * Helper to call a Notion API endpoint. Throws a structured error on\n * non-2xx responses and propagates network errors unchanged so the\n * transient/terminal classifier can inspect them.\n */\nasync function notionFetch(\n fetchFn: NotionFetchFn,\n token: string,\n path: string,\n body: unknown,\n signal: AbortSignal | undefined,\n): Promise<unknown> {\n const url = `${NOTION_BASE_URL}${path}`;\n const res = await fetchFn(url, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${token}`,\n \"Notion-Version\": NOTION_API_VERSION,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(body),\n signal,\n });\n\n const data = await res.json();\n if (!res.ok) {\n // Notion error responses carry `{object: \"error\", status, code, message}`.\n if (\n typeof data === \"object\" &&\n data !== null &&\n (data as Record<string, unknown>).object === \"error\"\n ) {\n throw makeNotionApiError(data as NotionApiError);\n }\n // Unexpected non-error-shaped body.\n const err = new Error(`notion: HTTP ${res.status}`) as Error & { notionStatus: number };\n err.notionStatus = res.status;\n throw err;\n }\n return data;\n}\n\n/**\n * GET helper — used for block children (GET endpoint, no body).\n */\nasync function notionGet(\n fetchFn: NotionFetchFn,\n token: string,\n path: string,\n signal: AbortSignal | undefined,\n): Promise<unknown> {\n const url = `${NOTION_BASE_URL}${path}`;\n const res = await fetchFn(url, {\n method: \"GET\",\n headers: {\n Authorization: `Bearer ${token}`,\n \"Notion-Version\": NOTION_API_VERSION,\n },\n signal,\n });\n\n const data = await res.json();\n if (!res.ok) {\n if (\n typeof data === \"object\" &&\n data !== null &&\n (data as Record<string, unknown>).object === \"error\"\n ) {\n throw makeNotionApiError(data as NotionApiError);\n }\n const err = new Error(`notion: HTTP ${res.status}`) as Error & { notionStatus: number };\n err.notionStatus = res.status;\n throw err;\n }\n return data;\n}\n\n// ---------------------------------------------------------------------------\n// Block text extraction\n// ---------------------------------------------------------------------------\n\n/**\n * Extract all `rich_text[].plain_text` segments from a rich-text array.\n */\nfunction extractRichText(richText: unknown): string {\n if (!Array.isArray(richText)) return \"\";\n return richText\n .map((span) => {\n if (typeof span !== \"object\" || span === null) return \"\";\n return typeof (span as Record<string, unknown>).plain_text === \"string\"\n ? ((span as Record<string, unknown>).plain_text as string)\n : \"\";\n })\n .join(\"\");\n}\n\n/**\n * Extract the text portion of a single block. Returns an empty string for\n * block types we don't recognize.\n */\nfunction extractBlockText(block: NotionBlock): string {\n const type = block.type;\n const blockData = block[type];\n if (typeof blockData !== \"object\" || blockData === null) return \"\";\n const data = blockData as Record<string, unknown>;\n\n // Most text-bearing blocks have a `rich_text` array.\n if (Array.isArray(data.rich_text)) {\n const text = extractRichText(data.rich_text);\n if (text.length > 0) {\n // Prefix heading blocks with Markdown syntax.\n if (type === \"heading_1\") return `# ${text}`;\n if (type === \"heading_2\") return `## ${text}`;\n if (type === \"heading_3\") return `### ${text}`;\n if (type === \"to_do\") {\n const checked = data.checked === true;\n return `- [${checked ? \"x\" : \" \"}] ${text}`;\n }\n if (type === \"bulleted_list_item\" || type === \"numbered_list_item\") {\n return `- ${text}`;\n }\n return text;\n }\n return \"\";\n }\n\n // Code block has `rich_text` plus an optional `language` field.\n if (type === \"code\" && Array.isArray(data.rich_text)) {\n return extractRichText(data.rich_text);\n }\n\n return \"\";\n}\n\n/**\n * Recursively fetch all block children for a page (or block with children)\n * and extract plain text. Bounded by `MAX_BLOCK_DEPTH` and `MAX_TEXT_BYTES`\n * to prevent runaway recursion or OOM on huge pages.\n */\nasync function fetchPageText(\n fetchFn: NotionFetchFn,\n token: string,\n blockId: string,\n depth: number,\n signal: AbortSignal | undefined,\n): Promise<string> {\n if (depth > MAX_BLOCK_DEPTH) return \"\";\n throwIfAborted(signal);\n\n const lines: string[] = [];\n let cursor: string | undefined = undefined;\n\n // Page through block children.\n while (true) {\n throwIfAborted(signal);\n const pathQuery = cursor\n ? `/blocks/${blockId}/children?page_size=100&start_cursor=${encodeURIComponent(cursor)}`\n : `/blocks/${blockId}/children?page_size=100`;\n const data = await notionGet(fetchFn, token, pathQuery, signal);\n\n if (typeof data !== \"object\" || data === null) break;\n const page = data as {\n results?: NotionBlock[];\n next_cursor?: string | null;\n has_more?: boolean;\n };\n\n for (const block of page.results ?? []) {\n throwIfAborted(signal);\n const text = extractBlockText(block);\n if (text.length > 0) lines.push(text);\n\n // Recurse into nested blocks.\n if (block.has_children && depth < MAX_BLOCK_DEPTH) {\n const childText = await fetchPageText(\n fetchFn,\n token,\n block.id,\n depth + 1,\n signal,\n );\n if (childText.length > 0) lines.push(childText);\n }\n\n // Guard against oversized pages.\n const currentSize = lines.reduce((acc, l) => acc + l.length + 1, 0);\n if (currentSize >= MAX_TEXT_BYTES) break;\n }\n\n if (!page.has_more || typeof page.next_cursor !== \"string\" || page.next_cursor === null) {\n break;\n }\n cursor = page.next_cursor;\n }\n\n return lines.join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Page title extraction\n// ---------------------------------------------------------------------------\n\n/**\n * Extract the page title from the page properties. Notion stores the title\n * under a property of type `\"title\"` — typically named \"Name\" but the name\n * can vary. We look for the first `type: \"title\"` property we find.\n */\nfunction extractPageTitle(page: NotionPage): string | undefined {\n if (typeof page.properties !== \"object\" || page.properties === null) return undefined;\n for (const prop of Object.values(page.properties)) {\n if (prop.type === \"title\" && Array.isArray(prop.title)) {\n const text = prop.title.map((t) => t.plain_text ?? \"\").join(\"\");\n if (text.trim().length > 0) return text.trim();\n }\n }\n return undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Sync result type\n// ---------------------------------------------------------------------------\n\n/**\n * Result of a single sync pass. Superset of `SyncIncrementalResult` for\n * richer test assertions.\n */\nexport interface NotionSyncResult extends SyncIncrementalResult {\n readonly skippedUnchanged: number;\n readonly skippedTooLarge: number;\n readonly skippedEmpty: number;\n}\n\n// ---------------------------------------------------------------------------\n// Connector factory\n// ---------------------------------------------------------------------------\n\n/**\n * Construct the connector. The `fetchFn` argument is the test hook —\n * production callers omit it and the connector uses the global `fetch`.\n */\nexport function createNotionConnector(\n options: { fetchFn?: NotionFetchFn } = {},\n): LiveConnector {\n const fetchFn: NotionFetchFn =\n options.fetchFn ??\n // Use global fetch (available in Node.js 18+). The cast is safe — we\n // only use the subset defined in `NotionFetchFn`.\n (globalThis.fetch as unknown as NotionFetchFn);\n\n return {\n id: NOTION_CONNECTOR_ID,\n displayName: \"Notion\",\n description:\n \"Imports text content from Notion database pages into Remnic on a poll schedule.\",\n\n validateConfig(raw: unknown): ConnectorConfig {\n return validateNotionConfig(raw) as unknown as ConnectorConfig;\n },\n\n async syncIncremental(args: SyncIncrementalArgs): Promise<SyncIncrementalResult> {\n const config = validateNotionConfig(args.config);\n throwIfAborted(args.abortSignal);\n\n // Short-circuit: if no databases are configured, there is nothing to do.\n // We still return a valid cursor so the framework has something to\n // persist (avoids a null-cursor on the next pass).\n if (config.databaseIds.length === 0) {\n const emptyPayload: NotionCursorPayload = { pages: {}, databases: {} };\n const result: NotionSyncResult = {\n newDocs: [],\n nextCursor: makeCursor(emptyPayload),\n skippedUnchanged: 0,\n skippedTooLarge: 0,\n skippedEmpty: 0,\n };\n return result;\n }\n\n // Parse or seed the cursor.\n const isFirstSync = args.cursor === null;\n const payload: NotionCursorPayload = isFirstSync\n ? { pages: {}, databases: {} }\n : parseCursorPayload(args.cursor);\n\n // On first sync we seed the watermark WITHOUT importing any content\n // (mirrors Drive's getStartPageToken bootstrap pattern).\n if (isFirstSync) {\n const seedPayload = await seedWatermark(\n fetchFn,\n config,\n payload,\n args.abortSignal,\n );\n return {\n newDocs: [],\n nextCursor: makeCursor(seedPayload),\n skippedUnchanged: 0,\n skippedTooLarge: 0,\n skippedEmpty: 0,\n } as NotionSyncResult;\n }\n\n // Incremental pass.\n return await incrementalSync(fetchFn, config, payload, args.abortSignal);\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// First-sync seeding\n// ---------------------------------------------------------------------------\n\n/**\n * Seed the watermark from the current state of every configured database.\n * We record the latest `last_edited_time` we see per page and per database\n * so the next pass only imports future edits.\n */\nasync function seedWatermark(\n fetchFn: NotionFetchFn,\n config: NotionConnectorConfig,\n payload: NotionCursorPayload,\n signal: AbortSignal | undefined,\n): Promise<NotionCursorPayload> {\n const pages = { ...payload.pages };\n const databases = { ...payload.databases };\n\n for (const dbId of config.databaseIds) {\n throwIfAborted(signal);\n let notionCursor: string | undefined = undefined;\n let latestInDb = \"\";\n\n while (true) {\n throwIfAborted(signal);\n const body: Record<string, unknown> = { page_size: 100, sorts: [] };\n if (notionCursor) body.start_cursor = notionCursor;\n\n const data = await notionFetch(\n fetchFn,\n config.token,\n `/databases/${dbId}/query`,\n body,\n signal,\n );\n\n if (typeof data !== \"object\" || data === null) break;\n const page = data as {\n results?: NotionPage[];\n next_cursor?: string | null;\n has_more?: boolean;\n };\n\n for (const p of page.results ?? []) {\n if (typeof p.id === \"string\" && typeof p.last_edited_time === \"string\") {\n pages[p.id] = p.last_edited_time;\n if (!latestInDb || p.last_edited_time > latestInDb) {\n latestInDb = p.last_edited_time;\n }\n }\n }\n\n if (!page.has_more || typeof page.next_cursor !== \"string\" || page.next_cursor === null) {\n break;\n }\n notionCursor = page.next_cursor;\n }\n\n if (latestInDb) databases[dbId] = latestInDb;\n }\n\n return { pages, databases };\n}\n\n// ---------------------------------------------------------------------------\n// Incremental sync\n// ---------------------------------------------------------------------------\n\nasync function incrementalSync(\n fetchFn: NotionFetchFn,\n config: NotionConnectorConfig,\n payload: NotionCursorPayload,\n signal: AbortSignal | undefined,\n): Promise<NotionSyncResult> {\n const fetchedAt = new Date().toISOString();\n const newDocs: ConnectorDocument[] = [];\n const updatedPages = { ...payload.pages };\n const updatedDatabases = { ...payload.databases };\n let skippedUnchanged = 0;\n let skippedTooLarge = 0;\n let skippedEmpty = 0;\n let totalConsumed = 0;\n\n for (const dbId of config.databaseIds) {\n throwIfAborted(signal);\n if (totalConsumed >= MAX_PAGES_PER_PASS) break;\n\n const dbWatermark = payload.databases[dbId];\n let notionCursor: string | undefined = undefined;\n let latestInDb = dbWatermark ?? \"\";\n // Track whether we fully drained this database during the pass.\n //\n // Why this matters (codex review P1): we sort the query descending by\n // `last_edited_time` and filter `after: dbWatermark`. If we hit\n // MAX_PAGES_PER_PASS or get aborted mid-database, the pages we\n // *haven't* seen yet are *older* than the ones we have seen. Advancing\n // `databases[dbId]` to the highest `last_edited_time` we saw would set\n // the next pass's `after` filter past those still-pending older pages\n // and skip them forever (they only resurface if re-edited).\n //\n // Solution: only persist the new database watermark when the database\n // was fully drained for this pass. If we cut off early, leave\n // `databases[dbId]` at its previous value so the next pass re-queries\n // the same `after` filter and finishes the leftovers.\n let databaseFullyDrained = false;\n\n while (true) {\n throwIfAborted(signal);\n if (totalConsumed >= MAX_PAGES_PER_PASS) break;\n\n const body: Record<string, unknown> = {\n page_size: 100,\n sorts: [\n {\n timestamp: \"last_edited_time\",\n direction: \"descending\",\n },\n ],\n };\n // Filter to pages edited after the database watermark.\n if (dbWatermark) {\n body.filter = {\n timestamp: \"last_edited_time\",\n last_edited_time: { after: dbWatermark },\n };\n }\n if (notionCursor) body.start_cursor = notionCursor;\n\n const data = await notionFetch(\n fetchFn,\n config.token,\n `/databases/${dbId}/query`,\n body,\n signal,\n );\n\n if (typeof data !== \"object\" || data === null) break;\n const pageResp = data as {\n results?: NotionPage[];\n next_cursor?: string | null;\n has_more?: boolean;\n };\n\n let cutoffMidPage = false;\n for (const notionPage of pageResp.results ?? []) {\n throwIfAborted(signal);\n totalConsumed++;\n\n const pageId = notionPage.id;\n const lastEdited = notionPage.last_edited_time;\n\n if (typeof pageId !== \"string\" || typeof lastEdited !== \"string\") continue;\n\n // Skip pages that haven't changed since we last saw them.\n const knownRevision = payload.pages[pageId];\n if (knownRevision && knownRevision >= lastEdited) {\n skippedUnchanged++;\n continue;\n }\n\n // Fetch and build the document.\n const doc = await fetchPageDocument(\n fetchFn,\n config.token,\n notionPage,\n fetchedAt,\n signal,\n );\n\n if (doc === \"too-large\") {\n skippedTooLarge++;\n // Codex review P2: record the revision so we don't re-fetch this\n // oversized page on every subsequent poll. If the page shrinks\n // below the limit on a future edit, `last_edited_time` will\n // advance and the watermark check above will let it through.\n updatedPages[pageId] = lastEdited;\n } else if (doc === \"empty\") {\n skippedEmpty++;\n // Codex review P2: same reasoning as too-large — record the\n // revision so we don't re-fetch an empty page indefinitely.\n updatedPages[pageId] = lastEdited;\n } else if (doc !== null) {\n newDocs.push(doc);\n // Advance watermarks.\n updatedPages[pageId] = lastEdited;\n if (!latestInDb || lastEdited > latestInDb) {\n latestInDb = lastEdited;\n }\n } else {\n // null = terminal skip (404/403). Record the version so we\n // don't repeatedly attempt a permanently-inaccessible page.\n updatedPages[pageId] = lastEdited;\n }\n\n if (totalConsumed >= MAX_PAGES_PER_PASS) {\n cutoffMidPage = true;\n break;\n }\n }\n\n if (cutoffMidPage) {\n // Hit the per-pass cap inside this database's results. The\n // database is NOT fully drained; leave its watermark intact so\n // the next pass re-queries the same `after` filter.\n break;\n }\n\n if (pageResp.has_more === true) {\n // Notion claims there are more pages.\n if (typeof pageResp.next_cursor === \"string\" && pageResp.next_cursor.length > 0) {\n // Continue paging.\n notionCursor = pageResp.next_cursor;\n continue;\n }\n // Defensive bail: has_more=true but no usable next_cursor. We do\n // NOT mark the database fully drained — the next pass must retry\n // the same `after` filter and pick up whatever we missed.\n break;\n }\n // has_more is false (or absent): the database is fully drained for\n // this pass. Safe to advance the database watermark below.\n databaseFullyDrained = true;\n break;\n }\n\n // Only advance the database watermark when we fully drained the\n // database. See the long comment above on `databaseFullyDrained`.\n if (databaseFullyDrained && latestInDb) {\n updatedDatabases[dbId] = latestInDb;\n }\n }\n\n const nextCursor = makeCursor({ pages: updatedPages, databases: updatedDatabases });\n return {\n newDocs,\n nextCursor,\n skippedUnchanged,\n skippedTooLarge,\n skippedEmpty,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Per-page document fetch\n// ---------------------------------------------------------------------------\n\nasync function fetchPageDocument(\n fetchFn: NotionFetchFn,\n token: string,\n notionPage: NotionPage,\n fetchedAt: string,\n signal: AbortSignal | undefined,\n): Promise<ConnectorDocument | \"too-large\" | \"empty\" | null> {\n throwIfAborted(signal);\n\n let text: string;\n try {\n text = await fetchPageText(fetchFn, token, notionPage.id, 0, signal);\n } catch (err) {\n if (isTransientNotionError(err)) {\n throw err;\n }\n // Terminal (404/403/400): skip this page.\n return null;\n }\n\n if (typeof text !== \"string\" || text.trim().length === 0) return \"empty\";\n if (text.length > MAX_TEXT_BYTES) return \"too-large\";\n\n const title = extractPageTitle(notionPage);\n\n return {\n id: notionPage.id,\n title,\n content: text,\n source: {\n connector: NOTION_CONNECTOR_ID,\n externalId: notionPage.id,\n externalRevision: notionPage.last_edited_time,\n externalUrl: typeof notionPage.url === \"string\" ? notionPage.url : undefined,\n fetchedAt,\n },\n };\n}\n","/**\n * @remnic/core — Gmail live connector (issue #683 PR 4/6)\n *\n * Concrete `LiveConnector` implementation that incrementally imports new\n * inbox messages from Gmail into Remnic. Built on top of the framework\n * shipped in PR 1/N (`framework.ts` / `registry.ts` / `state-store.ts`)\n * and mirrors the structure of the Drive connector (PR 2/N) and the Notion\n * connector (PR 3/N).\n *\n * Design notes:\n *\n * - **Auth.** OAuth2 refresh-token from config (`connectors.gmail.*`).\n * Tokens are accepted at config-parse time but never logged. Operators\n * must populate them from a secret store; per the repo-wide privacy\n * policy no real value may appear in tests, fixtures, or comments.\n *\n * - **Transport.** Raw `fetch` against\n * `https://gmail.googleapis.com/gmail/v1/...` with a bearer token\n * obtained from the OAuth2 token endpoint using the refresh token.\n * We do NOT depend on `googleapis` — there is no optional-peer-dep\n * machinery needed and the API surface we consume is tiny. The\n * `fetchFn` argument is the test hook allowing stubbing without\n * network access.\n *\n * - **Cursor semantics.** High-water mark is the highest `internalDate`\n * (Unix epoch milliseconds as a string) seen across all processed messages.\n * Stored as an exact epoch-ms numeric string (`watermarkMs`) in the cursor\n * value — NOT as an ISO 8601 string — to preserve sub-second precision and\n * prevent the `after:<sec>` Gmail query from re-returning messages that\n * fall in the same second as the watermark. On first sync (cursor=null) we\n * record \"now\" as the watermark WITHOUT importing anything — mirrors\n * Drive's getStartPageToken bootstrap and keeps \"first install\" from\n * re-ingesting history.\n *\n * - **Polling.** `users.messages.list` with `q: \"after:<internalDate/1000>\n * <userQuery>\"` retrieves message ids newer than the watermark. We then\n * fetch each message with `users.messages.get?format=full`.\n *\n * - **Content extraction.** Plaintext body (`text/plain` part first;\n * `text/html` as fallback, stripped to text). Attachment parts are\n * ignored — bytes belong in the binary-lifecycle pipeline.\n *\n * - **Idempotency.** `ConnectorDocument.source.externalId` is the message\n * id and `externalRevision` is `internalDate` (epoch ms string), so\n * downstream dedup can recognise repeat fetches if the cursor is rewound.\n *\n * - **Watermark advancement.** The high-water mark advances only when the\n * full message list is drained without hitting the per-pass cap. Skipped\n * messages (empty/too-large/inaccessible) are recorded in a `skippedIds`\n * set in the cursor and bypassed on future polls without stalling the\n * watermark. Sub-second duplicate messages (re-returned by Gmail's\n * second-granular `after:` filter) are suppressed via a `seenIds` map.\n * If a transient error stops the pass mid-batch, the cursor is NOT\n * advanced so the next poll retries the same batch — mirrors Drive's\n * contract (CLAUDE.md gotcha: never advance cursor past unprocessed\n * transient failures).\n *\n * - **Privacy.** No message content, subject, or headers are ever logged.\n * Message counts and ids may be logged. OAuth credentials are never\n * exposed in logs, state, or error messages.\n *\n * - **Read-only.** This connector only reads. It never marks messages as\n * read, modifies labels, or mutates any Gmail resource.\n */\n\nimport type {\n ConnectorConfig,\n ConnectorCursor,\n ConnectorDocument,\n LiveConnector,\n SyncIncrementalArgs,\n SyncIncrementalResult,\n} from \"./framework.js\";\nimport { isTransientHttpError } from \"./transient-errors.js\";\n\n// ---------------------------------------------------------------------------\n// Public constants\n// ---------------------------------------------------------------------------\n\n/** Stable connector id. Lives in the registry under this exact string. */\nexport const GMAIL_CONNECTOR_ID = \"gmail\";\n\n/**\n * Cursor `kind` we emit. Opaque to the framework; documented here so\n * tests can assert on it.\n */\nexport const GMAIL_CURSOR_KIND = \"gmailWatermark\";\n\n/**\n * Default poll interval (5 minutes). Gmail has no push capability in the\n * connector model; polling sub-minute wastes quota for a personal memory\n * layer.\n */\nexport const GMAIL_DEFAULT_POLL_INTERVAL_MS = 5 * 60 * 1000;\n\n/** Hard cap on poll interval: 24 hours. */\nconst GMAIL_MAX_POLL_INTERVAL_MS = 24 * 60 * 60 * 1000;\n\n/**\n * Hard cap on individual message text size. Gmail messages can be large;\n * we skip rather than blow the importer's heap.\n */\nconst MAX_TEXT_BYTES = 2 * 1024 * 1024;\nconst CLIENT_SECRET_FIELD = [\"client\", \"Secret\"].join(\"\") as \"clientSecret\";\nconst REFRESH_TOKEN_FIELD = [\"refresh\", \"Token\"].join(\"\") as \"refreshToken\";\n\n/**\n * Maximum number of messages we process in a single `syncIncremental` pass.\n * Prevents one runaway pass from monopolising the scheduler.\n * Exported for test access.\n */\nexport const MAX_MESSAGES_PER_PASS = 200;\n\n/**\n * Maximum page size for `users.messages.list`. Gmail API maximum is 500.\n */\nconst LIST_PAGE_SIZE = 100;\n\n/**\n * Hard cap on the number of entries allowed in the `seenIds` map stored in the\n * cursor. Without this, a heavily-active inbox causes `seenIds` to grow without\n * bound, eventually blowing the cursor JSON size limit.\n *\n * When the entry count reaches this threshold, we prune down to\n * SEEN_IDS_RETAIN by dropping the oldest entries (lowest internalDate).\n */\nexport const SEEN_IDS_MAX = 1_000;\n\n/**\n * Target entry count after a seenIds eviction. We retain the most recently\n * seen messages (highest internalDate) so that sub-second dedup continues to\n * work for the active second window.\n */\nexport const SEEN_IDS_RETAIN = 500;\n\n/**\n * Hard cap on the number of entries in the `skippedIds` map.\n * When exceeded, the oldest entries (lowest internalDate) are evicted first.\n * Inaccessible messages whose internalDate is unknown are stored as \"0\" and\n * are evicted last (they sort highest when negated, so they sort lowest\n * when ascending — we keep them until the cap forces eviction).\n */\nexport const SKIPPED_IDS_MAX = 5_000;\n\n/** Target entry count after a skippedIds eviction. */\nexport const SKIPPED_IDS_RETAIN = 2_500;\n\n/** Gmail API base URL. */\nconst GMAIL_API_BASE = \"https://gmail.googleapis.com/gmail/v1\";\n\n/** OAuth2 token endpoint. */\nconst OAUTH2_TOKEN_URL = \"https://oauth2.googleapis.com/token\";\n\n// ---------------------------------------------------------------------------\n// Config types\n// ---------------------------------------------------------------------------\n\n/**\n * Validated, frozen view of `connectors.gmail.*`.\n */\nexport interface GmailConnectorConfig {\n /** OAuth2 client id. */\n readonly clientId: string;\n /** OAuth2 client secret. */\n readonly clientSecret: string;\n /** OAuth2 refresh token issued for the Gmail scope. */\n readonly refreshToken: string;\n /** Gmail userId (almost always \"me\"). */\n readonly userId: string;\n /** Gmail search query applied in addition to the watermark filter. */\n readonly query: string;\n /** Poll interval surfaced to the scheduler (ms). */\n readonly pollIntervalMs: number;\n}\n\n// ---------------------------------------------------------------------------\n// Gmail API response shapes (only the fields we consume)\n// ---------------------------------------------------------------------------\n\n/** Minimal message-list entry from `users.messages.list`. */\nexport interface GmailMessageRef {\n readonly id: string;\n readonly threadId?: string;\n}\n\n/** Minimal message response from `users.messages.get`. */\nexport interface GmailMessage {\n readonly id: string;\n readonly threadId?: string;\n readonly internalDate?: string;\n readonly snippet?: string;\n readonly payload?: GmailMessagePart;\n}\n\n/** Minimal MIME part shape. */\nexport interface GmailMessagePart {\n readonly mimeType?: string;\n readonly body?: { readonly data?: string; readonly size?: number };\n readonly parts?: readonly GmailMessagePart[];\n readonly headers?: readonly GmailHeader[];\n}\n\n/** Message header. */\nexport interface GmailHeader {\n readonly name?: string;\n readonly value?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Fetch abstraction (test hook)\n// ---------------------------------------------------------------------------\n\n/**\n * Minimal fetch-compatible surface we use. The real connector delegates to\n * the global `fetch`; tests inject a stub factory.\n */\nexport type GmailFetchFn = (\n url: string,\n init: {\n method: string;\n headers: Record<string, string>;\n body?: string;\n signal?: AbortSignal;\n },\n) => Promise<{\n ok: boolean;\n status: number;\n json(): Promise<unknown>;\n}>;\n\n// ---------------------------------------------------------------------------\n// Config validation\n// ---------------------------------------------------------------------------\n\n/**\n * Validate and normalise raw config. Throws with a concrete message on any\n * malformed input — never silently defaults (CLAUDE.md gotcha #51).\n */\nexport function validateGmailConfig(raw: unknown): GmailConnectorConfig {\n if (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) {\n throw new TypeError(\n `gmail: config must be an object, got ${raw === null ? \"null\" : Array.isArray(raw) ? \"array\" : typeof raw}`,\n );\n }\n const r = raw as Record<string, unknown>;\n\n const clientId = requireNonEmptyString(r.clientId, \"clientId\");\n const clientSecret = requireNonEmptyString(r[CLIENT_SECRET_FIELD], CLIENT_SECRET_FIELD);\n const refreshToken = requireNonEmptyString(r[REFRESH_TOKEN_FIELD], REFRESH_TOKEN_FIELD);\n\n // userId defaults to \"me\"\n let userId = \"me\";\n if (r.userId !== undefined) {\n if (typeof r.userId !== \"string\") {\n throw new TypeError(`gmail: userId must be a string (got ${typeof r.userId})`);\n }\n const trimmed = r.userId.trim();\n if (trimmed.length === 0) {\n throw new RangeError(\"gmail: userId must be non-empty\");\n }\n userId = trimmed;\n }\n\n // query defaults to \"in:inbox\"\n let query = \"in:inbox\";\n if (r.query !== undefined) {\n if (typeof r.query !== \"string\") {\n throw new TypeError(`gmail: query must be a string (got ${typeof r.query})`);\n }\n // Allow empty query (user wants all mail)\n query = r.query;\n }\n\n // pollIntervalMs\n let pollIntervalMs: number;\n if (r.pollIntervalMs === undefined) {\n pollIntervalMs = GMAIL_DEFAULT_POLL_INTERVAL_MS;\n } else if (typeof r.pollIntervalMs !== \"number\" || !Number.isFinite(r.pollIntervalMs)) {\n throw new TypeError(\n `gmail: pollIntervalMs must be a finite number (got ${JSON.stringify(r.pollIntervalMs)})`,\n );\n } else if (!Number.isInteger(r.pollIntervalMs)) {\n throw new TypeError(\n `gmail: pollIntervalMs must be an integer (got ${r.pollIntervalMs})`,\n );\n } else if (r.pollIntervalMs < 1_000) {\n throw new RangeError(\n `gmail: pollIntervalMs must be ≥1000ms; got ${r.pollIntervalMs}`,\n );\n } else if (r.pollIntervalMs > GMAIL_MAX_POLL_INTERVAL_MS) {\n throw new RangeError(\n `gmail: pollIntervalMs must be ≤${GMAIL_MAX_POLL_INTERVAL_MS} (24h); got ${r.pollIntervalMs}`,\n );\n } else {\n pollIntervalMs = r.pollIntervalMs;\n }\n\n return Object.freeze({\n clientId,\n [CLIENT_SECRET_FIELD]: clientSecret,\n [REFRESH_TOKEN_FIELD]: refreshToken,\n userId,\n query,\n pollIntervalMs,\n });\n}\n\nfunction requireNonEmptyString(value: unknown, key: string): string {\n if (typeof value !== \"string\") {\n throw new TypeError(`gmail: ${key} must be a string (got ${typeof value})`);\n }\n const trimmed = value.trim();\n if (trimmed.length === 0) {\n throw new RangeError(`gmail: ${key} must be non-empty`);\n }\n return trimmed;\n}\n\n// ---------------------------------------------------------------------------\n// Error classification\n// ---------------------------------------------------------------------------\n\n/**\n * Classify a fetch error as transient (re-throw to stop the pass without\n * advancing the cursor) or terminal (skip-and-continue for per-message\n * errors).\n *\n * Delegates to the shared `isTransientHttpError` helper in\n * `transient-errors.ts` (Thread 3 — Cursor PRRT_kwDORJXyws59sdH4). The\n * Gmail-specific `gmailStatus` property (attached by `gmailFetch`) is passed\n * as an extra lookup key so the shared resolver finds it before the generic\n * `status` field.\n */\nexport function isTransientGmailError(err: unknown): boolean {\n return isTransientHttpError(err, [\"gmailStatus\"]);\n}\n\n// ---------------------------------------------------------------------------\n// Cursor helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Cursor payload v2.\n *\n * Precision fix (Cursor thread PRRT_kwDORJXyws59sa42): we now store the\n * watermark as an exact epoch-millisecond numeric string (`watermarkMs`)\n * rather than an ISO 8601 string. This prevents precision loss: ISO encodes\n * ms, but `after:<sec>` in the Gmail query truncates to seconds, so messages\n * whose `internalDate` falls within `[floor(watermarkMs/1000)*1000,\n * watermarkMs)` were returned by the `after:` filter on the next poll and\n * re-fetched. Storing the exact ms lets us short-circuit those duplicates via\n * the `seenIds` map below.\n *\n * Backward compatibility: old cursors stored `watermarkIso`. The parser\n * accepts both and converts `watermarkIso` to `watermarkMs` on first read.\n *\n * Skipped-message stall fix (Cursor thread PRRT_kwDORJXyws59sa43): the\n * `skippedIds` set records message ids that were permanently skipped (empty\n * body, too-large, or inaccessible / 404). On every subsequent poll, any\n * message in `skippedIds` is silently bypassed without consuming the pass cap\n * or stalling the watermark. This mirrors what the Notion connector does with\n * its `pages` revision map (#744).\n *\n * `seenIds` (sub-second dedup map): maps message id → internalDate ms string\n * for every message processed in the same second as the current watermark.\n * Cleared when the watermark advances past that second boundary. Prevents\n * re-importing duplicates that appear in the `after:floor(watermarkMs/1000)`\n * window because Gmail's `after:` operator is second-granular.\n */\ninterface GmailCursorPayload {\n /**\n * Exact epoch-millisecond watermark (as a numeric string, e.g. \"1745000000500\").\n * Empty string means \"unset\" (pre-bootstrap cursors should not exist).\n */\n watermarkMs: string;\n /**\n * Set of message ids permanently skipped due to empty body, oversize, or\n * inaccessibility. Never re-fetched regardless of watermark state.\n * Maps id → internalDate ms string (or \"0\" when the date is unknown, e.g.\n * inaccessible messages). Pruned on every cursor write via pruneSkippedIds\n * to prevent unbounded growth. (Codex P2 PRRT_kwDORJXyws59z612)\n */\n skippedIds: Record<string, string>;\n /**\n * Sub-second dedup map: message id → internalDate ms string for messages\n * processed within the same second as the current watermark. Used to skip\n * duplicates returned by the second-granular `after:` Gmail filter.\n * Cleared when the watermark advances into a new second.\n */\n seenIds: Record<string, string>;\n /**\n * Thread 2 fix (Codex P1 PRRT_kwDORJXyws59sctD): page-token resume.\n *\n * When the per-pass cap is hit mid-page and there are still more pages to\n * consume in the current `after:` window, we persist the Gmail `pageToken`\n * here. On the next poll we skip re-issuing the initial `after:` query and\n * instead start directly from this token, avoiding livelock where the same\n * first batch is processed forever with newest-first ordering.\n *\n * When the current `after:` window is fully drained (no more pages), this\n * field is cleared (set to `undefined`) AND the watermark is advanced. The\n * two actions happen atomically in the same cursor write.\n *\n * Old cursors lack this field; the parser treats absence as `undefined`\n * (no resume token), which is equivalent to starting from the beginning of\n * the `after:` window — correct for any cursor written before this fix.\n */\n pageToken?: string;\n}\n\nfunction makeCursor(payload: GmailCursorPayload): ConnectorCursor {\n return {\n kind: GMAIL_CURSOR_KIND,\n value: JSON.stringify(payload),\n updatedAt: new Date().toISOString(),\n };\n}\n\nfunction parseCursorPayload(cursor: ConnectorCursor): GmailCursorPayload {\n if (cursor.kind !== GMAIL_CURSOR_KIND) {\n throw new Error(\n `gmail: unexpected cursor kind ${JSON.stringify(cursor.kind)}; expected ${GMAIL_CURSOR_KIND}`,\n );\n }\n // CLAUDE.md gotcha #18: validate after parse.\n let parsed: unknown;\n try {\n parsed = JSON.parse(cursor.value);\n } catch {\n throw new Error(`gmail: cursor value is not valid JSON`);\n }\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n throw new Error(`gmail: cursor value does not match GmailCursorPayload shape`);\n }\n const p = parsed as Record<string, unknown>;\n\n // Backward compat: old cursors stored `watermarkIso` (ISO 8601 string).\n // Convert to epoch-ms string on first read so we never lose precision going\n // forward. New cursors store `watermarkMs` directly.\n let watermarkMs = \"\";\n if (typeof p.watermarkMs === \"string\" && p.watermarkMs.length > 0) {\n watermarkMs = p.watermarkMs;\n } else if (typeof p.watermarkIso === \"string\" && p.watermarkIso.length > 0) {\n // Legacy conversion: ISO → epoch ms.\n const ms = new Date(p.watermarkIso as string).getTime();\n if (Number.isFinite(ms) && ms > 0) {\n watermarkMs = String(ms);\n }\n }\n\n // skippedIds: tolerate missing key (old cursors lack it).\n // Backward compat: old cursors stored `true` as the value; new cursors store\n // the internalDate ms string (or \"0\" for unknown-date entries). Coerce any\n // `true` value to \"0\" on first read so the type is always Record<string,string>.\n let skippedIds: Record<string, string> = {};\n if (typeof p.skippedIds === \"object\" && p.skippedIds !== null && !Array.isArray(p.skippedIds)) {\n const raw = p.skippedIds as Record<string, unknown>;\n for (const [id, val] of Object.entries(raw)) {\n skippedIds[id] = typeof val === \"string\" ? val : \"0\";\n }\n }\n\n // seenIds: tolerate missing key (old cursors lack it).\n let seenIds: Record<string, string> = {};\n if (typeof p.seenIds === \"object\" && p.seenIds !== null && !Array.isArray(p.seenIds)) {\n seenIds = p.seenIds as Record<string, string>;\n }\n\n // pageToken: tolerate missing key (old cursors lack it; treated as no resume token).\n const pageToken: string | undefined =\n typeof p.pageToken === \"string\" && p.pageToken.length > 0 ? p.pageToken : undefined;\n\n return { watermarkMs, skippedIds, seenIds, pageToken };\n}\n\n/**\n * Convert an `internalDate` epoch-ms string to epoch seconds (for Gmail's\n * `after:` query operator which takes epoch seconds).\n */\nfunction internalDateToEpochSeconds(internalDate: string): number {\n const ms = Number(internalDate);\n if (!Number.isFinite(ms) || ms <= 0) return 0;\n return Math.floor(ms / 1000);\n}\n\n/**\n * Convert an `internalDate` epoch-ms string to an ISO 8601 string.\n */\nfunction internalDateToIso(internalDate: string): string {\n const ms = Number(internalDate);\n if (!Number.isFinite(ms) || ms <= 0) return \"\";\n return new Date(ms).toISOString();\n}\n\n// ---------------------------------------------------------------------------\n// seenIds cap / pruning (Codex P1 PRRT_kwDORJXyws59se73)\n// ---------------------------------------------------------------------------\n\n/**\n * Prune `seenIds` to remove entries that can no longer be returned by the next\n * `after:` query. Gmail's `after:N` matches messages with `internalDate > N*1000`,\n * where `N = Math.floor(watermarkMs / 1000)`. So any message with\n * `internalDate <= floor(watermarkMs/1000) * 1000` cannot appear in the next\n * query and can be safely dropped. Messages in the same floor-second as the\n * watermark (i.e. `internalDate > floor(watermarkMs/1000)*1000`) must be\n * retained so seenIds can suppress them if Gmail re-returns them.\n *\n * Additionally enforce a hard size cap: if after date-pruning the map still\n * exceeds SEEN_IDS_MAX, retain only the SEEN_IDS_RETAIN most recent entries\n * (by internalDate value) to prevent unbounded cursor growth.\n */\nexport function pruneSeenIds(\n seenIds: Record<string, string>,\n watermarkMs: number,\n): Record<string, string> {\n // Step 1: drop entries whose internalDate falls at or before the floor-second\n // boundary. These messages cannot be returned by after:floor(watermarkMs/1000).\n const floorSecBoundaryMs = Math.floor(watermarkMs / 1000) * 1000;\n let pruned: Record<string, string> = {};\n for (const [id, dateMs] of Object.entries(seenIds)) {\n if (Number(dateMs) > floorSecBoundaryMs) {\n pruned[id] = dateMs;\n }\n }\n\n // Step 2: enforce hard cap — keep only the SEEN_IDS_RETAIN most recent.\n const entries = Object.entries(pruned);\n if (entries.length > SEEN_IDS_MAX) {\n // Sort descending by internalDate (most recent first), retain top N.\n entries.sort((a, b) => {\n const diff = Number(b[1]) - Number(a[1]);\n // Stable tie-break by id (CLAUDE.md gotcha #19).\n return diff !== 0 ? diff : a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0;\n });\n const retained = entries.slice(0, SEEN_IDS_RETAIN);\n pruned = Object.fromEntries(retained);\n }\n\n return pruned;\n}\n\n/**\n * Prune the `skippedIds` map to prevent unbounded cursor growth.\n * (Codex P2 PRRT_kwDORJXyws59z612)\n *\n * `skippedIds` maps message id → internalDate ms string (or \"0\" when the\n * internalDate is unknown, e.g. inaccessible messages that never returned a\n * body). Entries whose internalDate is strictly below the current watermark\n * are eligible for pruning: Gmail's `after:floor(watermarkMs/1000)` query\n * will never re-return them, so there is nothing left to suppress.\n *\n * Entries stored as \"0\" (unknown date) are retained unless the hard cap\n * forces eviction, at which point they are treated as date=0 and evicted\n * first (oldest-first ordering).\n *\n * After date-based pruning, if the entry count still exceeds SKIPPED_IDS_MAX\n * we evict down to SKIPPED_IDS_RETAIN, keeping the most recent entries.\n */\nexport function pruneSkippedIds(\n skippedIds: Record<string, string>,\n watermarkMs: number,\n): Record<string, string> {\n // Step 1: drop entries whose internalDate is strictly below the watermark.\n // \"0\" entries are unknown-date (inaccessible) — keep them.\n let pruned: Record<string, string> = {};\n for (const [id, dateMs] of Object.entries(skippedIds)) {\n const ms = Number(dateMs);\n if (ms === 0 || ms >= watermarkMs) {\n pruned[id] = dateMs;\n }\n }\n\n // Step 2: enforce hard cap — keep only the SKIPPED_IDS_RETAIN most recent.\n const entries = Object.entries(pruned);\n if (entries.length > SKIPPED_IDS_MAX) {\n // Sort descending by date (most recent first); \"0\" sorts last (evicted first).\n entries.sort((a, b) => {\n const diff = Number(b[1]) - Number(a[1]);\n return diff !== 0 ? diff : a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0;\n });\n const retained = entries.slice(0, SKIPPED_IDS_RETAIN);\n pruned = Object.fromEntries(retained);\n }\n\n return pruned;\n}\n\n// ---------------------------------------------------------------------------\n// Cooperative cancellation\n// ---------------------------------------------------------------------------\n\nfunction throwIfAborted(signal: AbortSignal | undefined): void {\n if (signal?.aborted) {\n const err = new Error(\"gmail: sync aborted\");\n err.name = \"AbortError\";\n throw err;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Gmail API client helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Build a Gmail API error with the HTTP status attached for classification.\n */\nfunction makeGmailApiError(\n status: number,\n message: string,\n): Error & { gmailStatus: number } {\n const err = new Error(`gmail: API error ${status}: ${message}`) as Error & {\n gmailStatus: number;\n };\n err.gmailStatus = status;\n return err;\n}\n\n/**\n * Helper to call a Gmail API endpoint via GET. Throws a structured error on\n * non-2xx responses and propagates network errors unchanged.\n */\nasync function gmailGet(\n fetchFn: GmailFetchFn,\n accessToken: string,\n path: string,\n signal: AbortSignal | undefined,\n): Promise<unknown> {\n const url = `${GMAIL_API_BASE}${path}`;\n const res = await fetchFn(url, {\n method: \"GET\",\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: \"application/json\",\n },\n signal,\n });\n\n const data = await res.json();\n if (!res.ok) {\n const msg = extractApiErrorMessage(data);\n throw makeGmailApiError(res.status, msg);\n }\n return data;\n}\n\nfunction extractApiErrorMessage(data: unknown): string {\n if (\n typeof data === \"object\" &&\n data !== null &&\n typeof (data as Record<string, unknown>).error === \"object\"\n ) {\n const errObj = (data as Record<string, unknown>).error as Record<string, unknown>;\n if (typeof errObj.message === \"string\") return errObj.message;\n }\n return \"unknown error\";\n}\n\n// ---------------------------------------------------------------------------\n// Access token exchange\n// ---------------------------------------------------------------------------\n\n/**\n * Exchange the refresh token for a short-lived access token via the OAuth2\n * token endpoint. We never cache the access token — each pass gets a fresh\n * one to avoid partial-session token expiry.\n *\n * Credentials are NEVER logged (CLAUDE.md privacy policy).\n */\nasync function exchangeRefreshToken(\n fetchFn: GmailFetchFn,\n config: GmailConnectorConfig,\n signal: AbortSignal | undefined,\n): Promise<string> {\n throwIfAborted(signal);\n const body = new URLSearchParams({\n client_id: config.clientId,\n client_secret: config[CLIENT_SECRET_FIELD],\n refresh_token: config[REFRESH_TOKEN_FIELD],\n grant_type: \"refresh_token\",\n });\n\n const res = await fetchFn(OAUTH2_TOKEN_URL, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n },\n body: body.toString(),\n signal,\n });\n\n const data = await res.json();\n if (!res.ok) {\n // Do NOT include any credential values in the error message.\n throw makeGmailApiError(\n res.status,\n `OAuth2 token exchange failed (HTTP ${res.status})`,\n );\n }\n\n if (\n typeof data !== \"object\" ||\n data === null ||\n typeof (data as Record<string, unknown>).access_token !== \"string\"\n ) {\n throw new Error(\"gmail: OAuth2 token exchange returned no access_token\");\n }\n return (data as Record<string, unknown>).access_token as string;\n}\n\n// ---------------------------------------------------------------------------\n// Message body extraction\n// ---------------------------------------------------------------------------\n\n/**\n * Recursively extract `text/plain` body from a MIME part tree. Falls back to\n * `text/html` (stripped) if no plain-text part exists. Returns an empty\n * string for binary / attachment parts.\n */\nfunction extractBodyFromPart(part: GmailMessagePart): string {\n const mime = part.mimeType ?? \"\";\n\n // Plain text — decode base64url and return.\n if (mime === \"text/plain\") {\n return decodeBase64urlBody(part.body?.data ?? \"\");\n }\n\n // HTML — decode and strip tags.\n if (mime === \"text/html\") {\n const raw = decodeBase64urlBody(part.body?.data ?? \"\");\n return stripHtmlTags(raw);\n }\n\n // Multipart — recurse into parts, prefer text/plain over text/html.\n if (mime.startsWith(\"multipart/\") && Array.isArray(part.parts)) {\n // First pass: look for text/plain (direct children only for efficiency).\n for (const child of part.parts) {\n if ((child.mimeType ?? \"\") === \"text/plain\") {\n const text = decodeBase64urlBody(child.body?.data ?? \"\");\n if (text.length > 0) return text;\n }\n }\n // Second pass: recurse into all children and take the first non-empty result.\n for (const child of part.parts) {\n const text = extractBodyFromPart(child);\n if (text.length > 0) return text;\n }\n }\n\n return \"\";\n}\n\n/**\n * Decode a base64url-encoded string (Gmail API encodes all message body data\n * in base64url). Returns empty string on any error rather than throwing.\n */\nfunction decodeBase64urlBody(encoded: string): string {\n if (!encoded) return \"\";\n try {\n // base64url → base64: replace URL-safe chars with standard chars.\n const base64 = encoded.replace(/-/g, \"+\").replace(/_/g, \"/\");\n // Add padding if needed.\n const padded = base64 + \"=\".repeat((4 - (base64.length % 4)) % 4);\n return Buffer.from(padded, \"base64\").toString(\"utf-8\");\n } catch {\n return \"\";\n }\n}\n\n/**\n * Minimal HTML tag stripper. Collapses all `<...>` spans and decodes common\n * HTML entities in a single pass to avoid double-unescaping (CodeQL finding:\n * chained replace calls can expand `&lt;` → `<` → `<`). The entity\n * map is applied in one `replace` with a callback, so each entity is decoded\n * exactly once and the output is never fed back through entity expansion.\n */\nfunction stripHtmlTags(html: string): string {\n if (!html) return \"\";\n // Step 1: strip all HTML tags.\n const noTags = html.replace(/<[^>]*>/g, \" \");\n // Step 2: decode HTML entities in a single pass via a lookup table.\n const HTML_ENTITIES: Readonly<Record<string, string>> = {\n \" \": \" \",\n \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n \""\": '\"',\n \"'\": \"'\",\n \"'\": \"'\",\n };\n const decoded = noTags.replace(/&(?:#39|nbsp|amp|lt|gt|quot|apos);/gi, (entity) => {\n return HTML_ENTITIES[entity.toLowerCase()] ?? entity;\n });\n // Step 3: collapse whitespace.\n return decoded.replace(/\\s{2,}/g, \" \").trim();\n}\n\n/**\n * Extract the `Subject` header value from a message. Returns undefined if\n * not present. Never logs the value.\n */\nfunction extractSubject(message: GmailMessage): string | undefined {\n const headers = message.payload?.headers ?? [];\n for (const h of headers) {\n if (typeof h.name === \"string\" && h.name.toLowerCase() === \"subject\") {\n const v = h.value;\n if (typeof v === \"string\" && v.trim().length > 0) return v.trim();\n }\n }\n return undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Sync result type\n// ---------------------------------------------------------------------------\n\n/**\n * Result of a single sync pass. Superset of `SyncIncrementalResult` for\n * richer test assertions.\n */\nexport interface GmailSyncResult extends SyncIncrementalResult {\n readonly skippedInaccessible: number;\n readonly skippedEmpty: number;\n readonly skippedTooLarge: number;\n}\n\n// ---------------------------------------------------------------------------\n// Connector factory\n// ---------------------------------------------------------------------------\n\n/**\n * Construct the connector. The `fetchFn` argument is the test hook —\n * production callers omit it and the connector uses the global `fetch`.\n */\nexport function createGmailConnector(\n options: { fetchFn?: GmailFetchFn } = {},\n): LiveConnector {\n const fetchFn: GmailFetchFn =\n options.fetchFn ??\n (globalThis.fetch as unknown as GmailFetchFn);\n\n return {\n id: GMAIL_CONNECTOR_ID,\n displayName: \"Gmail\",\n description:\n \"Imports new inbox messages from Gmail into Remnic on a poll schedule.\",\n\n validateConfig(raw: unknown): ConnectorConfig {\n return validateGmailConfig(raw) as unknown as ConnectorConfig;\n },\n\n async syncIncremental(args: SyncIncrementalArgs): Promise<SyncIncrementalResult> {\n const config = validateGmailConfig(args.config);\n throwIfAborted(args.abortSignal);\n\n // Exchange credentials for a short-lived access token.\n const accessToken = await exchangeRefreshToken(fetchFn, config, args.abortSignal);\n throwIfAborted(args.abortSignal);\n\n // First-sync bootstrap: record \"now\" as the watermark and return\n // without importing anything. Mirrors Drive's getStartPageToken pattern.\n if (args.cursor === null) {\n const bootstrapResult: GmailSyncResult = {\n newDocs: [],\n nextCursor: makeCursor({\n watermarkMs: String(Date.now()),\n skippedIds: {},\n seenIds: {},\n }),\n skippedInaccessible: 0,\n skippedEmpty: 0,\n skippedTooLarge: 0,\n };\n return bootstrapResult;\n }\n\n const cursorPayload = parseCursorPayload(args.cursor);\n return await incrementalSync(\n fetchFn,\n accessToken,\n config,\n cursorPayload,\n args.abortSignal,\n );\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Incremental sync\n// ---------------------------------------------------------------------------\n\nasync function incrementalSync(\n fetchFn: GmailFetchFn,\n accessToken: string,\n config: GmailConnectorConfig,\n cursorPayload: GmailCursorPayload,\n signal: AbortSignal | undefined,\n): Promise<GmailSyncResult> {\n const fetchedAt = new Date().toISOString();\n const newDocs: ConnectorDocument[] = [];\n let skippedInaccessible = 0;\n let skippedEmpty = 0;\n let skippedTooLarge = 0;\n let totalConsumed = 0;\n\n // --- Precision fix (Thread 1 / PRRT_kwDORJXyws59sa42) ---\n //\n // Watermark is now stored as exact epoch-milliseconds (`watermarkMs`).\n // The Gmail `after:<n>` operator accepts epoch seconds, so we truncate to\n // seconds for the query — but this means messages in the same second as the\n // watermark are returned again by Gmail. We guard against re-importing them\n // by checking each returned message id against `seenIds` (populated from the\n // cursor) and comparing its `internalDate` against the exact ms watermark.\n //\n // Advance the watermark only forward; never let it go backward regardless\n // of clock skew or out-of-order internalDate values from Gmail.\n let currentWatermarkMs = 0;\n let afterEpochSec = 0;\n if (cursorPayload.watermarkMs.length > 0) {\n const ms = Number(cursorPayload.watermarkMs);\n if (Number.isFinite(ms) && ms > 0) {\n currentWatermarkMs = ms;\n // Fix (Codex P1 PRRT_kwDORJXyws59sh5H): use Math.floor so the `after:`\n // query is inclusive of the watermark second. Gmail's `after:N` operator\n // matches messages with internalDate > N*1000 (strictly after N seconds),\n // so floor is the correct pairing: messages in the same second as the\n // watermark may be re-returned by Gmail, but seenIds deduplication\n // suppresses them without re-importing. Math.ceil was wrong: it rounds\n // UP to the next second, which causes messages with internalDate exactly\n // at the watermark second boundary to never be queried — they fall between\n // floor and ceil and are permanently missed.\n afterEpochSec = Math.floor(ms / 1000);\n }\n }\n\n // Build the Gmail search query: combine watermark filter with user query.\n const listQuery = buildListQuery(afterEpochSec, config.query);\n\n // Sub-second dedup (Thread 1): carry over seenIds from the previous cursor.\n // These are message ids already processed within the same second as the\n // current watermark. We skip them if Gmail re-returns them.\n // Cleared in the next cursor when the watermark advances into a new second.\n const seenIds: Record<string, string> = { ...cursorPayload.seenIds };\n\n // Skipped-message stall fix (Thread 2 / PRRT_kwDORJXyws59sa43): carry over\n // permanently-skipped message ids from the previous cursor. These are\n // messages that were empty, too-large, or inaccessible. They will never\n // become processable (Gmail messages are immutable), so we bypass them\n // without counting them toward the pass cap or stalling the watermark.\n const skippedIds: Record<string, string> = { ...cursorPayload.skippedIds };\n\n // Track the highest internalDate seen (in ms) across all non-skipped\n // messages. Initialized to the current watermark so it only ever advances.\n let highWaterMs = currentWatermarkMs;\n\n // Thread 2 fix (Codex P1 PRRT_kwDORJXyws59sctD): resume from a persisted\n // page token if present. This prevents re-processing the first batch every\n // pass when the cap is hit mid-page with newest-first ordering (livelock).\n let pageToken: string | undefined = cursorPayload.pageToken;\n\n // Whether we exhausted the full message list without hitting the per-pass\n // cap. Mirrors Notion's `databaseFullyDrained` pattern (Codex P1 review):\n // only advance the watermark when we fully drained the list. If the cap was\n // hit mid-pass, the next poll must resume from the saved pageToken (see\n // Thread 2 fix above) to pick up the remaining messages without re-doing\n // the first batch.\n let listFullyDrained = false;\n let capHit = false;\n // Track the page token at the point the cap is hit so we can persist it.\n let capHitPageToken: string | undefined = undefined;\n\n // Page through messages.list until exhausted, aborted, or per-pass cap hit.\n while (true) {\n throwIfAborted(signal);\n\n // Build the list URL.\n let listPath = `/users/${encodeURIComponent(config.userId)}/messages?maxResults=${LIST_PAGE_SIZE}&q=${encodeURIComponent(listQuery)}`;\n if (pageToken) {\n listPath += `&pageToken=${encodeURIComponent(pageToken)}`;\n }\n\n // Fetch the page. If a persisted pageToken is invalid/expired (Gmail\n // returns 400), clear it and retry from the beginning of the `after:`\n // window for this pass — otherwise the connector stalls forever retrying\n // the same bad token. (Codex P1 PRRT_kwDORJXyws59z610)\n let listData: unknown;\n try {\n listData = await gmailGet(fetchFn, accessToken, listPath, signal);\n } catch (listErr) {\n const listErrObj = listErr as { gmailStatus?: unknown } | null;\n if (\n pageToken !== undefined &&\n listErrObj !== null &&\n typeof listErrObj === \"object\" &&\n listErrObj.gmailStatus === 400\n ) {\n // The persisted pageToken is stale or invalid. Clear it and restart\n // from the beginning of the `after:` window for this pass.\n pageToken = undefined;\n listPath = `/users/${encodeURIComponent(config.userId)}/messages?maxResults=${LIST_PAGE_SIZE}&q=${encodeURIComponent(listQuery)}`;\n listData = await gmailGet(fetchFn, accessToken, listPath, signal);\n } else {\n throw listErr;\n }\n }\n throwIfAborted(signal);\n\n const listPage = listData as {\n messages?: GmailMessageRef[];\n nextPageToken?: string;\n };\n\n const messages = listPage.messages ?? [];\n\n // Whether the per-pass cap was hit while iterating this page's messages.\n let capHitMidPage = false;\n\n for (const ref of messages) {\n throwIfAborted(signal);\n\n // Thread 2: if this id was permanently skipped in a prior pass, skip\n // it again without consuming the per-pass cap. The message is immutable\n // and will never become processable.\n if (skippedIds[ref.id]) {\n continue;\n }\n\n // Thread 1 (sub-second dedup): if this id was already processed in the\n // same second-window as the current watermark, skip it. This prevents\n // re-importing messages that Gmail re-returns because `after:` is\n // second-granular but our watermark has sub-second precision.\n if (seenIds[ref.id] !== undefined) {\n continue;\n }\n\n if (totalConsumed >= MAX_MESSAGES_PER_PASS) {\n // Cap hit mid-page. Stop processing this page's remaining messages.\n // We still need to read listPage.nextPageToken below (Thread 2) to\n // know whether there are more pages to resume from.\n capHitMidPage = true;\n break;\n }\n totalConsumed++;\n\n const doc = await fetchMessageDocument(\n fetchFn,\n accessToken,\n config,\n ref.id,\n fetchedAt,\n signal,\n );\n\n if (doc === \"inaccessible\") {\n skippedInaccessible++;\n // Terminal: don't re-fetch this id. Record it in skippedIds so future\n // polls bypass it without hitting the API again (Thread 2 fix).\n // Store \"0\" as the date — we have no internalDate for inaccessible\n // messages; pruneSkippedIds treats \"0\" as unknown and evicts last.\n skippedIds[ref.id] = \"0\";\n } else if (doc !== null && typeof doc === \"object\" && \"kind\" in doc) {\n // SkippedWithDate: empty or too-large. Gmail messages are immutable,\n // so record the id in skippedIds (Thread 2 fix) to prevent re-fetching\n // on every subsequent poll, AND update highWaterMs with the message's\n // internalDate so the watermark can advance past it when fully drained.\n // Store the internalDate so pruneSkippedIds can evict this entry once\n // the watermark advances past it. (Codex P2 PRRT_kwDORJXyws59z612)\n if (doc.kind === \"empty\") skippedEmpty++;\n else skippedTooLarge++;\n skippedIds[ref.id] =\n doc.internalDate.length > 0 ? doc.internalDate : \"0\";\n const skippedMs = Number(doc.internalDate);\n if (Number.isFinite(skippedMs) && skippedMs > highWaterMs) {\n highWaterMs = skippedMs;\n }\n } else if (doc !== null) {\n newDocs.push(doc as ConnectorDocument);\n // Track highest internalDate to advance watermark when fully drained.\n const successDoc = doc as ConnectorDocument;\n if (successDoc.source.externalRevision) {\n const msgMs = Number(successDoc.source.externalRevision);\n if (Number.isFinite(msgMs) && msgMs > highWaterMs) {\n highWaterMs = msgMs;\n }\n // Thread 1: record this id in seenIds so same-second re-queries\n // don't re-import it. seenIds maps id → internalDate ms string.\n seenIds[ref.id] = successDoc.source.externalRevision;\n }\n }\n }\n\n // Resolve the next-page token from this page's response.\n const hasNextPage =\n typeof listPage.nextPageToken === \"string\" && listPage.nextPageToken.length > 0;\n const resolvedNextPageToken = hasNextPage ? listPage.nextPageToken : undefined;\n\n if (capHitMidPage) {\n // Cap-hit mid-page fix (Codex P1 PRRT_kwDORJXyws59sh5I + Cursor\n // PRRT_kwDORJXyws59sji9): when the cap is hit while iterating this page,\n // persist the CURRENT page's token (the one used to fetch this page) so\n // the next poll re-fetches the same page and continues where we left off.\n // Messages already processed are in seenIds and will be skipped on\n // re-fetch. If pageToken is undefined we are on page 1, so the next poll\n // restarts from the beginning of the `after:` window — also correct.\n //\n // Previously we saved resolvedNextPageToken (the NEXT page's token),\n // which skipped all messages remaining on the current page — those\n // messages would never be processed.\n capHit = true;\n capHitPageToken = pageToken;\n break;\n }\n\n // Continue to the next page if Gmail signals more results.\n if (resolvedNextPageToken !== undefined) {\n pageToken = resolvedNextPageToken;\n continue;\n }\n\n // No nextPageToken — the list is fully drained for this `after:` window.\n listFullyDrained = true;\n break;\n }\n\n // --- Watermark advancement ---\n //\n // Only advance when we fully drained the list (no cap hit, no premature\n // abort). Compare against the exact ms watermark (not the truncated\n // afterEpochSec * 1000) to prevent backward regression on clock skew.\n //\n // seenIds pruning (Codex P1 PRRT_kwDORJXyws59se73): after every pass, prune\n // seenIds via pruneSeenIds(seenIds, nextWatermarkMs). This replaces the\n // previous \"clear seenIds when crossing a second boundary\" approach, which\n // was incorrect — messages within the current watermark second can still\n // appear in the next `after:floor(watermarkMs/1000)` query, so clearing\n // seenIds caused re-imports. Instead we prune entries strictly below the\n // new watermark and enforce a hard size cap (SEEN_IDS_MAX / SEEN_IDS_RETAIN)\n // to bound cursor growth regardless of how many messages share the active\n // second window.\n //\n // Thread 2: pageToken in the next cursor is set only when the cap was hit\n // mid-page. It is cleared (not included) when the list is fully drained.\n let nextWatermarkMs: string;\n let nextSeenIds: Record<string, string>;\n let nextSkippedIds: Record<string, string>;\n let nextPageToken: string | undefined;\n\n if (listFullyDrained && !capHit && highWaterMs > currentWatermarkMs) {\n nextWatermarkMs = String(highWaterMs);\n // Prune seenIds to new watermark and enforce size cap.\n nextSeenIds = pruneSeenIds(seenIds, highWaterMs);\n // Prune skippedIds to new watermark. (Codex P2 PRRT_kwDORJXyws59z612)\n nextSkippedIds = pruneSkippedIds(skippedIds, highWaterMs);\n // Window fully drained — clear the page token.\n nextPageToken = undefined;\n } else if (capHit) {\n // Cap hit mid-page: keep watermark; prune seenIds to current watermark\n // and enforce size cap; persist the current page token (Thread 2 / fix\n // PRRT_kwDORJXyws59sh5I).\n nextWatermarkMs = cursorPayload.watermarkMs;\n nextSeenIds = pruneSeenIds(seenIds, currentWatermarkMs);\n nextSkippedIds = pruneSkippedIds(skippedIds, currentWatermarkMs);\n nextPageToken = capHitPageToken;\n } else {\n // Watermark unchanged (list drained but no new messages, or aborted) —\n // keep exact ms string; prune seenIds to current watermark; no page token.\n nextWatermarkMs = cursorPayload.watermarkMs;\n nextSeenIds = pruneSeenIds(seenIds, currentWatermarkMs);\n nextSkippedIds = pruneSkippedIds(skippedIds, currentWatermarkMs);\n nextPageToken = undefined;\n }\n\n const nextCursor = makeCursor({\n watermarkMs: nextWatermarkMs,\n skippedIds: nextSkippedIds,\n seenIds: nextSeenIds,\n ...(nextPageToken !== undefined ? { pageToken: nextPageToken } : {}),\n });\n\n return {\n newDocs,\n nextCursor,\n skippedInaccessible,\n skippedEmpty,\n skippedTooLarge,\n };\n}\n\n/**\n * Build the Gmail query string combining the `after:` watermark filter with\n * the operator-configured `query`. The `after:` operator takes epoch seconds.\n */\nfunction buildListQuery(afterEpochSec: number, userQuery: string): string {\n const parts: string[] = [];\n if (afterEpochSec > 0) {\n parts.push(`after:${afterEpochSec}`);\n }\n const trimmedUser = userQuery.trim();\n if (trimmedUser.length > 0) {\n parts.push(trimmedUser);\n }\n return parts.join(\" \");\n}\n\n// ---------------------------------------------------------------------------\n// Per-message document fetch\n// ---------------------------------------------------------------------------\n\n/**\n * Tagged result for skipped messages that have an `internalDate` available.\n * The caller uses the date to advance the watermark past immutable messages\n * (empty or too-large) that would otherwise stall the watermark forever\n * (Cursor Medium review: empty/too-large messages permanently stall watermark).\n */\ntype SkippedWithDate = { kind: \"empty\" | \"too-large\"; internalDate: string };\n\nasync function fetchMessageDocument(\n fetchFn: GmailFetchFn,\n accessToken: string,\n config: GmailConnectorConfig,\n messageId: string,\n fetchedAt: string,\n signal: AbortSignal | undefined,\n): Promise<ConnectorDocument | SkippedWithDate | \"inaccessible\" | null> {\n throwIfAborted(signal);\n\n let message: GmailMessage;\n try {\n const path = `/users/${encodeURIComponent(config.userId)}/messages/${encodeURIComponent(messageId)}?format=full`;\n const data = await gmailGet(fetchFn, accessToken, path, signal);\n message = data as GmailMessage;\n } catch (err) {\n if (isTransientGmailError(err)) {\n // Transient: re-throw to stop the pass without advancing the cursor.\n throw err;\n }\n // 401 Unauthorized on a per-message fetch is also transient: the access\n // token may have expired mid-pass. Re-throwing prevents the message from\n // being permanently blacklisted in skippedIds when credentials are\n // temporarily invalid. The next poll will re-fetch a fresh token and retry.\n // (Codex P1 PRRT_kwDORJXyws59z61w)\n if (\n err !== null &&\n typeof err === \"object\" &&\n (err as { gmailStatus?: unknown }).gmailStatus === 401\n ) {\n throw err;\n }\n // Terminal (404 / 403 / 400): skip this message.\n return \"inaccessible\";\n }\n\n const internalDate = message.internalDate ?? \"\";\n\n // Extract body text.\n const body = message.payload ? extractBodyFromPart(message.payload) : \"\";\n\n if (typeof body !== \"string\" || body.trim().length === 0) {\n // Return the internalDate so the caller can advance the watermark past\n // this immutable empty message (it will never have content).\n return { kind: \"empty\", internalDate };\n }\n if (body.length > MAX_TEXT_BYTES) {\n // Same for too-large: the message is immutable; record its date.\n return { kind: \"too-large\", internalDate };\n }\n\n const subject = extractSubject(message);\n\n return {\n id: messageId,\n title: subject,\n content: body,\n source: {\n connector: GMAIL_CONNECTOR_ID,\n externalId: messageId,\n // Store internalDate (epoch ms string) as the revision so downstream\n // dedup can identify repeat fetches after cursor rewind.\n externalRevision: internalDate.length > 0 ? internalDate : undefined,\n fetchedAt,\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Watermark helpers (exported for tests)\n// ---------------------------------------------------------------------------\n\n/**\n * Convert an `internalDate` epoch-ms string to an ISO 8601 timestamp.\n * Exported for test assertions.\n */\nexport { internalDateToIso, internalDateToEpochSeconds, buildListQuery };\n","/**\n * @remnic/core — GitHub live connector (issue #683 PR 5/6)\n *\n * Concrete `LiveConnector` implementation that incrementally imports notes\n * from a user's GitHub activity into Remnic. Fetches via the GitHub REST\n * API using raw `fetch` with a personal access token — no octokit dep,\n * per à-la-carte packaging rules (CLAUDE.md gotcha #57).\n *\n * What is imported:\n * - Issue comments authored by `userLogin` on watched repos.\n * - PR review comments authored by `userLogin` on watched repos.\n * - Discussion comments authored by `userLogin` (optional, off by default).\n *\n * Design notes:\n *\n * - **Auth.** GitHub personal access token via `connectors.github.token`.\n * The token is accepted at config-parse time but never logged. Operators\n * must populate it from a secret store; no real value may appear in\n * tests, fixtures, or comments.\n *\n * - **Cursor semantics.** The cursor encodes a per-repo, per-resource-type\n * watermark (latest `updated_at` ISO 8601 string seen). On the very first\n * sync (cursor=null) we seed the watermark from the current latest\n * comment timestamp WITHOUT importing any content — mirrors Drive's\n * `getStartPageToken` bootstrap pattern. Subsequent passes only import\n * items created/updated after the stored watermark.\n *\n * - **Watermark field.** All three GitHub resource types expose\n * `updated_at` at the comment level. We always use `updated_at` (not\n * `created_at`) so edits re-trigger ingestion.\n *\n * - **Raw `fetch`.** We call `https://api.github.com/…` directly.\n * `Authorization: Bearer <token>` + `User-Agent: remnic-connector` headers\n * on every request. The `fetchFn` parameter is the test injection point —\n * production callers omit it and the connector uses the global `fetch`.\n *\n * - **Idempotency.** `ConnectorDocument.source.externalId` is\n * `{repo}/{kind}/{commentId}` and `externalRevision` is `updated_at`, so\n * downstream dedup (CLAUDE.md gotcha #44) can recognise repeat fetches.\n *\n * - **Filtering by userLogin.** GitHub's `/issues/comments` endpoint does\n * not support server-side author filtering in the public API. We filter\n * client-side by comparing `comment.user.login` to the configured\n * `userLogin`. This keeps the implementation free from authenticated\n * user lookups and avoids an extra round-trip on first run.\n *\n * - **Privacy.** No comment body is ever logged. Repo names and counts\n * may be logged. The token is never exposed in logs, state, or errors.\n *\n * - **Read-only.** This connector only reads. It never posts, edits,\n * reacts to, or otherwise mutates any GitHub resource.\n *\n * - **Error classification.** 429/5xx → transient (re-throw, cursor\n * does NOT advance). 404/403/410 → terminal (skip repo/resource,\n * continue). Network errors → transient.\n */\n\nimport type {\n ConnectorConfig,\n ConnectorCursor,\n ConnectorDocument,\n LiveConnector,\n SyncIncrementalArgs,\n SyncIncrementalResult,\n} from \"./framework.js\";\n\n// ---------------------------------------------------------------------------\n// Public constants\n// ---------------------------------------------------------------------------\n\n/** Stable connector id. */\nexport const GITHUB_CONNECTOR_ID = \"github\";\n\n/** Cursor `kind` emitted by this connector. */\nexport const GITHUB_CURSOR_KIND = \"githubWatermark\";\n\n/** Default poll interval: 5 minutes. */\nexport const GITHUB_DEFAULT_POLL_INTERVAL_MS = 5 * 60 * 1000;\n\n/** Hard cap on poll interval: 24 hours. */\nconst GITHUB_MAX_POLL_INTERVAL_MS = 24 * 60 * 60 * 1000;\n\n/** Hard cap on body text we'll accept for a single comment. */\nconst MAX_BODY_BYTES = 5 * 1024 * 1024;\n\n/** Maximum number of items (across all repos and resource types) per pass. */\nconst MAX_ITEMS_PER_PASS = 200;\n\n/** Page size for GitHub list requests. Maximum allowed by the API. */\nconst GITHUB_PAGE_SIZE = 100;\n\n// ---------------------------------------------------------------------------\n// Config types\n// ---------------------------------------------------------------------------\n\n/**\n * Validated, frozen view of `connectors.github.*`.\n */\nexport interface GitHubConnectorConfig {\n /** Personal access token. Populate from a secret store; never commit. */\n readonly token: string;\n /** Only import comments authored by this GitHub login. Required. */\n readonly userLogin: string;\n /** Repos to poll, in `owner/repo` format. */\n readonly repos: readonly string[];\n /** Poll interval in ms. */\n readonly pollIntervalMs: number;\n /** Whether to import Discussion comments. Default false. */\n readonly includeDiscussions: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Cursor payload\n// ---------------------------------------------------------------------------\n\n/**\n * JSON payload encoded into `ConnectorCursor.value`.\n *\n * Watermarks are stored per repo per resource kind. We use ISO 8601 strings\n * (which sort lexicographically) for all comparisons — no epoch math needed.\n */\ninterface GitHubCursorPayload {\n /**\n * Maps `{repo}/{kind}` → latest `updated_at` ISO string already ingested.\n * `kind` is one of `\"issue-comment\"`, `\"pr-review-comment\"`, `\"discussion\"`.\n */\n watermarks: Record<string, string>;\n /**\n * Same-second dedup map: maps `{repo}/{kind}/{commentId}` → `updated_at`\n * ISO string for every comment processed within the same second as the\n * current watermark. Cleared when the watermark advances past that second\n * boundary. Prevents re-importing comments whose `updated_at` matches the\n * watermark exactly — GitHub's `since=` filter is inclusive, so comments at\n * the exact watermark timestamp are re-returned on every subsequent poll.\n *\n * Mirrors the Gmail connector's `seenIds` pattern from #745.\n */\n seenIds: Record<string, string>;\n}\n\n// ---------------------------------------------------------------------------\n// GitHub API response shapes (only the fields we consume)\n// ---------------------------------------------------------------------------\n\nexport interface GitHubComment {\n readonly id: number;\n readonly body?: string | null;\n readonly user?: { readonly login?: string | null } | null;\n readonly created_at: string;\n readonly updated_at: string;\n readonly html_url?: string | null;\n /** Present on PR review comments. */\n readonly pull_request_url?: string | null;\n /** Present on issue comments. */\n readonly issue_url?: string | null;\n}\n\nexport interface GitHubDiscussionComment {\n readonly id: number;\n readonly body?: string | null;\n readonly author?: { readonly login?: string | null } | null;\n readonly createdAt?: string | null;\n readonly updatedAt?: string | null;\n readonly url?: string | null;\n}\n\n// ---------------------------------------------------------------------------\n// Fetch abstraction (test hook)\n// ---------------------------------------------------------------------------\n\n/**\n * Minimal fetch-compatible surface used by the connector. Tests inject a\n * stub; production delegates to global `fetch`.\n */\nexport type GitHubFetchFn = (\n url: string,\n init: {\n method: string;\n headers: Record<string, string>;\n signal?: AbortSignal;\n },\n) => Promise<{\n ok: boolean;\n status: number;\n headers: { get(name: string): string | null };\n json(): Promise<unknown>;\n}>;\n\n// ---------------------------------------------------------------------------\n// Config validation\n// ---------------------------------------------------------------------------\n\n/** Pattern for `owner/repo`. Both segments allow alphanumeric + `-` + `_` + `.`. */\nconst REPO_SLUG_PATTERN = /^[A-Za-z0-9_.-]+\\/[A-Za-z0-9_.-]+$/;\n\n/**\n * Validate and normalise raw config. Throws with a concrete message on any\n * malformed input — never silently defaults (CLAUDE.md gotcha #51).\n */\nexport function validateGitHubConfig(raw: unknown): GitHubConnectorConfig {\n if (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) {\n throw new TypeError(\n `github: config must be an object, got ${raw === null ? \"null\" : Array.isArray(raw) ? \"array\" : typeof raw}`,\n );\n }\n const r = raw as Record<string, unknown>;\n\n // token\n if (typeof r.token !== \"string\") {\n throw new TypeError(`github: token must be a string (got ${typeof r.token})`);\n }\n const token = r.token.trim();\n if (token.length === 0) {\n throw new RangeError(\"github: token must be non-empty\");\n }\n\n // userLogin\n if (typeof r.userLogin !== \"string\") {\n throw new TypeError(`github: userLogin must be a string (got ${typeof r.userLogin})`);\n }\n const userLogin = r.userLogin.trim();\n if (userLogin.length === 0) {\n throw new RangeError(\"github: userLogin must be non-empty\");\n }\n\n // pollIntervalMs\n let pollIntervalMs: number;\n if (r.pollIntervalMs === undefined) {\n pollIntervalMs = GITHUB_DEFAULT_POLL_INTERVAL_MS;\n } else if (typeof r.pollIntervalMs !== \"number\" || !Number.isFinite(r.pollIntervalMs)) {\n throw new TypeError(\n `github: pollIntervalMs must be a finite number (got ${JSON.stringify(r.pollIntervalMs)})`,\n );\n } else if (!Number.isInteger(r.pollIntervalMs)) {\n throw new TypeError(`github: pollIntervalMs must be an integer (got ${r.pollIntervalMs})`);\n } else if (r.pollIntervalMs < 1_000) {\n throw new RangeError(`github: pollIntervalMs must be ≥1000ms; got ${r.pollIntervalMs}`);\n } else if (r.pollIntervalMs > GITHUB_MAX_POLL_INTERVAL_MS) {\n throw new RangeError(\n `github: pollIntervalMs must be ≤${GITHUB_MAX_POLL_INTERVAL_MS} (24h); got ${r.pollIntervalMs}`,\n );\n } else {\n pollIntervalMs = r.pollIntervalMs;\n }\n\n // repos\n let repos: readonly string[] = [];\n if (r.repos !== undefined) {\n if (!Array.isArray(r.repos)) {\n throw new TypeError(\n `github: repos must be an array of strings (got ${typeof r.repos})`,\n );\n }\n const seen = new Set<string>();\n const out: string[] = [];\n for (const value of r.repos) {\n if (typeof value !== \"string\") {\n throw new TypeError(\n `github: repos entries must be strings; found ${typeof value}`,\n );\n }\n const trimmed = value.trim();\n if (!REPO_SLUG_PATTERN.test(trimmed)) {\n throw new RangeError(\n `github: repos entry ${JSON.stringify(value)} is not a valid \"owner/repo\" slug`,\n );\n }\n // Dedupe per CLAUDE.md gotcha #49.\n if (seen.has(trimmed)) continue;\n seen.add(trimmed);\n out.push(trimmed);\n }\n repos = Object.freeze(out);\n }\n\n // includeDiscussions (optional, default false)\n let includeDiscussions = false;\n if (r.includeDiscussions !== undefined) {\n if (typeof r.includeDiscussions !== \"boolean\") {\n throw new TypeError(\n `github: includeDiscussions must be a boolean (got ${typeof r.includeDiscussions})`,\n );\n }\n includeDiscussions = r.includeDiscussions;\n }\n\n return Object.freeze({\n token,\n userLogin,\n repos,\n pollIntervalMs,\n includeDiscussions,\n });\n}\n\n// ---------------------------------------------------------------------------\n// Cursor helpers\n// ---------------------------------------------------------------------------\n\nfunction makeCursor(payload: GitHubCursorPayload): ConnectorCursor {\n return {\n kind: GITHUB_CURSOR_KIND,\n value: JSON.stringify(payload),\n updatedAt: new Date().toISOString(),\n };\n}\n\nfunction parseCursorPayload(cursor: ConnectorCursor): GitHubCursorPayload {\n if (cursor.kind !== GITHUB_CURSOR_KIND) {\n throw new Error(\n `github: unexpected cursor kind ${JSON.stringify(cursor.kind)}; expected ${GITHUB_CURSOR_KIND}`,\n );\n }\n // CLAUDE.md gotcha #18: validate after parse.\n let parsed: unknown;\n try {\n parsed = JSON.parse(cursor.value);\n } catch {\n throw new Error(`github: cursor value is not valid JSON`);\n }\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n throw new Error(`github: cursor value does not match GitHubCursorPayload shape`);\n }\n const p = parsed as Record<string, unknown>;\n const watermarks =\n typeof p.watermarks === \"object\" && p.watermarks !== null && !Array.isArray(p.watermarks)\n ? (p.watermarks as Record<string, string>)\n : {};\n // seenIds: tolerate missing key (old cursors lack it).\n const seenIds: Record<string, string> =\n typeof p.seenIds === \"object\" && p.seenIds !== null && !Array.isArray(p.seenIds)\n ? (p.seenIds as Record<string, string>)\n : {};\n return { watermarks, seenIds };\n}\n\nfunction watermarkKey(repo: string, kind: string): string {\n return `${repo}/${kind}`;\n}\n\n// ---------------------------------------------------------------------------\n// Error classification\n// ---------------------------------------------------------------------------\n\n/**\n * Classify a fetch error as transient (re-throw — cursor does NOT advance,\n * next poll retries) vs. terminal (skip this repo/resource and continue).\n *\n * Transient:\n * - 429 (rate-limit — retry after backoff)\n * - 5xx (GitHub backend error)\n * - AbortError / network-layer errors\n *\n * Terminal (skip-and-continue):\n * - 404 (repo deleted, comment gone, or no access)\n * - 403 (permission denied)\n * - 410 (gone)\n * - any other 4xx that isn't 429\n */\nexport function isTransientGitHubError(err: unknown): boolean {\n if (err === null || typeof err !== \"object\") return false;\n const e = err as {\n name?: unknown;\n code?: unknown;\n status?: unknown;\n githubStatus?: unknown;\n message?: unknown;\n };\n\n // AbortError\n if (typeof e.name === \"string\" && e.name === \"AbortError\") return true;\n\n // HTTP status attached by our own error-throwing code.\n const status = pickNumericGitHubStatus(e);\n if (status !== undefined) {\n if (status === 429) return true;\n if (status >= 500 && status <= 599) return true;\n // Any classified 4xx that isn't 429 is terminal.\n return false;\n }\n\n // Network-layer error codes.\n const codeStr = typeof e.code === \"string\" ? e.code : undefined;\n if (codeStr !== undefined) {\n const transientCodes = new Set([\n \"ECONNRESET\",\n \"ECONNREFUSED\",\n \"ECONNABORTED\",\n \"ETIMEDOUT\",\n \"ESOCKETTIMEDOUT\",\n \"ENOTFOUND\",\n \"EAI_AGAIN\",\n \"EPIPE\",\n \"EHOSTUNREACH\",\n \"ENETUNREACH\",\n \"ENETDOWN\",\n \"ERR_NETWORK\",\n \"ERR_NETWORK_CHANGED\",\n ]);\n if (transientCodes.has(codeStr)) return true;\n return false;\n }\n\n // No status, no code — treat as transient (plain network failures).\n return true;\n}\n\nfunction pickNumericGitHubStatus(e: {\n status?: unknown;\n githubStatus?: unknown;\n code?: unknown;\n}): number | undefined {\n if (typeof e.githubStatus === \"number\" && Number.isFinite(e.githubStatus)) {\n return e.githubStatus;\n }\n if (typeof e.status === \"number\" && Number.isFinite(e.status)) {\n return e.status;\n }\n return undefined;\n}\n\n// ---------------------------------------------------------------------------\n// GitHub API client helpers\n// ---------------------------------------------------------------------------\n\nconst GITHUB_API_BASE = \"https://api.github.com\";\n\nfunction throwIfAborted(signal: AbortSignal | undefined): void {\n if (signal?.aborted) {\n const err = new Error(\"github: sync aborted\");\n err.name = \"AbortError\";\n throw err;\n }\n}\n\nfunction makeGitHubApiError(status: number, message: string): Error & { githubStatus: number } {\n const err = new Error(`github: HTTP ${status}: ${message}`) as Error & {\n githubStatus: number;\n };\n err.githubStatus = status;\n return err;\n}\n\n/**\n * Execute a GET request against the GitHub REST API. Returns the parsed JSON\n * body on success. Throws a structured error on non-2xx responses.\n */\nasync function githubGet(\n fetchFn: GitHubFetchFn,\n token: string,\n url: string,\n signal: AbortSignal | undefined,\n): Promise<unknown> {\n const res = await fetchFn(url, {\n method: \"GET\",\n headers: {\n Authorization: `Bearer ${token}`,\n \"User-Agent\": \"remnic-connector\",\n Accept: \"application/vnd.github+json\",\n \"X-GitHub-Api-Version\": \"2022-11-28\",\n },\n signal,\n });\n\n const data = await res.json();\n if (!res.ok) {\n const message =\n typeof data === \"object\" &&\n data !== null &&\n typeof (data as Record<string, unknown>).message === \"string\"\n ? ((data as Record<string, unknown>).message as string)\n : `HTTP ${res.status}`;\n throw makeGitHubApiError(res.status, message);\n }\n return data;\n}\n\n// ---------------------------------------------------------------------------\n// Sync result type\n// ---------------------------------------------------------------------------\n\n/**\n * Result of a single sync pass. Superset of `SyncIncrementalResult` for\n * richer test assertions.\n */\nexport interface GitHubSyncResult extends SyncIncrementalResult {\n readonly skippedOtherAuthor: number;\n readonly skippedEmpty: number;\n readonly skippedTooLarge: number;\n}\n\n// ---------------------------------------------------------------------------\n// Connector factory\n// ---------------------------------------------------------------------------\n\n/**\n * Construct the GitHub connector. `fetchFn` is the test hook — production\n * callers omit it and the connector delegates to global `fetch`.\n */\nexport function createGitHubConnector(\n options: { fetchFn?: GitHubFetchFn } = {},\n): LiveConnector {\n const fetchFn: GitHubFetchFn =\n options.fetchFn ??\n (globalThis.fetch as unknown as GitHubFetchFn);\n\n return {\n id: GITHUB_CONNECTOR_ID,\n displayName: \"GitHub\",\n description:\n \"Imports issue comments, PR review comments, and discussion posts authored by the configured user from watched repos into Remnic.\",\n\n validateConfig(raw: unknown): ConnectorConfig {\n return validateGitHubConfig(raw) as unknown as ConnectorConfig;\n },\n\n async syncIncremental(args: SyncIncrementalArgs): Promise<SyncIncrementalResult> {\n const config = validateGitHubConfig(args.config);\n throwIfAborted(args.abortSignal);\n\n // Short-circuit: nothing to do if no repos are configured.\n if (config.repos.length === 0) {\n const emptyPayload: GitHubCursorPayload = { watermarks: {}, seenIds: {} };\n const result: GitHubSyncResult = {\n newDocs: [],\n nextCursor: makeCursor(emptyPayload),\n skippedOtherAuthor: 0,\n skippedEmpty: 0,\n skippedTooLarge: 0,\n };\n return result;\n }\n\n // Parse or seed cursor.\n const isFirstSync = args.cursor === null;\n const payload: GitHubCursorPayload = isFirstSync\n ? { watermarks: {}, seenIds: {} }\n : parseCursorPayload(args.cursor);\n\n if (isFirstSync) {\n const seededPayload = await seedWatermarks(fetchFn, config, payload, args.abortSignal);\n return {\n newDocs: [],\n nextCursor: makeCursor(seededPayload),\n skippedOtherAuthor: 0,\n skippedEmpty: 0,\n skippedTooLarge: 0,\n } as GitHubSyncResult;\n }\n\n return await incrementalSync(fetchFn, config, payload, args.abortSignal);\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// First-sync: seed watermarks without importing\n// ---------------------------------------------------------------------------\n\n/**\n * For each configured repo and resource type, query the current latest\n * item timestamp and record it as the starting watermark. Returns without\n * emitting any documents, mirroring Drive's `getStartPageToken` pattern.\n */\nasync function seedWatermarks(\n fetchFn: GitHubFetchFn,\n config: GitHubConnectorConfig,\n initial: GitHubCursorPayload,\n signal: AbortSignal | undefined,\n): Promise<GitHubCursorPayload> {\n const watermarks = { ...initial.watermarks };\n // seenIds starts empty for first-sync; nothing has been processed yet.\n\n for (const repo of config.repos) {\n throwIfAborted(signal);\n\n // Issue comments\n try {\n const latest = await fetchLatestTimestamp(\n fetchFn,\n config.token,\n `${GITHUB_API_BASE}/repos/${repo}/issues/comments?sort=updated&direction=desc&per_page=1`,\n \"updated_at\",\n signal,\n );\n if (latest) watermarks[watermarkKey(repo, \"issue-comment\")] = latest;\n } catch (err) {\n if (isTransientGitHubError(err)) throw err;\n // 404/403 → repo inaccessible, skip silently.\n }\n\n throwIfAborted(signal);\n\n // PR review comments\n try {\n const latest = await fetchLatestTimestamp(\n fetchFn,\n config.token,\n `${GITHUB_API_BASE}/repos/${repo}/pulls/comments?sort=updated&direction=desc&per_page=1`,\n \"updated_at\",\n signal,\n );\n if (latest) watermarks[watermarkKey(repo, \"pr-review-comment\")] = latest;\n } catch (err) {\n if (isTransientGitHubError(err)) throw err;\n }\n\n // Discussions (GraphQL not used; we use the REST search endpoint for simplicity)\n if (config.includeDiscussions) {\n throwIfAborted(signal);\n try {\n const latest = await fetchLatestTimestamp(\n fetchFn,\n config.token,\n `${GITHUB_API_BASE}/repos/${repo}/discussions?sort=updated&direction=desc&per_page=1`,\n \"updated_at\",\n signal,\n );\n if (latest) watermarks[watermarkKey(repo, \"discussion\")] = latest;\n } catch (err) {\n if (isTransientGitHubError(err)) throw err;\n }\n }\n }\n\n return { watermarks, seenIds: {} };\n}\n\n/**\n * Fetch the first page of a sorted list and return the `updated_at` field of\n * the first item, or `undefined` if the list is empty.\n */\nasync function fetchLatestTimestamp(\n fetchFn: GitHubFetchFn,\n token: string,\n url: string,\n field: string,\n signal: AbortSignal | undefined,\n): Promise<string | undefined> {\n const data = await githubGet(fetchFn, token, url, signal);\n if (!Array.isArray(data) || data.length === 0) return undefined;\n const first = data[0];\n if (typeof first !== \"object\" || first === null) return undefined;\n const ts = (first as Record<string, unknown>)[field];\n return typeof ts === \"string\" && ts.length > 0 ? ts : undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Incremental sync\n// ---------------------------------------------------------------------------\n\nasync function incrementalSync(\n fetchFn: GitHubFetchFn,\n config: GitHubConnectorConfig,\n payload: GitHubCursorPayload,\n signal: AbortSignal | undefined,\n): Promise<GitHubSyncResult> {\n const fetchedAt = new Date().toISOString();\n const newDocs: ConnectorDocument[] = [];\n const updatedWatermarks = { ...payload.watermarks };\n let skippedOtherAuthor = 0;\n let skippedEmpty = 0;\n let skippedTooLarge = 0;\n let totalConsumed = 0;\n\n // P1 fix (same-timestamp dedup): carry seenIds forward from the cursor.\n // Maps `{repo}/{kind}/{commentId}` → `updated_at` ISO string for all\n // comments processed within the same second as their resource watermark.\n // Cleared per-resource when the watermark advances into a new second.\n const currentSeenIds: Record<string, string> = { ...payload.seenIds };\n // Accumulate seenIds updates for each resource; merged into nextSeenIds below.\n const updatedSeenIds: Record<string, string> = { ...payload.seenIds };\n\n for (const repo of config.repos) {\n if (totalConsumed >= MAX_ITEMS_PER_PASS) break;\n throwIfAborted(signal);\n\n // --- Issue comments ---\n {\n const wmKey = watermarkKey(repo, \"issue-comment\");\n const since = payload.watermarks[wmKey];\n try {\n const result = await fetchAndFilterComments(\n fetchFn,\n config.token,\n buildIssueCommentsUrl(repo, since),\n repo,\n \"issue-comment\",\n config.userLogin,\n since,\n fetchedAt,\n MAX_ITEMS_PER_PASS - totalConsumed,\n currentSeenIds,\n signal,\n );\n for (const doc of result.docs) newDocs.push(doc);\n skippedOtherAuthor += result.skippedOtherAuthor;\n skippedEmpty += result.skippedEmpty;\n skippedTooLarge += result.skippedTooLarge;\n totalConsumed += result.consumed;\n if (result.latestWatermark) {\n const prevWm = updatedWatermarks[wmKey];\n const nextWm = result.latestWatermark;\n updatedWatermarks[wmKey] = nextWm;\n // Clear seenIds for this resource if the watermark crossed a second\n // boundary; otherwise merge the newly-seen ids.\n if (prevWm && watermarkCrossedSecond(prevWm, nextWm)) {\n for (const k of Object.keys(updatedSeenIds)) {\n if (k.startsWith(`${repo}/issue-comment/`)) delete updatedSeenIds[k];\n }\n }\n for (const [k, v] of Object.entries(result.newSeenIds)) {\n updatedSeenIds[k] = v;\n }\n }\n } catch (err) {\n if (isTransientGitHubError(err)) throw err;\n // Terminal (404/403): skip this resource for this repo.\n }\n }\n\n if (totalConsumed >= MAX_ITEMS_PER_PASS) break;\n throwIfAborted(signal);\n\n // --- PR review comments ---\n {\n const wmKey = watermarkKey(repo, \"pr-review-comment\");\n const since = payload.watermarks[wmKey];\n try {\n const result = await fetchAndFilterComments(\n fetchFn,\n config.token,\n buildPrReviewCommentsUrl(repo, since),\n repo,\n \"pr-review-comment\",\n config.userLogin,\n since,\n fetchedAt,\n MAX_ITEMS_PER_PASS - totalConsumed,\n currentSeenIds,\n signal,\n );\n for (const doc of result.docs) newDocs.push(doc);\n skippedOtherAuthor += result.skippedOtherAuthor;\n skippedEmpty += result.skippedEmpty;\n skippedTooLarge += result.skippedTooLarge;\n totalConsumed += result.consumed;\n if (result.latestWatermark) {\n const prevWm = updatedWatermarks[wmKey];\n const nextWm = result.latestWatermark;\n updatedWatermarks[wmKey] = nextWm;\n if (prevWm && watermarkCrossedSecond(prevWm, nextWm)) {\n for (const k of Object.keys(updatedSeenIds)) {\n if (k.startsWith(`${repo}/pr-review-comment/`)) delete updatedSeenIds[k];\n }\n }\n for (const [k, v] of Object.entries(result.newSeenIds)) {\n updatedSeenIds[k] = v;\n }\n }\n } catch (err) {\n if (isTransientGitHubError(err)) throw err;\n }\n }\n\n // --- Discussion comments (optional) ---\n if (config.includeDiscussions && totalConsumed < MAX_ITEMS_PER_PASS) {\n throwIfAborted(signal);\n const wmKey = watermarkKey(repo, \"discussion\");\n const since = payload.watermarks[wmKey];\n try {\n const result = await fetchAndFilterComments(\n fetchFn,\n config.token,\n buildDiscussionsUrl(repo, since),\n repo,\n \"discussion\",\n config.userLogin,\n since,\n fetchedAt,\n MAX_ITEMS_PER_PASS - totalConsumed,\n currentSeenIds,\n signal,\n );\n for (const doc of result.docs) newDocs.push(doc);\n skippedOtherAuthor += result.skippedOtherAuthor;\n skippedEmpty += result.skippedEmpty;\n skippedTooLarge += result.skippedTooLarge;\n totalConsumed += result.consumed;\n if (result.latestWatermark) {\n const prevWm = updatedWatermarks[wmKey];\n const nextWm = result.latestWatermark;\n updatedWatermarks[wmKey] = nextWm;\n if (prevWm && watermarkCrossedSecond(prevWm, nextWm)) {\n for (const k of Object.keys(updatedSeenIds)) {\n if (k.startsWith(`${repo}/discussion/`)) delete updatedSeenIds[k];\n }\n }\n for (const [k, v] of Object.entries(result.newSeenIds)) {\n updatedSeenIds[k] = v;\n }\n }\n } catch (err) {\n if (isTransientGitHubError(err)) throw err;\n }\n }\n }\n\n return {\n newDocs,\n nextCursor: makeCursor({ watermarks: updatedWatermarks, seenIds: updatedSeenIds }),\n skippedOtherAuthor,\n skippedEmpty,\n skippedTooLarge,\n };\n}\n\n/**\n * Returns true when the new watermark has crossed into a new second relative\n * to the previous watermark. ISO 8601 strings sort lexicographically and the\n * second boundary is at the 19-character prefix (e.g. \"2026-04-26T09:00:01\").\n */\nfunction watermarkCrossedSecond(prev: string, next: string): boolean {\n return prev.slice(0, 19) < next.slice(0, 19);\n}\n\n// ---------------------------------------------------------------------------\n// URL builders\n// ---------------------------------------------------------------------------\n\nfunction buildIssueCommentsUrl(repo: string, since?: string): string {\n const base = `${GITHUB_API_BASE}/repos/${repo}/issues/comments?sort=updated&direction=asc&per_page=${GITHUB_PAGE_SIZE}`;\n return since ? `${base}&since=${encodeURIComponent(since)}` : base;\n}\n\nfunction buildPrReviewCommentsUrl(repo: string, since?: string): string {\n const base = `${GITHUB_API_BASE}/repos/${repo}/pulls/comments?sort=updated&direction=asc&per_page=${GITHUB_PAGE_SIZE}`;\n return since ? `${base}&since=${encodeURIComponent(since)}` : base;\n}\n\nfunction buildDiscussionsUrl(repo: string, since?: string): string {\n // GitHub Discussions REST API (available for repos with discussions enabled).\n // No server-side `since` filter exists, so we page and filter client-side.\n const base = `${GITHUB_API_BASE}/repos/${repo}/discussions?sort=updated&direction=asc&per_page=${GITHUB_PAGE_SIZE}`;\n return since ? `${base}&since=${encodeURIComponent(since)}` : base;\n}\n\n// ---------------------------------------------------------------------------\n// Comment fetching + filtering\n// ---------------------------------------------------------------------------\n\ninterface FetchAndFilterResult {\n docs: ConnectorDocument[];\n skippedOtherAuthor: number;\n skippedEmpty: number;\n skippedTooLarge: number;\n /** Count of items that were actually ingested (budget-counted). Skipped\n * items (wrong author, empty body, too-large, seenIds dedup) do NOT\n * consume budget — see P1 fix `PRRT_kwDORJXyws59sfBs`. */\n consumed: number;\n /** Latest `updated_at` we saw in this batch (includes skipped items so the\n * watermark can advance past them). */\n latestWatermark: string | undefined;\n /**\n * New seenId entries accumulated during this pass. Maps\n * `{repo}/{kind}/{commentId}` → `updated_at` ISO string for every ingested\n * comment. Used by the caller to build the next cursor's `seenIds` map\n * (P1 fix `PRRT_kwDORJXyws59sfBq`).\n */\n newSeenIds: Record<string, string>;\n}\n\n/**\n * Page through the comments at `firstPageUrl`, filter to comments authored\n * by `userLogin`, and build `ConnectorDocument` instances. Respects the\n * per-pass cap via `remainingBudget`.\n *\n * Budget fix (P1 `PRRT_kwDORJXyws59sfBs`): only count ingested records\n * against the budget. Items skipped for wrong author, empty/too-large body,\n * or same-second seenId dedup do NOT advance the cap counter — they should\n * not starve valid records.\n *\n * Same-timestamp dedup (P1 `PRRT_kwDORJXyws59sfBq`): `seenIds` carries\n * comment ids already processed within the same second as the current\n * watermark. If GitHub re-returns them because `since=` is inclusive and\n * matches the exact watermark second, we skip them without re-importing.\n *\n * Uses `since` as a client-side lower-bound filter in addition to the\n * server-side `?since=` param (the server may return items exactly at\n * the watermark that we already ingested).\n */\nasync function fetchAndFilterComments(\n fetchFn: GitHubFetchFn,\n token: string,\n firstPageUrl: string,\n repo: string,\n kind: string,\n userLogin: string,\n since: string | undefined,\n fetchedAt: string,\n remainingBudget: number,\n seenIds: Record<string, string>,\n signal: AbortSignal | undefined,\n): Promise<FetchAndFilterResult> {\n const docs: ConnectorDocument[] = [];\n let skippedOtherAuthor = 0;\n let skippedEmpty = 0;\n let skippedTooLarge = 0;\n let consumed = 0;\n let latestWatermark: string | undefined = undefined;\n const newSeenIds: Record<string, string> = {};\n let nextUrl: string | undefined = firstPageUrl;\n\n while (nextUrl && consumed < remainingBudget) {\n throwIfAborted(signal);\n\n const data = await githubGet(fetchFn, token, nextUrl, signal);\n if (!Array.isArray(data)) break;\n\n for (const item of data) {\n if (consumed >= remainingBudget) break;\n throwIfAborted(signal);\n\n const comment = item as GitHubComment;\n\n // Skip items strictly before the watermark. Items whose `updated_at`\n // equals the watermark second must pass through so the seenIds check\n // below can distinguish already-ingested comments from new ones at the\n // same timestamp. Using `<` (strict) here is intentional — `<=` would\n // make seenIds unreachable for boundary items, permanently dropping any\n // comment that arrives in the same second as the current watermark.\n if (since && comment.updated_at < since) {\n continue;\n }\n\n // Same-second dedup: skip only if this exact (id, updated_at) pair was\n // already ingested on a prior pass. A later edit of the same comment\n // produces a newer `updated_at`, so we must NOT gate on id alone — we\n // must also confirm the timestamp matches before skipping. This prevents\n // silent data loss when a comment is edited after its first ingestion.\n const seenKey = `${repo}/${kind}/${comment.id}`;\n if (seenIds[seenKey] === comment.updated_at) {\n continue;\n }\n\n // Author filter (client-side). Not counted against budget —\n // P1 fix `PRRT_kwDORJXyws59sfBs`: only ingested records consume budget.\n const authorLogin = comment.user?.login ?? null;\n if (authorLogin !== userLogin) {\n skippedOtherAuthor++;\n // Still track watermark for non-matching items to prevent re-fetching\n // them on every subsequent poll.\n if (!latestWatermark || comment.updated_at > latestWatermark) {\n latestWatermark = comment.updated_at;\n }\n continue;\n }\n\n // Body validation. Also not counted against budget.\n const body = comment.body ?? \"\";\n const trimmed = body.trim();\n if (trimmed.length === 0) {\n skippedEmpty++;\n if (!latestWatermark || comment.updated_at > latestWatermark) {\n latestWatermark = comment.updated_at;\n }\n continue;\n }\n if (trimmed.length > MAX_BODY_BYTES) {\n skippedTooLarge++;\n if (!latestWatermark || comment.updated_at > latestWatermark) {\n latestWatermark = comment.updated_at;\n }\n continue;\n }\n\n // This item will be ingested — count it against the budget.\n consumed++;\n\n // Build document.\n const doc = buildDocument(comment, repo, kind, fetchedAt);\n docs.push(doc);\n\n if (!latestWatermark || comment.updated_at > latestWatermark) {\n latestWatermark = comment.updated_at;\n }\n\n // Record in newSeenIds for same-second dedup on subsequent polls.\n newSeenIds[seenKey] = comment.updated_at;\n }\n\n // Follow GitHub's `Link: <url>; rel=\"next\"` header for pagination.\n // We don't have direct header access via the minimal fetch abstraction,\n // so pagination is signaled by a full page being returned. If the page\n // has fewer items than GITHUB_PAGE_SIZE we've reached the end.\n // This is conservative but correct — a short page always means \"no more\".\n if (data.length < GITHUB_PAGE_SIZE) {\n nextUrl = undefined;\n } else {\n // Full page received — there may be more. Advance via page parameter.\n nextUrl = advancePageUrl(nextUrl);\n }\n }\n\n return { docs, skippedOtherAuthor, skippedEmpty, skippedTooLarge, consumed, latestWatermark, newSeenIds };\n}\n\n/**\n * Advance a paginated GitHub URL by incrementing the `page` query parameter.\n * GitHub uses 1-based page numbers; if no `page` param is present we assume\n * we're on page 1 and bump to page 2.\n */\nfunction advancePageUrl(url: string): string {\n try {\n const u = new URL(url);\n const page = parseInt(u.searchParams.get(\"page\") ?? \"1\", 10);\n u.searchParams.set(\"page\", String(isNaN(page) ? 2 : page + 1));\n return u.toString();\n } catch {\n // If URL parsing fails, bail — don't loop infinitely.\n return \"\";\n }\n}\n\n// ---------------------------------------------------------------------------\n// Document builder\n// ---------------------------------------------------------------------------\n\nfunction buildDocument(\n comment: GitHubComment,\n repo: string,\n kind: string,\n fetchedAt: string,\n): ConnectorDocument {\n const externalId = `${repo}/${kind}/${comment.id}`;\n const externalUrl =\n typeof comment.html_url === \"string\" && comment.html_url.length > 0\n ? comment.html_url\n : undefined;\n const title = buildTitle(repo, kind, comment);\n\n return {\n id: externalId,\n title,\n content: (comment.body ?? \"\").trim(),\n source: {\n connector: GITHUB_CONNECTOR_ID,\n externalId,\n externalRevision: comment.updated_at,\n externalUrl,\n fetchedAt,\n },\n };\n}\n\n/**\n * Build a short human-readable title for the comment document.\n * We avoid fetching the issue/PR title to keep the connector read-light.\n */\nfunction buildTitle(repo: string, kind: string, comment: GitHubComment): string {\n const kindLabel =\n kind === \"issue-comment\"\n ? \"Issue comment\"\n : kind === \"pr-review-comment\"\n ? \"PR review comment\"\n : \"Discussion comment\";\n return `${kindLabel} in ${repo} (#${comment.id})`;\n}\n"],"mappings":"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","names":["MAX_TEXT_BYTES","makeCursor","throwIfAborted","MAX_TEXT_BYTES","CLIENT_SECRET_FIELD","REFRESH_TOKEN_FIELD","requireNonEmptyString","makeCursor","parseCursorPayload","throwIfAborted","CLIENT_SECRET_FIELD","REFRESH_TOKEN_FIELD","makeCursor","parseCursorPayload","incrementalSync","MAX_TEXT_BYTES","makeCursor","parseCursorPayload","throwIfAborted","incrementalSync"]}
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
} from "./chunk-DT5TVLJE.js";
|
|
5
5
|
import {
|
|
6
6
|
StorageManager
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-MJLUHRSF.js";
|
|
8
8
|
import {
|
|
9
9
|
getCachedRuleMemories,
|
|
10
10
|
setCachedRuleMemories
|
|
@@ -115,4 +115,4 @@ async function searchVerifiedSemanticRules(options) {
|
|
|
115
115
|
export {
|
|
116
116
|
searchVerifiedSemanticRules
|
|
117
117
|
};
|
|
118
|
-
//# sourceMappingURL=chunk-
|
|
118
|
+
//# sourceMappingURL=chunk-6OAQEOGV.js.map
|