negotium 0.1.11 → 0.1.13
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/README.md +11 -2
- package/dist/canonical-mcp-bridge.js +2 -3
- package/dist/canonical-mcp-bridge.js.map +1 -1
- package/dist/{chunk-gz12qcf0.js → chunk-a9y2rwxf.js} +2 -2
- package/dist/{chunk-gz12qcf0.js.map → chunk-a9y2rwxf.js.map} +3 -3
- package/dist/{chunk-j0kjscb7.js → chunk-s2gez3wg.js} +20 -2
- package/dist/{chunk-j0kjscb7.js.map → chunk-s2gez3wg.js.map} +2 -2
- package/dist/cron.js +292 -256
- package/dist/cron.js.map +23 -22
- package/dist/hosted-agent.js +3012 -17
- package/dist/hosted-agent.js.map +30 -3
- package/dist/main.js +31323 -36
- package/dist/main.js.map +198 -3
- package/dist/prompts.js +12 -9
- package/dist/prompts.js.map +5 -5
- package/dist/registry.js +10 -6
- package/dist/registry.js.map +3 -3
- package/dist/rollout.js +1 -1
- package/dist/runtime/cron/store.ts +24 -1
- package/dist/runtime/src/application/submit-user-message.ts +3 -0
- package/dist/runtime/src/index.ts +7 -0
- package/dist/runtime/src/mcp/runtime-spec.ts +3 -0
- package/dist/runtime/src/mcp/session-comm/server.ts +1 -1
- package/dist/runtime/src/platform/config.ts +2 -2
- package/dist/runtime/src/platform/mcp-config.ts +13 -0
- package/dist/runtime/src/prompts/builders.ts +20 -6
- package/dist/runtime/src/query/active-rooms.ts +2 -0
- package/dist/runtime/src/runtime/delivery-ack.ts +112 -0
- package/dist/runtime/src/runtime/errors.ts +12 -12
- package/dist/runtime/src/runtime/turn-runner.ts +12 -0
- package/dist/runtime/src/storage/api-messages.ts +7 -5
- package/dist/runtime/src/storage/api-topics.ts +5 -2
- package/dist/runtime/src/storage/forum-db.ts +4 -1
- package/dist/runtime/src/storage/runtime-turn-requests.ts +1 -0
- package/dist/runtime/src/storage/session-asks.ts +13 -5
- package/dist/runtime/src/storage/storage-contract.ts +54 -0
- package/dist/runtime/src/storage/storage-host.ts +89 -36
- package/dist/runtime/src/storage/storage-public-host.ts +16 -0
- package/dist/runtime/src/storage/storage-public.ts +21 -5
- package/dist/runtime/src/storage/token-stats.ts +1 -1
- package/dist/runtime/src/types/api.ts +2 -0
- package/dist/runtime/src/types.ts +2 -0
- package/dist/runtime/src/version.ts +1 -1
- package/dist/runtime-helpers.js +29 -18
- package/dist/runtime-helpers.js.map +8 -7
- package/dist/storage.js +260 -202
- package/dist/storage.js.map +11 -9
- package/dist/types/packages/core/src/mcp/runtime-spec.d.ts +2 -0
- package/dist/types/packages/core/src/platform/mcp-config.d.ts +4 -0
- package/dist/types/packages/core/src/prompts/builders.d.ts +2 -0
- package/dist/types/packages/core/src/storage/api-messages.d.ts +6 -6
- package/dist/types/packages/core/src/storage/api-topics.d.ts +20 -2
- package/dist/types/packages/core/src/storage/forum-db.d.ts +2 -1
- package/dist/types/packages/core/src/storage/session-asks.d.ts +3 -2
- package/dist/types/packages/core/src/storage/storage-contract.d.ts +46 -0
- package/dist/types/packages/core/src/storage/storage-host.d.ts +12 -19
- package/dist/types/packages/core/src/storage/storage-public-host.d.ts +7 -0
- package/dist/types/packages/core/src/storage/storage-public.d.ts +4 -2
- package/dist/types/packages/core/src/types/api.d.ts +2 -0
- package/dist/types/packages/core/src/types.d.ts +2 -0
- package/dist/types/packages/core/src/version.d.ts +1 -1
- package/dist/types/packages/module-cron/src/store.d.ts +22 -2
- package/dist/vault.js +2 -2
- package/dist/vault.js.map +3 -3
- package/package.json +1 -1
- package/dist/chunk-01xcc856.js +0 -149
- package/dist/chunk-01xcc856.js.map +0 -10
- package/dist/chunk-1a6kv9vt.js +0 -118
- package/dist/chunk-1a6kv9vt.js.map +0 -10
- package/dist/chunk-1b62d730.js +0 -117
- package/dist/chunk-1b62d730.js.map +0 -10
- package/dist/chunk-1k7t7bnx.js +0 -97
- package/dist/chunk-1k7t7bnx.js.map +0 -10
- package/dist/chunk-28apmz4r.js +0 -431
- package/dist/chunk-28apmz4r.js.map +0 -11
- package/dist/chunk-2fvydfkx.js +0 -141
- package/dist/chunk-2fvydfkx.js.map +0 -10
- package/dist/chunk-41ygvg5k.js +0 -2387
- package/dist/chunk-41ygvg5k.js.map +0 -26
- package/dist/chunk-4q966ttx.js +0 -371
- package/dist/chunk-4q966ttx.js.map +0 -11
- package/dist/chunk-51rcsavw.js +0 -1712
- package/dist/chunk-51rcsavw.js.map +0 -18
- package/dist/chunk-59d0shyj.js +0 -103
- package/dist/chunk-59d0shyj.js.map +0 -10
- package/dist/chunk-5fzgvn79.js +0 -3536
- package/dist/chunk-5fzgvn79.js.map +0 -20
- package/dist/chunk-61px0717.js +0 -153
- package/dist/chunk-61px0717.js.map +0 -11
- package/dist/chunk-66y0hr8c.js +0 -2136
- package/dist/chunk-66y0hr8c.js.map +0 -29
- package/dist/chunk-69k45zg8.js +0 -250
- package/dist/chunk-69k45zg8.js.map +0 -11
- package/dist/chunk-6ah5farg.js +0 -130
- package/dist/chunk-6ah5farg.js.map +0 -10
- package/dist/chunk-6d4fmea5.js +0 -117
- package/dist/chunk-6d4fmea5.js.map +0 -12
- package/dist/chunk-8cahx7xc.js +0 -46
- package/dist/chunk-8cahx7xc.js.map +0 -10
- package/dist/chunk-8f20fk4n.js +0 -252
- package/dist/chunk-8f20fk4n.js.map +0 -10
- package/dist/chunk-91ckrvk8.js +0 -318
- package/dist/chunk-91ckrvk8.js.map +0 -10
- package/dist/chunk-93krf6ng.js +0 -3069
- package/dist/chunk-93krf6ng.js.map +0 -32
- package/dist/chunk-9qx1t77a.js +0 -214
- package/dist/chunk-9qx1t77a.js.map +0 -10
- package/dist/chunk-beqhdr9w.js +0 -589
- package/dist/chunk-beqhdr9w.js.map +0 -11
- package/dist/chunk-bftftsgq.js +0 -1637
- package/dist/chunk-bftftsgq.js.map +0 -15
- package/dist/chunk-c500xe9z.js +0 -33
- package/dist/chunk-c500xe9z.js.map +0 -10
- package/dist/chunk-cjsnkvq2.js +0 -23
- package/dist/chunk-cjsnkvq2.js.map +0 -10
- package/dist/chunk-cxa7yghg.js +0 -577
- package/dist/chunk-cxa7yghg.js.map +0 -14
- package/dist/chunk-edb07sjp.js +0 -567
- package/dist/chunk-edb07sjp.js.map +0 -14
- package/dist/chunk-f361dwba.js +0 -589
- package/dist/chunk-f361dwba.js.map +0 -11
- package/dist/chunk-f5xgprg1.js +0 -59
- package/dist/chunk-f5xgprg1.js.map +0 -10
- package/dist/chunk-ffefbd3r.js +0 -17
- package/dist/chunk-ffefbd3r.js.map +0 -10
- package/dist/chunk-fswhzdf5.js +0 -64
- package/dist/chunk-fswhzdf5.js.map +0 -10
- package/dist/chunk-g144wdrs.js +0 -2907
- package/dist/chunk-g144wdrs.js.map +0 -24
- package/dist/chunk-h980hb2n.js +0 -512
- package/dist/chunk-h980hb2n.js.map +0 -13
- package/dist/chunk-h9vbhhgr.js +0 -1337
- package/dist/chunk-h9vbhhgr.js.map +0 -16
- package/dist/chunk-hwy3n31w.js +0 -21
- package/dist/chunk-hwy3n31w.js.map +0 -9
- package/dist/chunk-jtrk0khr.js +0 -108
- package/dist/chunk-jtrk0khr.js.map +0 -10
- package/dist/chunk-jzz91jwh.js +0 -70
- package/dist/chunk-jzz91jwh.js.map +0 -10
- package/dist/chunk-m9e3tbr8.js +0 -708
- package/dist/chunk-m9e3tbr8.js.map +0 -14
- package/dist/chunk-mh5mexsf.js +0 -147
- package/dist/chunk-mh5mexsf.js.map +0 -11
- package/dist/chunk-npd8n03v.js +0 -2152
- package/dist/chunk-npd8n03v.js.map +0 -15
- package/dist/chunk-pg1azh6n.js +0 -2
- package/dist/chunk-pg1azh6n.js.map +0 -9
- package/dist/chunk-qwy57177.js +0 -237
- package/dist/chunk-qwy57177.js.map +0 -11
- package/dist/chunk-r31q21h5.js +0 -53
- package/dist/chunk-r31q21h5.js.map +0 -10
- package/dist/chunk-rbvctv1n.js +0 -366
- package/dist/chunk-rbvctv1n.js.map +0 -10
- package/dist/chunk-sa874j5c.js +0 -9
- package/dist/chunk-sa874j5c.js.map +0 -10
- package/dist/chunk-sctcs5zb.js +0 -281
- package/dist/chunk-sctcs5zb.js.map +0 -10
- package/dist/chunk-tsstrcfc.js +0 -122
- package/dist/chunk-tsstrcfc.js.map +0 -10
- package/dist/chunk-wk2qf31s.js +0 -58
- package/dist/chunk-wk2qf31s.js.map +0 -10
- package/dist/chunk-wx7ag4dd.js +0 -2936
- package/dist/chunk-wx7ag4dd.js.map +0 -20
- package/dist/chunk-znx1fpfd.js +0 -61
- package/dist/chunk-znx1fpfd.js.map +0 -10
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../../../packages/core/src/topics/lifecycle.ts", "../../../packages/core/src/agents/topic-cleanup.ts", "../../../packages/core/src/query/session-inbox-cleanup.ts", "../../../packages/core/src/query/session-inbox-path.ts", "../../../packages/core/src/storage/self-schedules.ts"],
|
|
4
|
-
"sourcesContent": [
|
|
5
|
-
"/**\n * Topic hard-delete with full memory preservation — the single source of\n * truth shared by every host surface (negotium MCP `delete_topic`, CLI, any\n * channel adapter). Ported from otium runtime-api `api/topic-lifecycle.ts`\n * minus peer/cron cascades (deferred subsystems on a standalone node).\n */\n\nimport { rmSync } from \"node:fs\";\nimport { runArchiverTurn } from \"#agents/archiver\";\nimport { cancelIdleArchiveForTopic } from \"#agents/idle-archiver\";\nimport { cancelSubagentWatchForDeletedTopic } from \"#agents/mcp-tools/spawn-subagent\";\nimport { type PurgeSessionRef, purgeTopicLogs } from \"#agents/topic-cleanup\";\nimport { WsHub } from \"#bus\";\nimport { killBgBash } from \"#platform/background-bash/manager\";\nimport { resolveTopicWorkspaceDir } from \"#platform/config\";\nimport { GENERAL_TOPIC_ID } from \"#platform/constants\";\nimport { delay } from \"#platform/delay\";\nimport { logger } from \"#platform/logger\";\nimport { closeBrowserOwnerTabs, deleteTopicProfileDir } from \"#platform/playwright/manager\";\nimport { abortRoom, getRoomQuery, interSessionQueue } from \"#query/active-rooms\";\nimport { cleanupSessionInboxFiles } from \"#query/session-inbox-cleanup\";\nimport { clearQueryState } from \"#query/state\";\nimport { cancelAskCallbacksForTopic } from \"#runtime/ask-callbacks\";\nimport { deleteFilesForTopic } from \"#runtime/file-hooks\";\nimport { clearQueryUsageAlert } from \"#runtime/usage-alert\";\nimport { deleteTopicVisuals } from \"#runtime/visual-store\";\nimport { deleteMessagesForTopic } from \"#storage/api-messages\";\nimport { deleteTopicBrief } from \"#storage/api-topic-brief\";\nimport { deleteApiTopicConfig } from \"#storage/api-topic-config\";\nimport {\n deleteTopic as deleteTopicDB,\n getTopicMemoryOrigin,\n getTopicSessionId,\n listTopics,\n reparentTopicChildren,\n} from \"#storage/api-topics\";\nimport { getBrowserProfileOwner, getTopicBrowserProfile } from \"#storage/browser-profiles\";\nimport { getRuntimeTurnLease } from \"#storage/runtime-leases\";\nimport { beginRuntimeTopicMaintenance } from \"#storage/runtime-topic-state\";\nimport {\n cancelRuntimeUserTurnRequests,\n cancelRuntimeUserTurnRequestsBeforeEpoch,\n} from \"#storage/runtime-turn-requests\";\nimport { deleteSelfSchedulesForTopic } from \"#storage/self-schedules\";\nimport { deletePendingAsksForTopic } from \"#storage/session-asks\";\nimport { archiveTopicMessages } from \"#storage/topic-archive\";\nimport { deleteTopicArchiveState } from \"#storage/topic-archive-state\";\nimport type { TopicDto } from \"#types/api\";\n\nconst DELETE_TURN_WAIT_MS = 5_000;\n\nasync function abortAndWaitForTopic(topicId: string): Promise<boolean> {\n // Drop first: the dying turn's finally block must not dispatch queued work.\n interSessionQueue.drop(topicId);\n cancelIdleArchiveForTopic(topicId);\n const aborted = abortRoom(topicId);\n if (!aborted) return true;\n\n logger.info({ topicId }, \"deleteTopicCascade: aborted in-flight turn\");\n const deadline = Date.now() + DELETE_TURN_WAIT_MS;\n while ((getRoomQuery(topicId) || getRuntimeTurnLease(topicId)) && Date.now() < deadline) {\n await delay(50);\n }\n if (getRoomQuery(topicId) || getRuntimeTurnLease(topicId)) {\n logger.warn(\n { topicId, timeoutMs: DELETE_TURN_WAIT_MS },\n \"deleteTopicCascade: timed out waiting for in-flight turn cleanup\",\n );\n return false;\n }\n return true;\n}\n\nfunction currentSessionRef(topic: TopicDto, sessionId: string | null): PurgeSessionRef[] {\n return topic.agent && sessionId ? [{ agent: topic.agent, sessionId }] : [];\n}\n\nasync function cleanupParticipantResources(\n topic: TopicDto,\n userIds: string[],\n sessionId: string | null,\n cwd: string,\n): Promise<void> {\n for (const [index, participantUserId] of userIds.entries()) {\n try {\n await purgeTopicLogs({\n userId: participantUserId,\n topicName: topic.title,\n cwd,\n // The DB only has one current native session. Its unified-log owner is\n // not knowable after multi-user turns, so include it once as a fallback;\n // every participant manifest is still scanned for older sessions.\n extraSessions: index === 0 ? currentSessionRef(topic, sessionId) : [],\n });\n } catch (err) {\n logger.warn(\n { err, topicId: topic.id, userId: participantUserId },\n \"deleteTopicCascade: participant rollout cleanup failed\",\n );\n }\n\n cleanupSessionInboxFiles(participantUserId, topic.id, topic.title);\n clearQueryState(participantUserId, topic.title);\n clearQueryUsageAlert(participantUserId, topic.id);\n deletePendingAsksForTopic({ userId: participantUserId, topicName: topic.title });\n }\n}\n\nexport class TopicArchiveRequiredError extends Error {\n readonly code = \"TOPIC_ARCHIVE_FAILED\";\n readonly topicId: string;\n\n constructor(topicId: string, cause: unknown) {\n super(\"Topic archive failed; deletion was blocked to avoid losing conversation history.\");\n this.name = \"TopicArchiveRequiredError\";\n this.topicId = topicId;\n this.cause = cause;\n }\n}\n\nexport class TopicTurnStillActiveError extends Error {\n readonly code = \"TOPIC_TURN_STILL_ACTIVE\";\n readonly topicId: string;\n\n constructor(topicId: string) {\n super(\n \"The active turn did not stop in time; deletion was blocked. Use force-delete explicitly.\",\n );\n this.name = \"TopicTurnStillActiveError\";\n this.topicId = topicId;\n }\n}\n\nexport interface DeleteTopicCascadeOptions {\n force?: boolean;\n /** Account deletion may remove that account's otherwise-protected private General. */\n allowManager?: boolean;\n /** Account deletion must not recreate a private General via the archiver. */\n skipArchive?: boolean;\n}\n\n/**\n * Sequence (memory BEFORE wipe):\n * 1. archiveTopicMessages → dump SQLite conversation to the shared wiki\n * `archive/` as forensic JSONL.\n * 2. runArchiverTurn → background wiki-archiver (summaries/articles/brief).\n * 3. purge topic-owned runtime/filesystem/adapter state.\n * 4. delete messages + config + brief + the topic row.\n *\n * Step 1 is required by default: if the raw archive cannot be written, deletion\n * is blocked to avoid losing conversation history. Pass `force: true` only for\n * an explicit force-delete escape hatch. Step 2 is best-effort because the raw\n * transcript has already been preserved.\n * Caller is responsible for guards (essential-topic / owner / existence).\n */\nexport async function deleteTopicCascade(\n topic: TopicDto,\n userId: string,\n options: DeleteTopicCascadeOptions = {},\n): Promise<void> {\n await deleteTopicCascadeImpl(topic, userId, options, new Set());\n}\n\nasync function deleteTopicCascadeImpl(\n topic: TopicDto,\n userId: string,\n options: DeleteTopicCascadeOptions,\n deletingAncestorIds: ReadonlySet<string>,\n): Promise<void> {\n const topicId = topic.id;\n if (topicId === GENERAL_TOPIC_ID || (topic.kind === \"manager\" && !options.allowManager)) {\n logger.warn({ topicId }, \"deleteTopicCascade: refused to delete essential topic\");\n return;\n }\n const deletingTopicIds = new Set(deletingAncestorIds);\n deletingTopicIds.add(topicId);\n const force = options.force === true;\n const maintenance = beginRuntimeTopicMaintenance(topicId);\n if (!maintenance) throw new Error(\"Topic maintenance is already in progress.\");\n let deleted = false;\n const cancelledQueryIds = cancelRuntimeUserTurnRequestsBeforeEpoch(topicId, maintenance.epoch);\n for (const queryId of cancelledQueryIds) {\n WsHub.get().broadcastAborted(topicId, queryId, \"stopped\");\n }\n\n try {\n // Wait for the aborted turn's final session/log writes before taking the\n // forensic snapshot. Normal deletion blocks on timeout; only the explicit\n // force-delete escape hatch may proceed while a provider is still wedged.\n const turnStopped = await abortAndWaitForTopic(topicId);\n if (!turnStopped && !force) throw new TopicTurnStillActiveError(topicId);\n if (!maintenance.isOwned()) throw new Error(\"Topic maintenance ownership was lost.\");\n let archived: ReturnType<typeof archiveTopicMessages> = null;\n\n if (!options.skipArchive) {\n try {\n archived = archiveTopicMessages(topicId, topic.title);\n } catch (err) {\n if (!force) {\n logger.warn(\n { err, topicId },\n \"deleteTopicCascade: archive failed - blocking delete unless force=true\",\n );\n throw new TopicArchiveRequiredError(topicId, err);\n }\n logger.warn(\n { err, topicId },\n \"deleteTopicCascade: archive failed but force=true - continuing with hard delete\",\n );\n }\n }\n\n if (archived) {\n try {\n const memoryTopic = getTopicMemoryOrigin(topicId) ?? topic;\n runArchiverTurn({\n userId,\n // A memory origin that is also disappearing in this cascade must use\n // file + General memory only. Passing its id to wiki MCP would let the\n // detached archiver recreate an orphan api_topic_brief row later.\n ...(memoryTopic.id !== topicId && !deletingTopicIds.has(memoryTopic.id)\n ? { topicId: memoryTopic.id }\n : {}),\n topicTitle: memoryTopic.title,\n archivePath: archived.path,\n messageCount: archived.messageCount,\n });\n } catch (err) {\n logger.warn(\n { err, topicId, archive: archived.path },\n \"deleteTopicCascade: background archiver launch failed - raw archive preserved\",\n );\n }\n }\n\n cancelSubagentWatchForDeletedTopic(topicId);\n\n // spawn_subagent rooms are owned by the room that created them. Delete\n // them through the same lifecycle before removing the parent so their\n // active turns, archives, rollouts, and filesystem state are all cleaned\n // up. Other derived rooms (fork/spawn) remain independent and are\n // reparented below as before.\n const spawnedChildren = listTopics().filter(\n (candidate) =>\n candidate.parentTopicId === topicId &&\n candidate.isSubagent &&\n !deletingTopicIds.has(candidate.id),\n );\n for (const child of spawnedChildren) {\n await deleteTopicCascadeImpl(child, userId, options, deletingTopicIds);\n }\n\n const sessionId = getTopicSessionId(topicId);\n const cwd = resolveTopicWorkspaceDir(topicId);\n const participantUserIds = Array.from(\n new Set([userId, ...topic.participants.map((participant) => participant.userId)]),\n );\n\n // Background shells remain topic-owned. Browser profiles are shared, so\n // deletion closes this topic's tabs without deleting login state.\n killBgBash(userId, topicId);\n const browserProfileOwner = getBrowserProfileOwner(topicId, userId);\n const browserProfile = getTopicBrowserProfile(topicId);\n try {\n await closeBrowserOwnerTabs(browserProfileOwner, browserProfile, `topic:${topicId}`);\n } catch (err) {\n logger.warn({ err, topicId }, \"deleteTopicCascade: browser owner cleanup failed\");\n }\n deleteTopicProfileDir(userId, topicId);\n cancelAskCallbacksForTopic(topicId);\n deleteSelfSchedulesForTopic(topicId);\n\n await cleanupParticipantResources(topic, participantUserIds, sessionId, cwd);\n try {\n await deleteFilesForTopic(topicId);\n } catch (err) {\n logger.warn({ err, topicId }, \"deleteTopicCascade: host file cleanup failed\");\n }\n try {\n rmSync(cwd, { recursive: true, force: true });\n } catch (err) {\n logger.warn({ err, topicId, cwd }, \"deleteTopicCascade: workspace cleanup failed\");\n }\n\n deleteTopicVisuals(topicId);\n deleteTopicArchiveState(topicId);\n deleteMessagesForTopic(topicId);\n deleteApiTopicConfig(topicId);\n deleteTopicBrief(topicId);\n const reparentedChildIds = reparentTopicChildren(topicId, topic.parentTopicId ?? null);\n const rowDeleted = deleteTopicDB(topicId, { allowManager: options.allowManager });\n if (!rowDeleted) {\n logger.warn({ topicId }, \"deleteTopicCascade: topic row was not deleted\");\n return;\n }\n deleted = true;\n // Mirror the deletion onto the bus so channel adapters (Telegram forum\n // threads, web clients, …) can drop their bindings for the topic.\n WsHub.get().broadcastTopicDeleted(topicId);\n for (const childId of reparentedChildIds) WsHub.get().broadcastTopicUpdated(childId);\n } finally {\n if (deleted) {\n for (const queryId of cancelRuntimeUserTurnRequests(topicId)) {\n WsHub.get().broadcastAborted(topicId, queryId, \"stopped\");\n }\n maintenance.finish({ deleteState: true });\n } else {\n maintenance.finish();\n }\n }\n}\n",
|
|
6
|
-
"/**\n * Topic-level conversation cleanup.\n *\n * The unified conversation log (`data/conversations/<userId>/<topic>.jsonl`)\n * is **transient** by design — its only job is to feed the cross-agent\n * bridge in `set_agent` and to give per-agent SDK rollout reconstruction a\n * provider-agnostic source. Once a topic is reset (`/new`) or deleted\n * (`/del`, MCP `delete_topic`), the unified log AND every per-agent SDK\n * rollout file the topic produced should disappear together so a subsequent\n * fresh-start on the same topic name (or a stray `set_agent` call against a\n * recreated row) cannot resurrect orphan history.\n *\n * Permanent forensic preservation lives in `wiki/archive/` via\n * `archiveSessionLogs`; this module is the matching teardown side.\n */\n\nimport { mkdirSync, renameSync, unlinkSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { SUPPORTED_AGENTS } from \"#agents/index\";\nimport { getRegistry } from \"#agents/registry\";\nimport { logger } from \"#platform/logger\";\nimport {\n type ConversationEntry,\n getConversationPath,\n readConversation,\n} from \"#storage/conversations\";\nimport type { AgentKind } from \"#types\";\n\nexport interface PurgeSessionRef {\n agent: AgentKind;\n sessionId: string;\n}\n\n/**\n * Group every SDK session id this topic ever produced by its emitting\n * agent. `set_agent` round-trips can leave many ids per agent (one per\n * switch back), so we collect into a Set to deduplicate before deletion.\n */\nfunction collectSessionIdsByAgent(\n entries: ConversationEntry[],\n extraSessions: PurgeSessionRef[] = [],\n): Map<AgentKind, Set<string>> {\n const out = new Map<AgentKind, Set<string>>();\n for (const e of entries) {\n if (e.event.type !== \"session\") continue;\n const set = out.get(e.agent) ?? new Set<string>();\n set.add(e.event.sessionId);\n out.set(e.agent, set);\n }\n for (const { agent, sessionId } of extraSessions) {\n if (!sessionId) continue;\n const set = out.get(agent) ?? new Set<string>();\n set.add(sessionId);\n out.set(agent, set);\n }\n return out;\n}\n\nexport interface PurgeTopicLogsOptions {\n userId: number | string;\n /** Topic key for the unified log path. Use `__dm__` for the DM session. */\n topicName: string;\n /**\n * Working directory the topic ran with — Claude embeds an encoded copy\n * in its rollout path, so a wrong value would silently miss the actual\n * files. API topics should pass `resolveTopicWorkspaceDir(topicId)`.\n */\n cwd: string;\n /**\n * Additional SDK resume keys that may not have reached the unified log yet.\n *\n * `set_agent` writes a synthetic rollout and stores its id in DB before the\n * target agent's first real turn emits a `session` event. If the user resets\n * or deletes the topic in that window, the DB session id is the only manifest\n * entry for that synthetic file.\n */\n extraSessions?: PurgeSessionRef[];\n}\n\nasync function cleanupSessionRollouts(\n opts: PurgeTopicLogsOptions,\n entries: ConversationEntry[],\n): Promise<boolean> {\n const { userId, topicName, cwd, extraSessions = [] } = opts;\n const idsByAgent = collectSessionIdsByAgent(entries, extraSessions);\n let cleanupFailed = false;\n\n await Promise.all(\n SUPPORTED_AGENTS.map(async (agent) => {\n const ids = idsByAgent.get(agent);\n if (!ids || ids.size === 0) return;\n try {\n await getRegistry(agent).cleanupRollouts({\n cwd,\n sessionIds: Array.from(ids),\n });\n } catch (e) {\n logger.warn(\n { err: e, userId, topicName, agent, count: ids.size },\n \"topic logs: agent cleanup failed\",\n );\n cleanupFailed = true;\n }\n }),\n );\n\n return !cleanupFailed;\n}\n\n/** Remove every provider rollout currently manifested by a topic log. */\nexport async function cleanupTopicRollouts(opts: PurgeTopicLogsOptions): Promise<boolean> {\n return cleanupSessionRollouts(opts, readConversation(opts.userId, opts.topicName));\n}\n\nexport interface RotateTopicLogsOptions extends PurgeTopicLogsOptions {\n /** Number of complete user turns carried into the replacement session. */\n retainTurns: number;\n}\n\nexport interface RotateTopicLogsResult {\n rotated: boolean;\n totalTurns: number;\n retainedTurns: number;\n retainedEntries: number;\n}\n\n/**\n * Replace a topic's native provider sessions while retaining a bounded tail\n * of its provider-neutral conversation log. This is used by long-lived Cron\n * topics: a fresh native rollout keeps prompt/session state bounded, and the\n * retained tail gives the next rollout enough context to continue naturally.\n */\nexport async function rotateTopicLogs(\n opts: RotateTopicLogsOptions,\n): Promise<RotateTopicLogsResult> {\n const retainTurns = Math.max(0, Math.floor(opts.retainTurns));\n const entries = readConversation(opts.userId, opts.topicName);\n const userEntryIndexes = entries.flatMap((entry, index) =>\n entry.event.type === \"user_message\" ? [index] : [],\n );\n const totalTurns = userEntryIndexes.length;\n const firstRetainedIndex =\n retainTurns === 0\n ? entries.length\n : (userEntryIndexes[Math.max(0, totalTurns - retainTurns)] ?? 0);\n // Native session ids point at rollouts that are removed below. Keeping\n // those manifest entries would make later cleanup treat deleted files as\n // live context, so only conversational events cross the rotation boundary.\n const retained = entries\n .slice(firstRetainedIndex)\n .filter((entry) => entry.event.type !== \"session\");\n\n if (!(await cleanupSessionRollouts(opts, entries))) {\n logger.warn(\n { userId: opts.userId, topicName: opts.topicName },\n \"rotateTopicLogs: keeping current context because rollout cleanup failed\",\n );\n return {\n rotated: false,\n totalTurns,\n retainedTurns: Math.min(totalTurns, retainTurns),\n retainedEntries: retained.length,\n };\n }\n\n const path = getConversationPath(opts.userId, opts.topicName);\n const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;\n try {\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(\n tempPath,\n retained.length > 0 ? `${retained.map((entry) => JSON.stringify(entry)).join(\"\\n\")}\\n` : \"\",\n { flag: \"wx\" },\n );\n renameSync(tempPath, path);\n } catch (e) {\n try {\n unlinkSync(tempPath);\n } catch {}\n logger.warn({ err: e, path }, \"rotateTopicLogs: conversation replacement failed\");\n return {\n rotated: false,\n totalTurns,\n retainedTurns: Math.min(totalTurns, retainTurns),\n retainedEntries: retained.length,\n };\n }\n\n return {\n rotated: true,\n totalTurns,\n retainedTurns: Math.min(totalTurns, retainTurns),\n retainedEntries: retained.length,\n };\n}\n\n/**\n * Best-effort teardown of every artifact associated with a topic's\n * conversation lifecycle. Order matters and is intentional:\n *\n * 1. Read the unified log first — it's the authoritative manifest of\n * every SDK session id the topic has emitted. Merge any DB session id\n * passed via `extraSessions` for synthetic rollouts not yet emitted.\n * 2. Dispatch per-agent rollout cleanup in parallel (independent\n * filesystems, no shared state).\n * 3. Unlink the unified log LAST so a partial rollout-cleanup failure\n * leaves the manifest in place for a future retry.\n *\n * Errors at every step are logged but never thrown — cleanup runs at\n * topic teardown when there is nothing left to abort to.\n */\nexport async function purgeTopicLogs(opts: PurgeTopicLogsOptions): Promise<void> {\n const { userId, topicName } = opts;\n const entries = readConversation(userId, topicName);\n if (!(await cleanupSessionRollouts(opts, entries))) {\n logger.warn(\n { userId, topicName },\n \"purgeTopicLogs: keeping unified log because one or more rollout cleanups failed\",\n );\n return;\n }\n\n const path = getConversationPath(userId, topicName);\n try {\n unlinkSync(path);\n } catch (e) {\n if ((e as NodeJS.ErrnoException)?.code !== \"ENOENT\") {\n logger.warn({ err: e, path }, \"purgeTopicLogs: unified log unlink failed\");\n }\n }\n}\n",
|
|
7
|
-
"import { unlinkSync } from \"node:fs\";\nimport { basename, join } from \"node:path\";\nimport { SESSION_INBOX_DIR } from \"#platform/config\";\nimport { logger } from \"#platform/logger\";\nimport { scheduledSessionInboxPath, sessionInboxPath } from \"#query/session-inbox-path\";\n\n/**\n * Delete pending, scheduled, and crash-claimed inbox files for a topic.\n * Canonical files are id-keyed; title-keyed candidates cover rolling upgrades.\n */\nexport function cleanupSessionInboxFiles(\n userId: string,\n topicId: string,\n legacyTopicTitle?: string,\n): number {\n const live = sessionInboxPath(userId, topicId);\n const scheduled = scheduledSessionInboxPath(userId, topicId);\n const candidates = new Set([live, `${live}.processing`, scheduled, `${scheduled}.processing`]);\n\n if (\n legacyTopicTitle &&\n legacyTopicTitle !== \".\" &&\n legacyTopicTitle !== \"..\" &&\n basename(legacyTopicTitle) === legacyTopicTitle\n ) {\n const legacyBase = join(SESSION_INBOX_DIR, userId, legacyTopicTitle);\n for (const suffix of [\".jsonl\", \".jsonl.processing\", \".schedule\", \".schedule.processing\"]) {\n candidates.add(`${legacyBase}${suffix}`);\n }\n }\n\n let deleted = 0;\n for (const path of candidates) {\n try {\n unlinkSync(path);\n deleted++;\n } catch (err) {\n if ((err as NodeJS.ErrnoException)?.code !== \"ENOENT\") {\n logger.warn({ err, path, topicId, userId }, \"topic cleanup: inbox unlink failed\");\n }\n }\n }\n return deleted;\n}\n",
|
|
8
|
-
"import { join } from \"node:path\";\nimport { SESSION_INBOX_DIR } from \"#platform/config\";\n\nconst TOPIC_ID_FILE_PREFIX = \"topic-id-\";\nconst JSONL_SUFFIX = \".jsonl\";\nconst SCHEDULE_SUFFIX = \".schedule\";\n\n/** Build a flat inbox path from the canonical topic id, never its title. */\nexport function sessionInboxPath(userId: string, topicId: string): string {\n const key = Buffer.from(topicId, \"utf8\").toString(\"base64url\");\n return join(SESSION_INBOX_DIR, userId, `${TOPIC_ID_FILE_PREFIX}${key}${JSONL_SUFFIX}`);\n}\n\n/** Durable sidecar consumed by the session-inbox worker once deliverAt passes. */\nexport function scheduledSessionInboxPath(userId: string, topicId: string): string {\n const key = Buffer.from(topicId, \"utf8\").toString(\"base64url\");\n return join(SESSION_INBOX_DIR, userId, `${TOPIC_ID_FILE_PREFIX}${key}${SCHEDULE_SUFFIX}`);\n}\n\nfunction decodeTopicIdFileName(fileName: string, suffix: string): string | null {\n if (!fileName.startsWith(TOPIC_ID_FILE_PREFIX) || !fileName.endsWith(suffix)) return null;\n const encoded = fileName.slice(TOPIC_ID_FILE_PREFIX.length, -suffix.length);\n if (!encoded || !/^[A-Za-z0-9_-]+$/.test(encoded)) return null;\n try {\n return Buffer.from(encoded, \"base64url\").toString(\"utf8\") || null;\n } catch {\n return null;\n }\n}\n\n/** Null denotes a legacy `{topicTitle}.jsonl` filename from an older runtime. */\nexport function topicIdFromSessionInboxFileName(fileName: string): string | null {\n return decodeTopicIdFileName(fileName, JSONL_SUFFIX);\n}\n\n/** Accept both a live .schedule file and a crash-residue .schedule.processing claim. */\nexport function topicIdFromScheduledSessionInboxFileName(fileName: string): string | null {\n const base = fileName.endsWith(\".processing\")\n ? fileName.slice(0, -\".processing\".length)\n : fileName;\n return decodeTopicIdFileName(base, SCHEDULE_SUFFIX);\n}\n",
|
|
9
|
-
"import { randomUUID } from \"node:crypto\";\nimport { db } from \"#storage/forum-db\";\nimport { TURN_LEASE_STALE_MS } from \"#storage/runtime-leases\";\nimport { TOPIC_MAINTENANCE_STALE_MS } from \"#storage/runtime-topic-state\";\n\nexport type SelfScheduleStatus = \"pending\" | \"running\";\n\nexport interface SelfSchedule {\n id: string;\n topicId: string;\n userId: string;\n message: string;\n deliverAt: number;\n createdAt: number;\n updatedAt: number;\n status: SelfScheduleStatus;\n claimedBy?: string;\n claimedAt?: number;\n runningQueryId?: string;\n}\n\ninterface SelfScheduleRow {\n id: string;\n topic_id: string;\n user_id: string;\n message: string;\n deliver_at: number | bigint;\n created_at: number | bigint;\n updated_at: number | bigint;\n status: string;\n claimed_by: string | null;\n claimed_at: number | bigint | null;\n running_query_id: string | null;\n}\n\ndb.exec(`\n CREATE TABLE IF NOT EXISTS runtime_self_schedules (\n id TEXT PRIMARY KEY,\n topic_id TEXT NOT NULL,\n user_id TEXT NOT NULL,\n message TEXT NOT NULL,\n deliver_at INTEGER NOT NULL,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'running')),\n claimed_by TEXT,\n claimed_at INTEGER,\n running_query_id TEXT\n )\n`);\ndb.exec(`\n CREATE UNIQUE INDEX IF NOT EXISTS idx_runtime_self_schedules_one_pending\n ON runtime_self_schedules(topic_id)\n WHERE status = 'pending'\n`);\ndb.exec(`\n CREATE INDEX IF NOT EXISTS idx_runtime_self_schedules_due\n ON runtime_self_schedules(status, deliver_at)\n`);\n\nfunction rowToSchedule(row: SelfScheduleRow): SelfSchedule {\n return {\n id: row.id,\n topicId: row.topic_id,\n userId: row.user_id,\n message: row.message,\n deliverAt: Number(row.deliver_at),\n createdAt: Number(row.created_at),\n updatedAt: Number(row.updated_at),\n status: row.status === \"running\" ? \"running\" : \"pending\",\n claimedBy: row.claimed_by ?? undefined,\n claimedAt: row.claimed_at === null ? undefined : Number(row.claimed_at),\n runningQueryId: row.running_query_id ?? undefined,\n };\n}\n\nexport type CreatePendingSelfScheduleResult =\n | { ok: true; schedule: SelfSchedule }\n | { ok: false; existing: SelfSchedule };\n\n/** Create the topic's sole pending self-schedule without replacing an existing one. */\nexport function createPendingSelfSchedule(input: {\n topicId: string;\n userId: string;\n message: string;\n deliverAt: number;\n now?: number;\n}): CreatePendingSelfScheduleResult {\n return db\n .transaction(() => {\n const existing = getPendingSelfSchedule(input.topicId);\n if (existing) return { ok: false as const, existing };\n\n const now = input.now ?? Date.now();\n const schedule: SelfSchedule = {\n id: `scheduled-${randomUUID()}`,\n topicId: input.topicId,\n userId: input.userId,\n message: input.message,\n deliverAt: input.deliverAt,\n createdAt: now,\n updatedAt: now,\n status: \"pending\",\n };\n db.query(\n `INSERT INTO runtime_self_schedules\n (id, topic_id, user_id, message, deliver_at, created_at, updated_at, status)\n VALUES (?, ?, ?, ?, ?, ?, ?, 'pending')`,\n ).run(\n schedule.id,\n schedule.topicId,\n schedule.userId,\n schedule.message,\n schedule.deliverAt,\n schedule.createdAt,\n schedule.updatedAt,\n );\n return { ok: true as const, schedule };\n })\n .immediate();\n}\n\nexport function getPendingSelfSchedule(topicId: string): SelfSchedule | null {\n const row = db\n .query<SelfScheduleRow, [string]>(\n \"SELECT * FROM runtime_self_schedules WHERE topic_id = ? AND status = 'pending'\",\n )\n .get(topicId);\n return row ? rowToSchedule(row) : null;\n}\n\nexport function updatePendingSelfSchedule(input: {\n topicId: string;\n scheduleId: string;\n message?: string;\n deliverAt?: number;\n now?: number;\n}): SelfSchedule | null {\n const existing = getPendingSelfSchedule(input.topicId);\n if (!existing || existing.id !== input.scheduleId) return null;\n const result = db\n .query(\n `UPDATE runtime_self_schedules\n SET message = ?, deliver_at = ?, updated_at = ?\n WHERE id = ? AND topic_id = ? AND status = 'pending'`,\n )\n .run(\n input.message ?? existing.message,\n input.deliverAt ?? existing.deliverAt,\n input.now ?? Date.now(),\n input.scheduleId,\n input.topicId,\n );\n return Number(result.changes ?? 0) > 0 ? getPendingSelfSchedule(input.topicId) : null;\n}\n\nexport function cancelPendingSelfSchedule(topicId: string, scheduleId: string): boolean {\n const result = db\n .query(\n \"DELETE FROM runtime_self_schedules WHERE id = ? AND topic_id = ? AND status = 'pending'\",\n )\n .run(scheduleId, topicId);\n return Number(result.changes ?? 0) > 0;\n}\n\n/**\n * Claim one due schedule whose topic is idle. A stale running row is retryable\n * after its process and topic lease disappear, preserving work across crashes.\n */\nexport function claimNextDueSelfSchedule(ownerId: string, now = Date.now()): SelfSchedule | null {\n const staleBefore = now - TURN_LEASE_STALE_MS;\n return db\n .transaction(() => {\n const row = db\n .query<SelfScheduleRow, [number, number, number, number]>(\n `SELECT s.*\n FROM runtime_self_schedules s\n LEFT JOIN runtime_turn_leases l ON l.topic_id = s.topic_id\n LEFT JOIN runtime_topic_state t ON t.topic_id = s.topic_id\n WHERE (l.topic_id IS NULL OR l.heartbeat_at < ?)\n AND (t.topic_id IS NULL OR t.maintenance = 0 OR t.heartbeat_at < ?)\n AND (\n (s.status = 'pending' AND s.deliver_at <= ?)\n OR (s.status = 'running' AND (s.claimed_at IS NULL OR s.claimed_at < ?))\n )\n ORDER BY CASE s.status WHEN 'running' THEN 0 ELSE 1 END, s.deliver_at ASC\n LIMIT 1`,\n )\n .get(staleBefore, now - TOPIC_MAINTENANCE_STALE_MS, now, staleBefore);\n if (!row) return null;\n\n const result = db\n .query(\n `UPDATE runtime_self_schedules\n SET status = 'running', claimed_by = ?, claimed_at = ?, running_query_id = NULL\n WHERE id = ? AND (\n (status = 'pending' AND deliver_at <= ?)\n OR (status = 'running' AND (claimed_at IS NULL OR claimed_at < ?))\n )`,\n )\n .run(ownerId, now, row.id, now, staleBefore);\n if (Number(result.changes ?? 0) === 0) return null;\n return rowToSchedule({\n ...row,\n status: \"running\",\n claimed_by: ownerId,\n claimed_at: now,\n running_query_id: null,\n });\n })\n .immediate();\n}\n\nexport function markSelfScheduleRunning(\n scheduleId: string,\n ownerId: string,\n queryId: string,\n): boolean {\n const result = db\n .query(\n `UPDATE runtime_self_schedules\n SET running_query_id = ?, claimed_at = ?\n WHERE id = ? AND status = 'running' AND claimed_by = ?`,\n )\n .run(queryId, Date.now(), scheduleId, ownerId);\n return Number(result.changes ?? 0) > 0;\n}\n\nexport function heartbeatSelfScheduleClaim(\n scheduleId: string,\n ownerId: string,\n now = Date.now(),\n): boolean {\n const result = db\n .query(\n `UPDATE runtime_self_schedules\n SET claimed_at = ?\n WHERE id = ? AND status = 'running' AND claimed_by = ?`,\n )\n .run(now, scheduleId, ownerId);\n return Number(result.changes ?? 0) > 0;\n}\n\n/**\n * Put an interrupted run back into the single pending slot. If its own turn\n * already created a newer pending schedule, the newer explicit schedule wins.\n */\nexport function releaseSelfScheduleClaim(\n scheduleId: string,\n ownerId: string,\n): \"released\" | \"dropped\" {\n return db\n .transaction(() => {\n const row = db\n .query<SelfScheduleRow, [string, string]>(\n \"SELECT * FROM runtime_self_schedules WHERE id = ? AND status = 'running' AND claimed_by = ?\",\n )\n .get(scheduleId, ownerId);\n if (!row) return \"dropped\" as const;\n const newer = getPendingSelfSchedule(row.topic_id);\n if (newer) {\n db.query(\"DELETE FROM runtime_self_schedules WHERE id = ?\").run(scheduleId);\n return \"dropped\" as const;\n }\n db.query(\n `UPDATE runtime_self_schedules\n SET status = 'pending', claimed_by = NULL, claimed_at = NULL,\n running_query_id = NULL, updated_at = ?\n WHERE id = ? AND claimed_by = ?`,\n ).run(Date.now(), scheduleId, ownerId);\n return \"released\" as const;\n })\n .immediate();\n}\n\nexport function completeSelfSchedule(scheduleId: string, ownerId?: string): boolean {\n const result = ownerId\n ? db\n .query(\"DELETE FROM runtime_self_schedules WHERE id = ? AND claimed_by = ?\")\n .run(scheduleId, ownerId)\n : db.query(\"DELETE FROM runtime_self_schedules WHERE id = ?\").run(scheduleId);\n return Number(result.changes ?? 0) > 0;\n}\n\nexport function deleteSelfSchedulesForTopic(topicId: string): number {\n const result = db.query(\"DELETE FROM runtime_self_schedules WHERE topic_id = ?\").run(topicId);\n return Number(result.changes ?? 0);\n}\n\n/** Test/diagnostic helper covering pending and in-flight rows. */\nexport function listSelfSchedulesForTopic(topicId: string): SelfSchedule[] {\n return db\n .query<SelfScheduleRow, [string]>(\n \"SELECT * FROM runtime_self_schedules WHERE topic_id = ? ORDER BY deliver_at, created_at\",\n )\n .all(topicId)\n .map(rowToSchedule);\n}\n"
|
|
10
|
-
],
|
|
11
|
-
"mappings": "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",
|
|
12
|
-
"debugId": "A77902FD7A938BC964756E2164756E21",
|
|
13
|
-
"names": []
|
|
14
|
-
}
|
package/dist/chunk-edb07sjp.js
DELETED
|
@@ -1,567 +0,0 @@
|
|
|
1
|
-
// ../../packages/mcp-host/src/spec.ts
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
var KEY_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
4
|
-
var portSchema = z.number().int().min(1).max(65535);
|
|
5
|
-
var mcpServerSpecSchema = z.object({
|
|
6
|
-
key: z.string().min(1).regex(KEY_PATTERN, "key may only contain [a-zA-Z0-9._-]").refine((k) => k !== "." && k !== "..", "key may not be a dot path"),
|
|
7
|
-
transport: z.enum(["stdio", "http"]),
|
|
8
|
-
command: z.string().min(1),
|
|
9
|
-
args: z.array(z.string()).optional(),
|
|
10
|
-
env: z.record(z.string(), z.string()).optional(),
|
|
11
|
-
cwd: z.string().optional(),
|
|
12
|
-
portRange: z.object({ base: portSchema, max: portSchema }).optional(),
|
|
13
|
-
scope: z.enum(["node", "instance"]),
|
|
14
|
-
readyTimeoutMs: z.number().int().positive().optional(),
|
|
15
|
-
idleEvictMs: z.number().int().positive().optional(),
|
|
16
|
-
healthIntervalMs: z.number().int().positive().optional()
|
|
17
|
-
}).superRefine((spec, ctx) => {
|
|
18
|
-
if (spec.transport === "http") {
|
|
19
|
-
if (!spec.portRange) {
|
|
20
|
-
ctx.addIssue({
|
|
21
|
-
code: "custom",
|
|
22
|
-
path: ["portRange"],
|
|
23
|
-
message: 'portRange is required when transport is "http"'
|
|
24
|
-
});
|
|
25
|
-
} else if (spec.portRange.max < spec.portRange.base) {
|
|
26
|
-
ctx.addIssue({
|
|
27
|
-
code: "custom",
|
|
28
|
-
path: ["portRange"],
|
|
29
|
-
message: "portRange.max must be >= portRange.base"
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
// ../../packages/mcp-host/src/manifest.ts
|
|
36
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
37
|
-
import { dirname } from "path";
|
|
38
|
-
import { z as z2 } from "zod";
|
|
39
|
-
|
|
40
|
-
// ../../packages/mcp-host/src/paths.ts
|
|
41
|
-
import { homedir } from "os";
|
|
42
|
-
import { resolve } from "path";
|
|
43
|
-
function stateDir() {
|
|
44
|
-
const env = process.env.NEGOTIUM_STATE_DIR?.trim();
|
|
45
|
-
return env ? resolve(env) : resolve(homedir(), ".negotium");
|
|
46
|
-
}
|
|
47
|
-
function defaultPortsDir() {
|
|
48
|
-
return resolve(stateDir(), "run", "mcp-ports");
|
|
49
|
-
}
|
|
50
|
-
function defaultManifestFile() {
|
|
51
|
-
return resolve(stateDir(), "data", "mcp-manifest.json");
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// ../../packages/mcp-host/src/manifest.ts
|
|
55
|
-
var manifestEntrySchema = z2.object({
|
|
56
|
-
spec: mcpServerSpecSchema,
|
|
57
|
-
enabled: z2.boolean()
|
|
58
|
-
});
|
|
59
|
-
var manifestFileSchema = z2.object({
|
|
60
|
-
version: z2.literal(1),
|
|
61
|
-
servers: z2.array(manifestEntrySchema)
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
class McpManifest {
|
|
65
|
-
file;
|
|
66
|
-
entries = new Map;
|
|
67
|
-
constructor(opts) {
|
|
68
|
-
this.file = opts?.file ?? defaultManifestFile();
|
|
69
|
-
this.load();
|
|
70
|
-
}
|
|
71
|
-
list() {
|
|
72
|
-
return [...this.entries.values()].map((e) => structuredClone(e.spec));
|
|
73
|
-
}
|
|
74
|
-
get(key) {
|
|
75
|
-
const entry = this.entries.get(key);
|
|
76
|
-
return entry ? structuredClone(entry.spec) : undefined;
|
|
77
|
-
}
|
|
78
|
-
add(spec) {
|
|
79
|
-
const parsed = mcpServerSpecSchema.parse(spec);
|
|
80
|
-
if (this.entries.has(parsed.key)) {
|
|
81
|
-
throw new Error(`MCP spec already exists in manifest: "${parsed.key}"`);
|
|
82
|
-
}
|
|
83
|
-
this.entries.set(parsed.key, { spec: parsed, enabled: true });
|
|
84
|
-
this.save();
|
|
85
|
-
}
|
|
86
|
-
remove(key) {
|
|
87
|
-
const removed = this.entries.delete(key);
|
|
88
|
-
if (removed)
|
|
89
|
-
this.save();
|
|
90
|
-
return removed;
|
|
91
|
-
}
|
|
92
|
-
setEnabled(key, enabled) {
|
|
93
|
-
const entry = this.entries.get(key);
|
|
94
|
-
if (!entry)
|
|
95
|
-
throw new Error(`Unknown MCP spec: "${key}"`);
|
|
96
|
-
if (entry.enabled === enabled)
|
|
97
|
-
return;
|
|
98
|
-
entry.enabled = enabled;
|
|
99
|
-
this.save();
|
|
100
|
-
}
|
|
101
|
-
isEnabled(key) {
|
|
102
|
-
return this.entries.get(key)?.enabled ?? false;
|
|
103
|
-
}
|
|
104
|
-
load() {
|
|
105
|
-
if (!existsSync(this.file))
|
|
106
|
-
return;
|
|
107
|
-
let parsed;
|
|
108
|
-
try {
|
|
109
|
-
parsed = manifestFileSchema.parse(JSON.parse(readFileSync(this.file, "utf8")));
|
|
110
|
-
} catch (e) {
|
|
111
|
-
throw new Error(`Invalid MCP manifest at ${this.file}: ${e instanceof Error ? e.message : String(e)}`);
|
|
112
|
-
}
|
|
113
|
-
for (const entry of parsed.servers) {
|
|
114
|
-
if (this.entries.has(entry.spec.key)) {
|
|
115
|
-
throw new Error(`Duplicate MCP spec key in manifest ${this.file}: "${entry.spec.key}"`);
|
|
116
|
-
}
|
|
117
|
-
this.entries.set(entry.spec.key, entry);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
save() {
|
|
121
|
-
mkdirSync(dirname(this.file), { recursive: true });
|
|
122
|
-
const payload = {
|
|
123
|
-
version: 1,
|
|
124
|
-
servers: [...this.entries.values()]
|
|
125
|
-
};
|
|
126
|
-
const tmp = `${this.file}.tmp-${process.pid}`;
|
|
127
|
-
writeFileSync(tmp, `${JSON.stringify(payload, null, 2)}
|
|
128
|
-
`);
|
|
129
|
-
renameSync(tmp, this.file);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// ../../packages/mcp-host/src/manager.ts
|
|
134
|
-
import { execFileSync, spawn } from "child_process";
|
|
135
|
-
import {
|
|
136
|
-
mkdirSync as mkdirSync2,
|
|
137
|
-
readdirSync,
|
|
138
|
-
readFileSync as readFileSync2,
|
|
139
|
-
renameSync as renameSync2,
|
|
140
|
-
unlinkSync,
|
|
141
|
-
writeFileSync as writeFileSync2
|
|
142
|
-
} from "fs";
|
|
143
|
-
import { connect, createServer } from "net";
|
|
144
|
-
import { join } from "path";
|
|
145
|
-
|
|
146
|
-
// ../../packages/mcp-host/src/sanitize.ts
|
|
147
|
-
function sanitizePathComponent(name) {
|
|
148
|
-
const safe = name.replace(/[^a-zA-Z0-9._-]/g, "_") || "_";
|
|
149
|
-
if (safe === "." || safe === "..")
|
|
150
|
-
return "_";
|
|
151
|
-
return safe;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// ../../packages/mcp-host/src/manager.ts
|
|
155
|
-
var DEFAULT_READY_TIMEOUT_MS = 20000;
|
|
156
|
-
var READY_POLL_MS = 150;
|
|
157
|
-
var KILL_GRACE_MS = 5000;
|
|
158
|
-
var DEFAULT_SWEEP_INTERVAL_MS = 30000;
|
|
159
|
-
var PROBE_TIMEOUT_MS = 1000;
|
|
160
|
-
var defaultLog = (level, msg, ctx) => {
|
|
161
|
-
const fn = level === "info" ? console.log : level === "warn" ? console.warn : console.error;
|
|
162
|
-
if (ctx)
|
|
163
|
-
fn(`[mcp-host] ${msg}`, ctx);
|
|
164
|
-
else
|
|
165
|
-
fn(`[mcp-host] ${msg}`);
|
|
166
|
-
};
|
|
167
|
-
var delay = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
168
|
-
function regKey(key, instanceKey) {
|
|
169
|
-
return `${key}\x00${instanceKey}`;
|
|
170
|
-
}
|
|
171
|
-
function portFileName(key, instanceKey) {
|
|
172
|
-
return `${sanitizePathComponent(key)}--${sanitizePathComponent(instanceKey)}`;
|
|
173
|
-
}
|
|
174
|
-
function resolveInstanceKey(spec, instanceKey) {
|
|
175
|
-
if (spec.scope === "node")
|
|
176
|
-
return "node";
|
|
177
|
-
if (!instanceKey) {
|
|
178
|
-
throw new Error(`MCP spec "${spec.key}" is instance-scoped; an instanceKey is required`);
|
|
179
|
-
}
|
|
180
|
-
return instanceKey;
|
|
181
|
-
}
|
|
182
|
-
function canBind(port) {
|
|
183
|
-
return new Promise((resolve2) => {
|
|
184
|
-
const srv = createServer();
|
|
185
|
-
srv.once("error", () => resolve2(false));
|
|
186
|
-
srv.listen({ port, host: "127.0.0.1", exclusive: true }, () => {
|
|
187
|
-
srv.close(() => resolve2(true));
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
function tcpProbe(port, timeoutMs = PROBE_TIMEOUT_MS) {
|
|
192
|
-
return new Promise((resolve2) => {
|
|
193
|
-
const sock = connect({ port, host: "127.0.0.1" });
|
|
194
|
-
let settled = false;
|
|
195
|
-
const done = (ok) => {
|
|
196
|
-
if (settled)
|
|
197
|
-
return;
|
|
198
|
-
settled = true;
|
|
199
|
-
sock.destroy();
|
|
200
|
-
resolve2(ok);
|
|
201
|
-
};
|
|
202
|
-
sock.once("connect", () => done(true));
|
|
203
|
-
sock.once("error", () => done(false));
|
|
204
|
-
sock.setTimeout(timeoutMs, () => done(false));
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
function killProcessTree(pid) {
|
|
208
|
-
let children = [];
|
|
209
|
-
try {
|
|
210
|
-
children = execFileSync("pgrep", ["-P", String(pid)], { stdio: "pipe" }).toString().trim().split(`
|
|
211
|
-
`).map((s) => Number.parseInt(s, 10)).filter((n) => !Number.isNaN(n));
|
|
212
|
-
} catch {
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
for (const child of children)
|
|
216
|
-
killProcessTree(child);
|
|
217
|
-
for (const signal of ["SIGTERM", "SIGKILL"]) {
|
|
218
|
-
for (const child of children) {
|
|
219
|
-
try {
|
|
220
|
-
process.kill(child, signal);
|
|
221
|
-
} catch {}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
function waitExit(proc, timeoutMs) {
|
|
226
|
-
if (proc.exitCode !== null || proc.signalCode !== null)
|
|
227
|
-
return Promise.resolve(true);
|
|
228
|
-
return new Promise((resolve2) => {
|
|
229
|
-
const timer = setTimeout(() => resolve2(false), timeoutMs);
|
|
230
|
-
timer.unref?.();
|
|
231
|
-
const settle = () => {
|
|
232
|
-
clearTimeout(timer);
|
|
233
|
-
resolve2(true);
|
|
234
|
-
};
|
|
235
|
-
proc.once("exit", settle);
|
|
236
|
-
proc.once("error", settle);
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
class McpHost {
|
|
241
|
-
manifest;
|
|
242
|
-
portsDir;
|
|
243
|
-
log;
|
|
244
|
-
sweepIntervalMs;
|
|
245
|
-
registry = new Map;
|
|
246
|
-
usedPorts = new Set;
|
|
247
|
-
spawning = new Map;
|
|
248
|
-
sweepInFlight = false;
|
|
249
|
-
closed = false;
|
|
250
|
-
constructor(opts) {
|
|
251
|
-
this.manifest = opts?.manifest ?? new McpManifest;
|
|
252
|
-
this.portsDir = opts?.portsDir ?? defaultPortsDir();
|
|
253
|
-
this.log = opts?.log ?? defaultLog;
|
|
254
|
-
this.sweepIntervalMs = opts?.sweepIntervalMs ?? DEFAULT_SWEEP_INTERVAL_MS;
|
|
255
|
-
}
|
|
256
|
-
async ensure(key, instanceKey) {
|
|
257
|
-
if (this.closed)
|
|
258
|
-
throw new Error("MCP host is stopped");
|
|
259
|
-
const spec = this.requireEnabledSpec(key);
|
|
260
|
-
const ik = resolveInstanceKey(spec, instanceKey);
|
|
261
|
-
if (spec.transport === "stdio") {
|
|
262
|
-
const now = new Date().toISOString();
|
|
263
|
-
return { key: spec.key, instanceKey: ik, startedAt: now, lastUsedAt: now };
|
|
264
|
-
}
|
|
265
|
-
const rkey = regKey(spec.key, ik);
|
|
266
|
-
const inProgress = this.spawning.get(rkey);
|
|
267
|
-
if (inProgress)
|
|
268
|
-
return inProgress;
|
|
269
|
-
const promise = (async () => {
|
|
270
|
-
const existing = this.registry.get(rkey);
|
|
271
|
-
if (existing && !existing.process.killed && existing.process.exitCode === null) {
|
|
272
|
-
if (await tcpProbe(existing.port)) {
|
|
273
|
-
existing.lastUsedAt = Date.now();
|
|
274
|
-
return this.toInstance(existing);
|
|
275
|
-
}
|
|
276
|
-
this.log("warn", "MCP instance unresponsive, restarting", {
|
|
277
|
-
key: spec.key,
|
|
278
|
-
instanceKey: ik,
|
|
279
|
-
port: existing.port
|
|
280
|
-
});
|
|
281
|
-
await this.stopRunning(rkey, existing);
|
|
282
|
-
} else if (existing) {
|
|
283
|
-
this.cleanupRunning(rkey, existing);
|
|
284
|
-
}
|
|
285
|
-
return this.spawnHttp(spec, ik, rkey);
|
|
286
|
-
})().finally(() => this.spawning.delete(rkey));
|
|
287
|
-
this.spawning.set(rkey, promise);
|
|
288
|
-
return promise;
|
|
289
|
-
}
|
|
290
|
-
touch(key, instanceKey) {
|
|
291
|
-
const inst = this.registry.get(regKey(key, instanceKey ?? "node"));
|
|
292
|
-
if (inst)
|
|
293
|
-
inst.lastUsedAt = Date.now();
|
|
294
|
-
}
|
|
295
|
-
async stop(key, instanceKey) {
|
|
296
|
-
const rkey = regKey(key, instanceKey ?? "node");
|
|
297
|
-
const inst = this.registry.get(rkey);
|
|
298
|
-
if (!inst)
|
|
299
|
-
return false;
|
|
300
|
-
await this.stopRunning(rkey, inst);
|
|
301
|
-
return true;
|
|
302
|
-
}
|
|
303
|
-
async stopAll() {
|
|
304
|
-
this.closed = true;
|
|
305
|
-
await Promise.allSettled([...this.spawning.values()]);
|
|
306
|
-
await Promise.all([...this.registry.entries()].map(([rkey, inst]) => this.stopRunning(rkey, inst)));
|
|
307
|
-
}
|
|
308
|
-
listRunning() {
|
|
309
|
-
return [...this.registry.values()].map((inst) => this.toInstance(inst));
|
|
310
|
-
}
|
|
311
|
-
async buildAgentSpec(key, flavor, instanceKey) {
|
|
312
|
-
const spec = this.requireEnabledSpec(key);
|
|
313
|
-
if (spec.transport === "stdio") {
|
|
314
|
-
return {
|
|
315
|
-
command: spec.command,
|
|
316
|
-
...spec.args ? { args: [...spec.args] } : {},
|
|
317
|
-
...spec.env ? { env: { ...spec.env } } : {}
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
const inst = await this.ensure(key, instanceKey);
|
|
321
|
-
if (!inst.url)
|
|
322
|
-
throw new Error(`MCP instance "${key}" has no URL (unexpected for http)`);
|
|
323
|
-
return flavor === "sse" ? { type: "sse", url: `${inst.url}/sse` } : { url: `${inst.url}/mcp` };
|
|
324
|
-
}
|
|
325
|
-
startSweeper() {
|
|
326
|
-
const timer = setInterval(() => {
|
|
327
|
-
this.sweep();
|
|
328
|
-
}, this.sweepIntervalMs);
|
|
329
|
-
timer.unref?.();
|
|
330
|
-
return () => clearInterval(timer);
|
|
331
|
-
}
|
|
332
|
-
requireEnabledSpec(key) {
|
|
333
|
-
const spec = this.manifest.get(key);
|
|
334
|
-
if (!spec)
|
|
335
|
-
throw new Error(`Unknown MCP spec: "${key}"`);
|
|
336
|
-
if (!this.manifest.isEnabled(key))
|
|
337
|
-
throw new Error(`MCP spec "${key}" is disabled`);
|
|
338
|
-
return spec;
|
|
339
|
-
}
|
|
340
|
-
toInstance(inst) {
|
|
341
|
-
return {
|
|
342
|
-
key: inst.key,
|
|
343
|
-
instanceKey: inst.instanceKey,
|
|
344
|
-
pid: inst.process.pid ?? -1,
|
|
345
|
-
port: inst.port,
|
|
346
|
-
url: `http://127.0.0.1:${inst.port}`,
|
|
347
|
-
startedAt: new Date(inst.startedAt).toISOString(),
|
|
348
|
-
lastUsedAt: new Date(inst.lastUsedAt).toISOString()
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
async spawnHttp(spec, instanceKey, rkey) {
|
|
352
|
-
const fileName = portFileName(spec.key, instanceKey);
|
|
353
|
-
const port = await this.allocatePort(spec, fileName);
|
|
354
|
-
if (this.closed) {
|
|
355
|
-
this.usedPorts.delete(port);
|
|
356
|
-
throw new Error("MCP host stopped during server startup");
|
|
357
|
-
}
|
|
358
|
-
const args = (spec.args ?? []).map((a) => a.replaceAll("{port}", String(port)));
|
|
359
|
-
let spawnFailed = false;
|
|
360
|
-
let proc;
|
|
361
|
-
try {
|
|
362
|
-
proc = spawn(spec.command, args, {
|
|
363
|
-
stdio: "ignore",
|
|
364
|
-
detached: false,
|
|
365
|
-
cwd: spec.cwd,
|
|
366
|
-
env: { ...process.env, ...spec.env }
|
|
367
|
-
});
|
|
368
|
-
} catch (e) {
|
|
369
|
-
this.usedPorts.delete(port);
|
|
370
|
-
throw e;
|
|
371
|
-
}
|
|
372
|
-
proc.once("error", (err) => {
|
|
373
|
-
spawnFailed = true;
|
|
374
|
-
this.log("error", "MCP server process error", {
|
|
375
|
-
key: spec.key,
|
|
376
|
-
instanceKey,
|
|
377
|
-
err: String(err)
|
|
378
|
-
});
|
|
379
|
-
const cur = this.registry.get(rkey);
|
|
380
|
-
if (cur?.process === proc)
|
|
381
|
-
this.cleanupRunning(rkey, cur);
|
|
382
|
-
});
|
|
383
|
-
proc.once("exit", (code) => {
|
|
384
|
-
const cur = this.registry.get(rkey);
|
|
385
|
-
if (cur?.process === proc) {
|
|
386
|
-
this.log("warn", "MCP server exited unexpectedly", { key: spec.key, instanceKey, code });
|
|
387
|
-
this.cleanupRunning(rkey, cur);
|
|
388
|
-
}
|
|
389
|
-
});
|
|
390
|
-
const now = Date.now();
|
|
391
|
-
const inst = {
|
|
392
|
-
spec,
|
|
393
|
-
key: spec.key,
|
|
394
|
-
instanceKey,
|
|
395
|
-
process: proc,
|
|
396
|
-
port,
|
|
397
|
-
fileName,
|
|
398
|
-
startedAt: now,
|
|
399
|
-
lastUsedAt: now,
|
|
400
|
-
lastHealthAt: now
|
|
401
|
-
};
|
|
402
|
-
this.registry.set(rkey, inst);
|
|
403
|
-
const timeoutMs = spec.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
|
|
404
|
-
const deadline = Date.now() + timeoutMs;
|
|
405
|
-
let ready = false;
|
|
406
|
-
while (Date.now() < deadline) {
|
|
407
|
-
if (this.closed || spawnFailed || proc.exitCode !== null)
|
|
408
|
-
break;
|
|
409
|
-
if (await tcpProbe(port, 500)) {
|
|
410
|
-
ready = true;
|
|
411
|
-
break;
|
|
412
|
-
}
|
|
413
|
-
await delay(READY_POLL_MS);
|
|
414
|
-
}
|
|
415
|
-
if (!ready) {
|
|
416
|
-
const exitCode = proc.exitCode;
|
|
417
|
-
await this.stopRunning(rkey, inst);
|
|
418
|
-
throw new Error(`MCP server "${spec.key}" (${instanceKey}) not ready on port ${port} within ${timeoutMs}ms` + (exitCode === null ? "" : ` (exitCode=${exitCode})`));
|
|
419
|
-
}
|
|
420
|
-
this.writePortFile(fileName, port);
|
|
421
|
-
this.log("info", "MCP server started", { key: spec.key, instanceKey, port, pid: proc.pid });
|
|
422
|
-
return this.toInstance(inst);
|
|
423
|
-
}
|
|
424
|
-
async allocatePort(spec, fileName) {
|
|
425
|
-
const range = spec.portRange;
|
|
426
|
-
if (!range)
|
|
427
|
-
throw new Error(`MCP spec "${spec.key}" is http but has no portRange`);
|
|
428
|
-
mkdirSync2(this.portsDir, { recursive: true });
|
|
429
|
-
const claims = this.readPortClaims();
|
|
430
|
-
const own = claims.get(fileName);
|
|
431
|
-
if (own !== undefined && own >= range.base && own <= range.max && !this.usedPorts.has(own)) {
|
|
432
|
-
this.usedPorts.add(own);
|
|
433
|
-
if (await canBind(own))
|
|
434
|
-
return own;
|
|
435
|
-
this.usedPorts.delete(own);
|
|
436
|
-
}
|
|
437
|
-
const foreign = new Set;
|
|
438
|
-
for (const [file, port] of claims) {
|
|
439
|
-
if (file !== fileName)
|
|
440
|
-
foreign.add(port);
|
|
441
|
-
}
|
|
442
|
-
for (let port = range.base;port <= range.max; port++) {
|
|
443
|
-
if (this.usedPorts.has(port) || foreign.has(port))
|
|
444
|
-
continue;
|
|
445
|
-
this.usedPorts.add(port);
|
|
446
|
-
if (await canBind(port))
|
|
447
|
-
return port;
|
|
448
|
-
this.usedPorts.delete(port);
|
|
449
|
-
}
|
|
450
|
-
throw new Error(`No free port for MCP spec "${spec.key}" in range ${range.base}-${range.max} ` + `(${this.registry.size} instances running)`);
|
|
451
|
-
}
|
|
452
|
-
readPortClaims() {
|
|
453
|
-
const claims = new Map;
|
|
454
|
-
let files;
|
|
455
|
-
try {
|
|
456
|
-
files = readdirSync(this.portsDir);
|
|
457
|
-
} catch {
|
|
458
|
-
return claims;
|
|
459
|
-
}
|
|
460
|
-
for (const file of files) {
|
|
461
|
-
if (file.includes(".tmp-"))
|
|
462
|
-
continue;
|
|
463
|
-
try {
|
|
464
|
-
const port = Number.parseInt(readFileSync2(join(this.portsDir, file), "utf8").trim(), 10);
|
|
465
|
-
if (!Number.isNaN(port))
|
|
466
|
-
claims.set(file, port);
|
|
467
|
-
} catch {}
|
|
468
|
-
}
|
|
469
|
-
return claims;
|
|
470
|
-
}
|
|
471
|
-
writePortFile(fileName, port) {
|
|
472
|
-
try {
|
|
473
|
-
mkdirSync2(this.portsDir, { recursive: true });
|
|
474
|
-
const file = join(this.portsDir, fileName);
|
|
475
|
-
const tmp = `${file}.tmp-${process.pid}`;
|
|
476
|
-
writeFileSync2(tmp, String(port));
|
|
477
|
-
renameSync2(tmp, file);
|
|
478
|
-
} catch (e) {
|
|
479
|
-
this.log("warn", "Failed to write MCP port file", { fileName, port, err: String(e) });
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
deletePortFile(fileName) {
|
|
483
|
-
try {
|
|
484
|
-
unlinkSync(join(this.portsDir, fileName));
|
|
485
|
-
} catch {}
|
|
486
|
-
}
|
|
487
|
-
cleanupRunning(rkey, inst) {
|
|
488
|
-
if (this.registry.get(rkey) === inst)
|
|
489
|
-
this.registry.delete(rkey);
|
|
490
|
-
this.usedPorts.delete(inst.port);
|
|
491
|
-
this.deletePortFile(inst.fileName);
|
|
492
|
-
}
|
|
493
|
-
async stopRunning(rkey, inst) {
|
|
494
|
-
const proc = inst.process;
|
|
495
|
-
proc.removeAllListeners("error");
|
|
496
|
-
proc.removeAllListeners("exit");
|
|
497
|
-
this.cleanupRunning(rkey, inst);
|
|
498
|
-
if (proc.exitCode !== null || proc.signalCode !== null)
|
|
499
|
-
return;
|
|
500
|
-
try {
|
|
501
|
-
proc.kill("SIGTERM");
|
|
502
|
-
} catch {}
|
|
503
|
-
const exited = await waitExit(proc, KILL_GRACE_MS);
|
|
504
|
-
if (!exited) {
|
|
505
|
-
if (proc.pid)
|
|
506
|
-
killProcessTree(proc.pid);
|
|
507
|
-
try {
|
|
508
|
-
proc.kill("SIGKILL");
|
|
509
|
-
} catch {}
|
|
510
|
-
await waitExit(proc, 1000);
|
|
511
|
-
}
|
|
512
|
-
this.log("info", "MCP server stopped", {
|
|
513
|
-
key: inst.key,
|
|
514
|
-
instanceKey: inst.instanceKey,
|
|
515
|
-
port: inst.port,
|
|
516
|
-
forced: !exited
|
|
517
|
-
});
|
|
518
|
-
}
|
|
519
|
-
async sweep() {
|
|
520
|
-
if (this.closed || this.sweepInFlight)
|
|
521
|
-
return;
|
|
522
|
-
this.sweepInFlight = true;
|
|
523
|
-
try {
|
|
524
|
-
const now = Date.now();
|
|
525
|
-
for (const [rkey, inst] of [...this.registry]) {
|
|
526
|
-
if (this.registry.get(rkey) !== inst)
|
|
527
|
-
continue;
|
|
528
|
-
const { idleEvictMs, healthIntervalMs } = inst.spec;
|
|
529
|
-
if (idleEvictMs && now - inst.lastUsedAt >= idleEvictMs) {
|
|
530
|
-
this.log("info", "Evicting idle MCP instance", {
|
|
531
|
-
key: inst.key,
|
|
532
|
-
instanceKey: inst.instanceKey,
|
|
533
|
-
idleMs: now - inst.lastUsedAt
|
|
534
|
-
});
|
|
535
|
-
await this.stopRunning(rkey, inst);
|
|
536
|
-
continue;
|
|
537
|
-
}
|
|
538
|
-
if (healthIntervalMs && now - inst.lastHealthAt >= healthIntervalMs) {
|
|
539
|
-
inst.lastHealthAt = now;
|
|
540
|
-
const healthy = await tcpProbe(inst.port);
|
|
541
|
-
if (healthy || this.registry.get(rkey) !== inst)
|
|
542
|
-
continue;
|
|
543
|
-
this.log("warn", "MCP health probe failed, restarting", {
|
|
544
|
-
key: inst.key,
|
|
545
|
-
instanceKey: inst.instanceKey,
|
|
546
|
-
port: inst.port
|
|
547
|
-
});
|
|
548
|
-
await this.stopRunning(rkey, inst);
|
|
549
|
-
try {
|
|
550
|
-
await this.ensure(inst.key, inst.instanceKey);
|
|
551
|
-
} catch (e) {
|
|
552
|
-
this.log("error", "Failed to restart unhealthy MCP instance", {
|
|
553
|
-
key: inst.key,
|
|
554
|
-
instanceKey: inst.instanceKey,
|
|
555
|
-
err: String(e)
|
|
556
|
-
});
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
} finally {
|
|
561
|
-
this.sweepInFlight = false;
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
export { mcpServerSpecSchema, McpManifest, McpHost };
|
|
566
|
-
|
|
567
|
-
//# debugId=A8A346BF3845A8CF64756E2164756E21
|