negotium 0.1.7 → 0.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/README.md +26 -2
- package/dist/canonical-mcp-bridge.js +14 -0
- package/dist/canonical-mcp-bridge.js.map +9 -0
- package/dist/chunk-10ct8814.js +429 -0
- package/dist/chunk-10ct8814.js.map +11 -0
- package/dist/chunk-18fzftjf.js +214 -0
- package/dist/chunk-18fzftjf.js.map +10 -0
- package/dist/chunk-1k7t7bnx.js +97 -0
- package/dist/chunk-1k7t7bnx.js.map +10 -0
- package/dist/chunk-1msje2zf.js +23 -0
- package/dist/chunk-1msje2zf.js.map +10 -0
- package/dist/chunk-2dgkah1z.js +706 -0
- package/dist/chunk-2dgkah1z.js.map +14 -0
- package/dist/chunk-3t4dtm26.js +61 -0
- package/dist/chunk-3t4dtm26.js.map +10 -0
- package/dist/chunk-3vn2vnj1.js +51 -0
- package/dist/chunk-3vn2vnj1.js.map +10 -0
- package/dist/chunk-6hknphtc.js +2054 -0
- package/dist/chunk-6hknphtc.js.map +29 -0
- package/dist/chunk-6tfzpcvk.js +2149 -0
- package/dist/chunk-6tfzpcvk.js.map +15 -0
- package/dist/chunk-8g0c4as0.js +23 -0
- package/dist/chunk-8g0c4as0.js.map +10 -0
- package/dist/chunk-9spx3h9n.js +279 -0
- package/dist/chunk-9spx3h9n.js.map +10 -0
- package/dist/chunk-c500xe9z.js +33 -0
- package/dist/chunk-c500xe9z.js.map +10 -0
- package/dist/chunk-e9b2xpff.js +147 -0
- package/dist/chunk-e9b2xpff.js.map +10 -0
- package/dist/chunk-edb07sjp.js +567 -0
- package/dist/chunk-edb07sjp.js.map +14 -0
- package/dist/chunk-eexqsee7.js +368 -0
- package/dist/chunk-eexqsee7.js.map +11 -0
- package/dist/chunk-efyxk97v.js +500 -0
- package/dist/chunk-efyxk97v.js.map +13 -0
- package/dist/chunk-epqxwkvh.js +3534 -0
- package/dist/chunk-epqxwkvh.js.map +20 -0
- package/dist/chunk-f1pzh3xh.js +115 -0
- package/dist/chunk-f1pzh3xh.js.map +10 -0
- package/dist/chunk-f5xgprg1.js +59 -0
- package/dist/chunk-f5xgprg1.js.map +10 -0
- package/dist/chunk-f97qybwk.js +584 -0
- package/dist/chunk-f97qybwk.js.map +11 -0
- package/dist/chunk-fvc7ncxd.js +252 -0
- package/dist/chunk-fvc7ncxd.js.map +10 -0
- package/dist/chunk-g7pvfj5e.js +574 -0
- package/dist/chunk-g7pvfj5e.js.map +14 -0
- package/dist/chunk-h8vmb9gf.js +315 -0
- package/dist/chunk-h8vmb9gf.js.map +10 -0
- package/dist/chunk-hgw7n8cm.js +43 -0
- package/dist/chunk-hgw7n8cm.js.map +10 -0
- package/dist/chunk-hsq7j9jk.js +126 -0
- package/dist/chunk-hsq7j9jk.js.map +10 -0
- package/dist/chunk-hwy3n31w.js +21 -0
- package/dist/chunk-hwy3n31w.js.map +9 -0
- package/dist/chunk-j0kjscb7.js +53 -0
- package/dist/chunk-j0kjscb7.js.map +10 -0
- package/dist/chunk-jzz91jwh.js +70 -0
- package/dist/chunk-jzz91jwh.js.map +10 -0
- package/dist/chunk-k9z47mab.js +247 -0
- package/dist/chunk-k9z47mab.js.map +12 -0
- package/dist/chunk-kbm35t7w.js +1331 -0
- package/dist/chunk-kbm35t7w.js.map +16 -0
- package/dist/chunk-m4veth0c.js +1649 -0
- package/dist/chunk-m4veth0c.js.map +17 -0
- package/dist/chunk-mhty3nna.js +150 -0
- package/dist/chunk-mhty3nna.js.map +11 -0
- package/dist/chunk-mmgh670x.js +248 -0
- package/dist/chunk-mmgh670x.js.map +11 -0
- package/dist/chunk-n0kk5xbq.js +2358 -0
- package/dist/chunk-n0kk5xbq.js.map +26 -0
- package/dist/chunk-nmv41qx9.js +2847 -0
- package/dist/chunk-nmv41qx9.js.map +23 -0
- package/dist/chunk-nqdp52aj.js +113 -0
- package/dist/chunk-nqdp52aj.js.map +12 -0
- package/dist/chunk-ns2m25dz.js +139 -0
- package/dist/chunk-ns2m25dz.js.map +10 -0
- package/dist/chunk-pkdmypdd.js +147 -0
- package/dist/chunk-pkdmypdd.js.map +11 -0
- package/dist/chunk-rmyfmrrr.js +1634 -0
- package/dist/chunk-rmyfmrrr.js.map +15 -0
- package/dist/chunk-sa874j5c.js +9 -0
- package/dist/chunk-sa874j5c.js.map +10 -0
- package/dist/chunk-sgsz2fpq.js +289 -0
- package/dist/chunk-sgsz2fpq.js.map +10 -0
- package/dist/chunk-sn51wnf1.js +3020 -0
- package/dist/chunk-sn51wnf1.js.map +32 -0
- package/dist/chunk-sqhr1a7e.js +587 -0
- package/dist/chunk-sqhr1a7e.js.map +11 -0
- package/dist/chunk-tmj5r2zv.js +2933 -0
- package/dist/chunk-tmj5r2zv.js.map +20 -0
- package/dist/chunk-vefj3880.js +56 -0
- package/dist/chunk-vefj3880.js.map +10 -0
- package/dist/chunk-vp2db42d.js +121 -0
- package/dist/chunk-vp2db42d.js.map +10 -0
- package/dist/chunk-wry79q55.js +108 -0
- package/dist/chunk-wry79q55.js.map +10 -0
- package/dist/chunk-x1arr7ze.js +63 -0
- package/dist/chunk-x1arr7ze.js.map +10 -0
- package/dist/chunk-z2xy5n2q.js +103 -0
- package/dist/chunk-z2xy5n2q.js.map +10 -0
- package/dist/hosted-agent.js +53 -0
- package/dist/hosted-agent.js.map +10 -0
- package/dist/main.js +132 -20
- package/dist/main.js.map +4 -4
- package/dist/runtime/scripts/faster-whisper-wrapper.py +87 -0
- package/dist/runtime/scripts/mcp-patchright-http.mjs +349 -0
- package/dist/runtime/src/agents/api-topic-agent-switch.ts +178 -0
- package/dist/runtime/src/agents/archiver.ts +396 -0
- package/dist/runtime/src/agents/auth-check.ts +64 -0
- package/dist/runtime/src/agents/claude-provider.ts +737 -0
- package/dist/runtime/src/agents/claude-registry.ts +115 -0
- package/dist/runtime/src/agents/codex-native-multi-agent.ts +177 -0
- package/dist/runtime/src/agents/codex-provider.ts +785 -0
- package/dist/runtime/src/agents/codex-registry.ts +115 -0
- package/dist/runtime/src/agents/codex-tree-kill.ts +365 -0
- package/dist/runtime/src/agents/contracts.ts +69 -0
- package/dist/runtime/src/agents/deep-map.ts +13 -0
- package/dist/runtime/src/agents/execution-host.ts +91 -0
- package/dist/runtime/src/agents/fixtures/claude-attachments.jsonl +2 -0
- package/dist/runtime/src/agents/fixtures/codex-shell.jsonl +5 -0
- package/dist/runtime/src/agents/fork.ts +65 -0
- package/dist/runtime/src/agents/hosted-agent.ts +49 -0
- package/dist/runtime/src/agents/idle-archiver.ts +128 -0
- package/dist/runtime/src/agents/index.ts +205 -0
- package/dist/runtime/src/agents/maestro-provider.ts +206 -0
- package/dist/runtime/src/agents/maestro-registry.ts +31 -0
- package/dist/runtime/src/agents/mcp-tools/ask-user.ts +280 -0
- package/dist/runtime/src/agents/mcp-tools/common.ts +21 -0
- package/dist/runtime/src/agents/mcp-tools/self-config.ts +220 -0
- package/dist/runtime/src/agents/mcp-tools/spawn-subagent.ts +525 -0
- package/dist/runtime/src/agents/mcp-tools/visuals.ts +88 -0
- package/dist/runtime/src/agents/model-catalog.ts +151 -0
- package/dist/runtime/src/agents/registry.ts +24 -0
- package/dist/runtime/src/agents/rollout/claude.ts +331 -0
- package/dist/runtime/src/agents/rollout/codex.ts +569 -0
- package/dist/runtime/src/agents/rollout/shared.ts +205 -0
- package/dist/runtime/src/agents/self-config-core.ts +415 -0
- package/dist/runtime/src/agents/task-events.ts +44 -0
- package/dist/runtime/src/agents/tool-format.ts +352 -0
- package/dist/runtime/src/agents/topic-agent-switch.ts +193 -0
- package/dist/runtime/src/agents/topic-cleanup.ts +231 -0
- package/dist/runtime/src/agents/vault-tool-policy.ts +33 -0
- package/dist/runtime/src/application/execute-external-user-turn.ts +23 -0
- package/dist/runtime/src/application/submit-user-message.ts +50 -0
- package/dist/runtime/src/application/switch-topic-effort.ts +48 -0
- package/dist/runtime/src/application/switch-topic-model.ts +79 -0
- package/dist/runtime/src/application/topic-service.ts +139 -0
- package/dist/runtime/src/application/vault-command.ts +170 -0
- package/dist/runtime/src/bus.ts +319 -0
- package/dist/runtime/src/index.ts +333 -0
- package/dist/runtime/src/mcp/agent-health-server.ts +309 -0
- package/dist/runtime/src/mcp/background-bash-server.ts +346 -0
- package/dist/runtime/src/mcp/canonical-bridge-config.ts +84 -0
- package/dist/runtime/src/mcp/canonical-proxy-server.ts +171 -0
- package/dist/runtime/src/mcp/dm-ipc.ts +60 -0
- package/dist/runtime/src/mcp/mcp-helpers.ts +40 -0
- package/dist/runtime/src/mcp/paddleocr-server.ts +105 -0
- package/dist/runtime/src/mcp/peer-bridge.ts +114 -0
- package/dist/runtime/src/mcp/runtime-spec.ts +137 -0
- package/dist/runtime/src/mcp/session-comm/bridge-ipc-config.ts +27 -0
- package/dist/runtime/src/mcp/session-comm/peer-forward.ts +165 -0
- package/dist/runtime/src/mcp/session-comm/runtime.ts +115 -0
- package/dist/runtime/src/mcp/session-comm/server.ts +649 -0
- package/dist/runtime/src/mcp/session-comm/topic-config.ts +131 -0
- package/dist/runtime/src/mcp/session-comm/topics.ts +229 -0
- package/dist/runtime/src/mcp/stdio-protect.ts +84 -0
- package/dist/runtime/src/mcp/system-health-server.ts +130 -0
- package/dist/runtime/src/mcp/task-server.ts +173 -0
- package/dist/runtime/src/mcp/token-stats-server.ts +98 -0
- package/dist/runtime/src/mcp/vault-http.ts +219 -0
- package/dist/runtime/src/mcp/vault-run.ts +153 -0
- package/dist/runtime/src/mcp/vault-server.ts +104 -0
- package/dist/runtime/src/mcp/wiki-server.ts +755 -0
- package/dist/runtime/src/media/file-events.ts +33 -0
- package/dist/runtime/src/media/text-extractor.ts +312 -0
- package/dist/runtime/src/media/video.ts +92 -0
- package/dist/runtime/src/outbox/file-ops.ts +210 -0
- package/dist/runtime/src/outbox/index.ts +8 -0
- package/dist/runtime/src/outbox/utils.ts +51 -0
- package/dist/runtime/src/platform/background-bash/manager.ts +279 -0
- package/dist/runtime/src/platform/config.ts +371 -0
- package/dist/runtime/src/platform/constants.ts +18 -0
- package/dist/runtime/src/platform/debug-users.ts +14 -0
- package/dist/runtime/src/platform/delay.ts +1 -0
- package/dist/runtime/src/platform/error.ts +9 -0
- package/dist/runtime/src/platform/file-utils.ts +16 -0
- package/dist/runtime/src/platform/jsonl.ts +202 -0
- package/dist/runtime/src/platform/lifecycle.ts +174 -0
- package/dist/runtime/src/platform/logger.ts +31 -0
- package/dist/runtime/src/platform/maestro-bootstrap-env.ts +3 -0
- package/dist/runtime/src/platform/mcp-config.ts +878 -0
- package/dist/runtime/src/platform/modules.ts +145 -0
- package/dist/runtime/src/platform/node-plugins.ts +46 -0
- package/dist/runtime/src/platform/playwright/manager.ts +1233 -0
- package/dist/runtime/src/prompts/agents/wiki-archiver.md +261 -0
- package/dist/runtime/src/prompts/builders.ts +368 -0
- package/dist/runtime/src/prompts/sessions/channel-system.md +48 -0
- package/dist/runtime/src/prompts/sessions/manager-system.md +37 -0
- package/dist/runtime/src/prompts/sessions/topic-system.md +47 -0
- package/dist/runtime/src/prompts/sessions/visual-design.md +86 -0
- package/dist/runtime/src/query/active-rooms.ts +534 -0
- package/dist/runtime/src/query/control.ts +313 -0
- package/dist/runtime/src/query/index.ts +24 -0
- package/dist/runtime/src/query/session-inbox-cleanup.ts +44 -0
- package/dist/runtime/src/query/session-inbox-path.ts +42 -0
- package/dist/runtime/src/query/state.ts +94 -0
- package/dist/runtime/src/query/types.ts +83 -0
- package/dist/runtime/src/runtime/ask-callbacks.ts +153 -0
- package/dist/runtime/src/runtime/attachments.ts +151 -0
- package/dist/runtime/src/runtime/background-sessions.ts +83 -0
- package/dist/runtime/src/runtime/channel-context.ts +107 -0
- package/dist/runtime/src/runtime/errors.ts +69 -0
- package/dist/runtime/src/runtime/event-heartbeat.ts +77 -0
- package/dist/runtime/src/runtime/file-hooks.ts +81 -0
- package/dist/runtime/src/runtime/footer.ts +23 -0
- package/dist/runtime/src/runtime/inbox.ts +946 -0
- package/dist/runtime/src/runtime/tasks.ts +56 -0
- package/dist/runtime/src/runtime/topic-config.ts +29 -0
- package/dist/runtime/src/runtime/turn-runner.ts +2298 -0
- package/dist/runtime/src/runtime/usage-alert.ts +84 -0
- package/dist/runtime/src/runtime/visual-html.ts +109 -0
- package/dist/runtime/src/runtime/visual-store.ts +430 -0
- package/dist/runtime/src/runtime/visuals.ts +171 -0
- package/dist/runtime/src/security/sanitize.ts +27 -0
- package/dist/runtime/src/security/sensitive-path.ts +34 -0
- package/dist/runtime/src/storage/activity-log.ts +152 -0
- package/dist/runtime/src/storage/api-messages.ts +617 -0
- package/dist/runtime/src/storage/api-topic-brief.ts +127 -0
- package/dist/runtime/src/storage/api-topic-config.ts +192 -0
- package/dist/runtime/src/storage/api-topics.ts +844 -0
- package/dist/runtime/src/storage/app-settings.ts +44 -0
- package/dist/runtime/src/storage/browser-profiles.ts +133 -0
- package/dist/runtime/src/storage/conversations.ts +268 -0
- package/dist/runtime/src/storage/forum/agent-settings.ts +20 -0
- package/dist/runtime/src/storage/forum/index.ts +4 -0
- package/dist/runtime/src/storage/forum/repository.ts +283 -0
- package/dist/runtime/src/storage/forum/schema.ts +300 -0
- package/dist/runtime/src/storage/forum-db.ts +23 -0
- package/dist/runtime/src/storage/runtime-events.ts +130 -0
- package/dist/runtime/src/storage/runtime-leases.ts +189 -0
- package/dist/runtime/src/storage/runtime-process-leases.ts +216 -0
- package/dist/runtime/src/storage/runtime-topic-state.ts +148 -0
- package/dist/runtime/src/storage/runtime-turn-requests.ts +301 -0
- package/dist/runtime/src/storage/self-schedules.ts +298 -0
- package/dist/runtime/src/storage/session-asks.ts +347 -0
- package/dist/runtime/src/storage/sqlite.ts +129 -0
- package/dist/runtime/src/storage/tasks.ts +179 -0
- package/dist/runtime/src/storage/token-stats.ts +144 -0
- package/dist/runtime/src/storage/topic-archive-state.ts +59 -0
- package/dist/runtime/src/storage/topic-archive.ts +74 -0
- package/dist/runtime/src/storage/topic-settings.ts +67 -0
- package/dist/runtime/src/storage/topic-transcript.ts +98 -0
- package/dist/runtime/src/storage/vault-crypto.ts +65 -0
- package/dist/runtime/src/storage/vault.ts +225 -0
- package/dist/runtime/src/storage/wiki-summary-names.ts +33 -0
- package/dist/runtime/src/storage/wiki.ts +85 -0
- package/dist/runtime/src/topics/create.ts +123 -0
- package/dist/runtime/src/topics/derive.ts +398 -0
- package/dist/runtime/src/topics/lifecycle.ts +311 -0
- package/dist/runtime/src/topics/links.ts +7 -0
- package/dist/runtime/src/topics/personal-general.ts +65 -0
- package/dist/runtime/src/topics/session.ts +381 -0
- package/dist/runtime/src/types/api.ts +497 -0
- package/dist/runtime/src/types.ts +257 -0
- package/dist/runtime/src/version.ts +1 -0
- package/dist/runtime/tsconfig.json +13 -0
- package/package.json +28 -3
- package/dist/main.d.ts +0 -3
- package/dist/main.d.ts.map +0 -1
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { ACTIVE_QUERY_STALE_MS, USERS_LOG_DIR } from "#platform/config";
|
|
7
|
+
import { errMsg } from "#platform/error";
|
|
8
|
+
import { appendJsonlEntry, readJsonFile } from "#platform/jsonl";
|
|
9
|
+
import {
|
|
10
|
+
formatMcpStatus,
|
|
11
|
+
OPTIONAL_FORUM_MCP_SERVERS,
|
|
12
|
+
REQUIRED_FORUM_MCP_SERVERS,
|
|
13
|
+
} from "#platform/mcp-config";
|
|
14
|
+
import { closeBrowserOwnerTabs } from "#platform/playwright/manager";
|
|
15
|
+
import { getTopic } from "#storage/api-topics";
|
|
16
|
+
import {
|
|
17
|
+
assignTopicBrowserProfile,
|
|
18
|
+
getBrowserProfileOwner,
|
|
19
|
+
getTopicBrowserProfile,
|
|
20
|
+
isTopicBrowserProfileOwner,
|
|
21
|
+
listBrowserProfiles,
|
|
22
|
+
} from "#storage/browser-profiles";
|
|
23
|
+
import {
|
|
24
|
+
clearPendingAsk,
|
|
25
|
+
createPendingAsk,
|
|
26
|
+
describePendingAskState,
|
|
27
|
+
listPendingAsksForCaller,
|
|
28
|
+
} from "#storage/session-asks";
|
|
29
|
+
import { connectStdio, mcpError, mcpOk } from "../mcp-helpers";
|
|
30
|
+
import { forwardToPeer, peerSessionsForUser } from "./peer-forward";
|
|
31
|
+
import {
|
|
32
|
+
currentDepth,
|
|
33
|
+
currentTopic,
|
|
34
|
+
currentTopicId,
|
|
35
|
+
isManagerTopic,
|
|
36
|
+
isReplyOnly,
|
|
37
|
+
MAX_MESSAGE_LENGTH,
|
|
38
|
+
MAX_TELL_DEPTH,
|
|
39
|
+
peerHostQueryId,
|
|
40
|
+
userId,
|
|
41
|
+
} from "./runtime";
|
|
42
|
+
import {
|
|
43
|
+
currentApiTopicId,
|
|
44
|
+
getMcpConfig,
|
|
45
|
+
setCurrentTopicDescription,
|
|
46
|
+
setMcpConfig,
|
|
47
|
+
} from "./topic-config";
|
|
48
|
+
import { buildInboxPath, getTopicsForUser, type QueryState, validateTarget } from "./topics";
|
|
49
|
+
|
|
50
|
+
/** Addressing: "<nodeName>/<topicTitle>". Node names never contain "/", so
|
|
51
|
+
* the first slash splits reliably (kept for future cross-node adapters). */
|
|
52
|
+
interface PeerTarget {
|
|
53
|
+
node: string;
|
|
54
|
+
topic: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A target is remote when it is not a local topic name AND parses as
|
|
58
|
+
* "node/topic". Local titles containing "/" keep working — the local map is
|
|
59
|
+
* checked first. */
|
|
60
|
+
function remotePeerTarget(to: string): PeerTarget | null {
|
|
61
|
+
// Local titles always win; placement routing is adapter-owned (an otium
|
|
62
|
+
// worker keeps its own placement store), so core never remote-routes a
|
|
63
|
+
// local topic.
|
|
64
|
+
const local = getTopicsForUser()[to];
|
|
65
|
+
if (local?.topicId) return null;
|
|
66
|
+
const slash = to.indexOf("/");
|
|
67
|
+
if (slash <= 0 || slash === to.length - 1) return null;
|
|
68
|
+
return { node: to.slice(0, slash), topic: to.slice(slash + 1) };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// --- MCP Server ---
|
|
72
|
+
|
|
73
|
+
const server = new McpServer({
|
|
74
|
+
name: "session-comm",
|
|
75
|
+
version: "1.0.0",
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
function queryStateFileName(topicName: string): string {
|
|
79
|
+
return `${topicName}.json`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readQueryState(activeQueriesDir: string, topicName: string): QueryState | null {
|
|
83
|
+
return readJsonFile<QueryState>(join(activeQueriesDir, queryStateFileName(topicName)));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function currentTopicRef(): { key: string; title: string; topicId?: string } {
|
|
87
|
+
if (currentTopicId) {
|
|
88
|
+
try {
|
|
89
|
+
const topic = getTopic(currentTopicId);
|
|
90
|
+
if (topic) {
|
|
91
|
+
return {
|
|
92
|
+
key: `${topic.kind ?? "channel"}:${topic.title}`,
|
|
93
|
+
title: topic.title,
|
|
94
|
+
topicId: topic.id,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
// Fall back to the CLI title below.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
key: currentTopic,
|
|
103
|
+
title: currentTopic,
|
|
104
|
+
...(currentTopicId ? { topicId: currentTopicId } : {}),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- always available ---
|
|
109
|
+
|
|
110
|
+
server.tool(
|
|
111
|
+
"list_sessions",
|
|
112
|
+
"List all available Claude sessions (forum topics) for inter-session communication. Topics without an active session are still valid targets — ask_session / tell_session will wake them with a fresh session on first delivery.",
|
|
113
|
+
{},
|
|
114
|
+
async () => {
|
|
115
|
+
const topics = getTopicsForUser();
|
|
116
|
+
const entries = Object.entries(topics)
|
|
117
|
+
.filter(([name, topic]) => name !== currentTopic && Boolean(topic.agent))
|
|
118
|
+
.map(([name, t]) => {
|
|
119
|
+
const status = !t.agent
|
|
120
|
+
? "no AI invited"
|
|
121
|
+
: t.sessionId
|
|
122
|
+
? `active (${t.sessionId.slice(0, 8)})`
|
|
123
|
+
: "fresh-start ready";
|
|
124
|
+
const desc = t.description
|
|
125
|
+
? `\n description: ${t.description.slice(0, 80)}${t.description.length > 80 ? "..." : ""}`
|
|
126
|
+
: "";
|
|
127
|
+
return `- ${name}: ${status}${desc}`;
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Multi-node: append each peer node's rooms, addressed as "node/topic".
|
|
131
|
+
// Disabled or unattached nodes answer with an error — skip silently so
|
|
132
|
+
// single-node behavior is untouched.
|
|
133
|
+
const remoteSections: string[] = [];
|
|
134
|
+
const peers = await peerSessionsForUser(userId, peerHostQueryId || undefined);
|
|
135
|
+
if (peers.ok && peers.nodes) {
|
|
136
|
+
for (const node of peers.nodes) {
|
|
137
|
+
if (!node.node) continue;
|
|
138
|
+
if (node.error) {
|
|
139
|
+
remoteSections.push(`\nNode ${node.node}: (unreachable: ${node.error})`);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const lines = (node.sessions ?? [])
|
|
143
|
+
.filter((session) => Boolean(session.agent))
|
|
144
|
+
.map((s) => {
|
|
145
|
+
const status = s.hasSession ? "active" : "fresh-start ready";
|
|
146
|
+
const desc = s.description ? ` — ${s.description.slice(0, 60)}` : "";
|
|
147
|
+
return `- ${node.node}/${s.name}: ${status}${desc}`;
|
|
148
|
+
});
|
|
149
|
+
remoteSections.push(
|
|
150
|
+
`\nNode ${node.node}:\n${lines.join("\n") || " (no rooms for this user)"}`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (entries.length === 0 && remoteSections.length === 0) {
|
|
156
|
+
return mcpOk("No other sessions available.");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
content: [
|
|
161
|
+
{
|
|
162
|
+
type: "text" as const,
|
|
163
|
+
text:
|
|
164
|
+
`Current session: ${currentTopic}\nTell depth: ${currentDepth}/${MAX_TELL_DEPTH}\n\n` +
|
|
165
|
+
`Available sessions:\n${entries.join("\n")}${remoteSections.join("\n")}`,
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
};
|
|
169
|
+
},
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
server.tool(
|
|
173
|
+
"configure_mcp",
|
|
174
|
+
`Configure MCP servers for the current topic. Changes take effect on the next user message automatically (no restart needed).\n` +
|
|
175
|
+
`- enabled: optional servers to whitelist on top of required ones (null or [] = required-only). Required servers are always active regardless: ${REQUIRED_FORUM_MCP_SERVERS.join(", ")}\n` +
|
|
176
|
+
`- Optional servers you can toggle: ${OPTIONAL_FORUM_MCP_SERVERS.join(", ")}.`,
|
|
177
|
+
{
|
|
178
|
+
enabled: z
|
|
179
|
+
.array(z.string())
|
|
180
|
+
.nullable()
|
|
181
|
+
.optional()
|
|
182
|
+
.describe(
|
|
183
|
+
`Optional servers to enable on top of required ones. null or [] = required-only, or a subset of: ${OPTIONAL_FORUM_MCP_SERVERS.join(", ")}`,
|
|
184
|
+
),
|
|
185
|
+
},
|
|
186
|
+
async ({ enabled }) => {
|
|
187
|
+
if (!currentTopic || !userId) {
|
|
188
|
+
return mcpError("Error: No current topic.");
|
|
189
|
+
}
|
|
190
|
+
if (isManagerTopic) {
|
|
191
|
+
return mcpError(
|
|
192
|
+
"Error: General is a manager room and does not use per-topic MCP whitelists. Use topic-admin set_topic_mcp for a target topic.",
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
setMcpConfig(enabled);
|
|
197
|
+
const current = getMcpConfig();
|
|
198
|
+
const lines = [
|
|
199
|
+
`MCP 설정 저장됨 (다음 사용자 메시지부터 자동 적용, 재시작 불필요)`,
|
|
200
|
+
``,
|
|
201
|
+
...formatMcpStatus(current),
|
|
202
|
+
];
|
|
203
|
+
return mcpOk(lines.join("\n"));
|
|
204
|
+
} catch (err) {
|
|
205
|
+
return mcpError(`Error: ${errMsg(err)}`);
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
server.tool(
|
|
211
|
+
"get_mcp_config",
|
|
212
|
+
"Get the current MCP server configuration for this topic. Required servers are always active regardless of whitelist settings.",
|
|
213
|
+
{},
|
|
214
|
+
async () => {
|
|
215
|
+
if (isManagerTopic) {
|
|
216
|
+
return mcpOk(
|
|
217
|
+
"General is a manager room. It uses the manager MCP bundle directly; per-topic MCP whitelist settings do not apply. Use topic-admin get_topic_mcp for a target topic.",
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
const config = getMcpConfig();
|
|
221
|
+
const lines = [`현재 토픽: ${currentTopic}`, ``, ...formatMcpStatus(config)];
|
|
222
|
+
return mcpOk(lines.join("\n"));
|
|
223
|
+
},
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
server.tool(
|
|
227
|
+
"get_browser_profile",
|
|
228
|
+
"Get this topic's browser profile and the other owned topics assigned to each profile.",
|
|
229
|
+
{},
|
|
230
|
+
async () => {
|
|
231
|
+
const topicId = currentApiTopicId();
|
|
232
|
+
if (!topicId || !userId) return mcpError("Error: No current API topic.");
|
|
233
|
+
if (!isTopicBrowserProfileOwner(topicId, userId)) {
|
|
234
|
+
return mcpError("Error: Only the topic owner can inspect its browser profiles.");
|
|
235
|
+
}
|
|
236
|
+
const ownerId = getBrowserProfileOwner(topicId, userId);
|
|
237
|
+
return mcpOk(
|
|
238
|
+
JSON.stringify(
|
|
239
|
+
{
|
|
240
|
+
current: getTopicBrowserProfile(topicId),
|
|
241
|
+
profiles: listBrowserProfiles(ownerId),
|
|
242
|
+
},
|
|
243
|
+
null,
|
|
244
|
+
2,
|
|
245
|
+
),
|
|
246
|
+
);
|
|
247
|
+
},
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
server.tool(
|
|
251
|
+
"set_browser_profile",
|
|
252
|
+
"Assign this topic to a named shared browser profile. Topics with the same owner and profile share login state but keep owner-isolated tabs. Takes effect next turn.",
|
|
253
|
+
{
|
|
254
|
+
profile: z
|
|
255
|
+
.string()
|
|
256
|
+
.describe("1-48 lowercase letters, numbers, '-' or '_'; use default for the default profile"),
|
|
257
|
+
},
|
|
258
|
+
async ({ profile }) => {
|
|
259
|
+
const topicId = currentApiTopicId();
|
|
260
|
+
if (!topicId || !userId) return mcpError("Error: No current API topic.");
|
|
261
|
+
try {
|
|
262
|
+
if (!isTopicBrowserProfileOwner(topicId, userId)) {
|
|
263
|
+
return mcpError("Error: Only the topic owner can change its browser profile.");
|
|
264
|
+
}
|
|
265
|
+
const result = assignTopicBrowserProfile({ topicId, actorUserId: userId, profile });
|
|
266
|
+
let cleanupWarning = "";
|
|
267
|
+
if (result.previous !== result.profile) {
|
|
268
|
+
try {
|
|
269
|
+
await closeBrowserOwnerTabs(userId, result.previous, `topic:${topicId}`);
|
|
270
|
+
} catch (err) {
|
|
271
|
+
cleanupWarning = ` Previous-profile tab cleanup failed: ${errMsg(err)}`;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return mcpOk(
|
|
275
|
+
`Browser profile changed: ${result.previous} -> ${result.profile}. It takes effect next turn.${cleanupWarning}`,
|
|
276
|
+
);
|
|
277
|
+
} catch (err) {
|
|
278
|
+
return mcpError(`Error: ${errMsg(err)}`);
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
// --- always-available session inspection / self-config ---
|
|
284
|
+
|
|
285
|
+
server.tool(
|
|
286
|
+
"peek_session",
|
|
287
|
+
"Check which sessions are currently running a query (busy) vs idle. Useful before abort_session.",
|
|
288
|
+
{},
|
|
289
|
+
async () => {
|
|
290
|
+
const topics = getTopicsForUser();
|
|
291
|
+
const topicNames = Object.keys(topics);
|
|
292
|
+
const activeQueriesDir = join(USERS_LOG_DIR, userId, "active-queries");
|
|
293
|
+
|
|
294
|
+
// Read own query state for consistent display
|
|
295
|
+
let selfLabel = `${currentTopic} (자신 — 실행 중)`;
|
|
296
|
+
const selfState = readQueryState(activeQueriesDir, currentTopic);
|
|
297
|
+
if (selfState) {
|
|
298
|
+
const selfElapsed = Date.now() - new Date(selfState.since).getTime();
|
|
299
|
+
const selfMins = Math.floor(selfElapsed / 60000);
|
|
300
|
+
const selfSecs = Math.floor((selfElapsed % 60000) / 1000);
|
|
301
|
+
const selfTimeStr = selfMins > 0 ? `${selfMins}분 ${selfSecs}초` : `${selfSecs}초`;
|
|
302
|
+
const selfTaskStr = selfState.task ? ` | ${selfState.task}` : "";
|
|
303
|
+
selfLabel = `${currentTopic} (자신 — ${selfTimeStr}${selfTaskStr})`;
|
|
304
|
+
}
|
|
305
|
+
const running: string[] = [selfLabel];
|
|
306
|
+
const idle: string[] = [];
|
|
307
|
+
|
|
308
|
+
for (const name of topicNames) {
|
|
309
|
+
if (name === currentTopic) continue;
|
|
310
|
+
let isRunning = false;
|
|
311
|
+
const state = readQueryState(activeQueriesDir, name);
|
|
312
|
+
if (state) {
|
|
313
|
+
const elapsed = Date.now() - new Date(state.since).getTime();
|
|
314
|
+
if (elapsed <= ACTIVE_QUERY_STALE_MS) {
|
|
315
|
+
const mins = Math.floor(elapsed / 60000);
|
|
316
|
+
const secs = Math.floor((elapsed % 60000) / 1000);
|
|
317
|
+
const timeStr = mins > 0 ? `${mins}분 ${secs}초` : `${secs}초`;
|
|
318
|
+
const taskStr = state.task ? ` | ${state.task}` : "";
|
|
319
|
+
running.push(`${name} (${timeStr}${taskStr})`);
|
|
320
|
+
isRunning = true;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (!isRunning) idle.push(name);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const runningHeader = `실행 중 (${running.length}):\n${running.map((r) => ` ${r}`).join("\n")}`;
|
|
327
|
+
const pendingAsks = listPendingAsksForCaller({
|
|
328
|
+
userId,
|
|
329
|
+
from: currentTopicRef().key,
|
|
330
|
+
});
|
|
331
|
+
const lines = [
|
|
332
|
+
`현재 세션 상태`,
|
|
333
|
+
``,
|
|
334
|
+
runningHeader,
|
|
335
|
+
`유휴 (${idle.length}): ${idle.join(", ") || "없음"}`,
|
|
336
|
+
];
|
|
337
|
+
if (pendingAsks.length) {
|
|
338
|
+
lines.push(
|
|
339
|
+
``,
|
|
340
|
+
`내 ask_session 대기:`,
|
|
341
|
+
...pendingAsks.map(
|
|
342
|
+
(ask) =>
|
|
343
|
+
` ${ask.to}: ${describePendingAskState(ask.state)} (request_id: ${ask.requestId})`,
|
|
344
|
+
),
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
return mcpOk(lines.join("\n"));
|
|
348
|
+
},
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
server.tool(
|
|
352
|
+
"set_description",
|
|
353
|
+
"Set a description for the current session. Acts as a system prompt addition and routing hint for other sessions using list_sessions. Call this once at session start based on the topic's CLAUDE.md.",
|
|
354
|
+
{
|
|
355
|
+
description: z
|
|
356
|
+
.string()
|
|
357
|
+
.describe(
|
|
358
|
+
"What this session specializes in (e.g. 'UE5 graphics development, shader optimization')",
|
|
359
|
+
),
|
|
360
|
+
},
|
|
361
|
+
async ({ description }) => {
|
|
362
|
+
try {
|
|
363
|
+
setCurrentTopicDescription(description);
|
|
364
|
+
return mcpOk(`Description set for "${currentTopic}".`);
|
|
365
|
+
} catch (err) {
|
|
366
|
+
return mcpError(`Error: ${errMsg(err)}`);
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
// --- outbound session-to-session tools ---
|
|
372
|
+
// Suppressed when running as a silent fork that exists only to generate an
|
|
373
|
+
// ask_session reply — such a fork has no reason to initiate further calls.
|
|
374
|
+
|
|
375
|
+
if (!isReplyOnly) {
|
|
376
|
+
server.tool(
|
|
377
|
+
"ask_session",
|
|
378
|
+
"ASK — Delegate to another session and pull the result back INTO YOUR CONTEXT. Target forks (no history pollution), processes with full tools, and the answer is auto-injected into your conversation. Use ONLY when YOU need the output to drive your next action (code reviews whose verdict determines your next edit, fact checks you'll cite, lookups that decide your next step). If the user can just read the result in the target topic, use tell_session — ask burns your context window with content you don't actually need. Decision rule: 'Do I need this output in MY context to proceed?' Yes → ask. No (result lives in target topic, user reads it there) → tell_session.",
|
|
379
|
+
{
|
|
380
|
+
to: z.string().describe("Target session/topic name (e.g. '회의록', '신건')"),
|
|
381
|
+
message: z.string().describe("Message to send to the target session"),
|
|
382
|
+
},
|
|
383
|
+
async ({ to, message }) => {
|
|
384
|
+
if (message.length > MAX_MESSAGE_LENGTH) {
|
|
385
|
+
return mcpError(
|
|
386
|
+
`Error: message too long (${message.length} chars, max ${MAX_MESSAGE_LENGTH})`,
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Remote target ("node/topic") — hand to the runtime's peer forwarder.
|
|
391
|
+
const remote = remotePeerTarget(to);
|
|
392
|
+
if (remote) {
|
|
393
|
+
const fromRef = currentTopicRef();
|
|
394
|
+
if (!fromRef.topicId) {
|
|
395
|
+
return mcpError(
|
|
396
|
+
"Error: 이 세션은 원격 ask_session을 사용할 수 없습니다 (topic id 없음).",
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
const requestId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
400
|
+
const pending = createPendingAsk({ userId, from: fromRef.key, to, requestId });
|
|
401
|
+
if (!pending.ok) {
|
|
402
|
+
const detail = pending.existing
|
|
403
|
+
? `${describePendingAskState(pending.existing.state)} (request_id: ${pending.existing.requestId})`
|
|
404
|
+
: "상태 파일 확인 중";
|
|
405
|
+
return mcpError(
|
|
406
|
+
`"${to}"에 이미 진행 중인 ask_session 요청이 있습니다: ${detail}. 응답이 이 세션에 자동으로 돌아올 때까지 기다리세요.`,
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
const result = await forwardToPeer({
|
|
410
|
+
action: "ask",
|
|
411
|
+
toNode: remote.node,
|
|
412
|
+
toTopic: remote.topic,
|
|
413
|
+
userId,
|
|
414
|
+
fromKey: fromRef.key,
|
|
415
|
+
fromTitle: fromRef.title,
|
|
416
|
+
fromTopicId: fromRef.topicId,
|
|
417
|
+
message,
|
|
418
|
+
requestId,
|
|
419
|
+
fromDepth: currentDepth,
|
|
420
|
+
...(peerHostQueryId ? { sourceQueryId: peerHostQueryId } : {}),
|
|
421
|
+
});
|
|
422
|
+
if (!result.ok) {
|
|
423
|
+
clearPendingAsk({ userId, from: fromRef.key, to, requestId });
|
|
424
|
+
return mcpError(`Error: "${to}" 원격 세션에 전송 실패: ${result.error}`);
|
|
425
|
+
}
|
|
426
|
+
return mcpOk(
|
|
427
|
+
`"${to}" 세션(노드 ${remote.node})에 참조 요청을 보냈습니다.\n\nrequest_id: ${requestId}\n\n응답은 '[Reply from ${remote.node}/${remote.topic}]' 형식으로 이 세션에 자동으로 돌아옵니다. 응답이 도착할 때까지 같은 요청으로 ask_session을 재호출하지 마세요.`,
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const validation = validateTarget(to);
|
|
432
|
+
if (!validation.ok) return validation.error;
|
|
433
|
+
if (!validation.target.agent) {
|
|
434
|
+
return mcpError(
|
|
435
|
+
`Error: "${to}" 토픽에는 AI가 초대되어 있지 않아 ask_session을 실행할 수 없습니다.`,
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const fromRef = currentTopicRef();
|
|
440
|
+
const requestId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
441
|
+
const pending = createPendingAsk({
|
|
442
|
+
userId,
|
|
443
|
+
from: fromRef.key,
|
|
444
|
+
to,
|
|
445
|
+
requestId,
|
|
446
|
+
});
|
|
447
|
+
if (!pending.ok) {
|
|
448
|
+
const detail = pending.existing
|
|
449
|
+
? `${describePendingAskState(pending.existing.state)} (request_id: ${pending.existing.requestId})`
|
|
450
|
+
: "상태 파일 확인 중";
|
|
451
|
+
return mcpError(
|
|
452
|
+
`"${to}"에 이미 진행 중인 ask_session 요청이 있습니다: ${detail}. 응답이 이 세션에 자동으로 돌아올 때까지 기다리세요.`,
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
const targetTopicId = validation.target.topicId;
|
|
458
|
+
if (!targetTopicId) {
|
|
459
|
+
clearPendingAsk({ userId, from: fromRef.key, to, requestId });
|
|
460
|
+
return mcpError(`Error: "${to}" 세션의 토픽 ID를 찾을 수 없습니다.`);
|
|
461
|
+
}
|
|
462
|
+
const inboxFile = buildInboxPath(targetTopicId);
|
|
463
|
+
const entry = {
|
|
464
|
+
type: "ask" as const,
|
|
465
|
+
requestId,
|
|
466
|
+
from: fromRef.key,
|
|
467
|
+
fromTitle: fromRef.title,
|
|
468
|
+
...(fromRef.topicId ? { fromTopicId: fromRef.topicId } : {}),
|
|
469
|
+
message,
|
|
470
|
+
// Caller's depth — used to resume this session at the correct depth
|
|
471
|
+
// when the fork's reply is injected back.
|
|
472
|
+
fromDepth: currentDepth,
|
|
473
|
+
timestamp: new Date().toISOString(),
|
|
474
|
+
};
|
|
475
|
+
appendJsonlEntry(inboxFile, entry);
|
|
476
|
+
process.stderr.write(
|
|
477
|
+
`[session-comm] ask_session: ${fromRef.key} → ${to} requestId=${requestId}\n`,
|
|
478
|
+
);
|
|
479
|
+
} catch (err) {
|
|
480
|
+
clearPendingAsk({
|
|
481
|
+
userId,
|
|
482
|
+
from: fromRef.key,
|
|
483
|
+
to,
|
|
484
|
+
requestId,
|
|
485
|
+
});
|
|
486
|
+
const e = err as { message?: string };
|
|
487
|
+
process.stderr.write(
|
|
488
|
+
`[session-comm] ask_session: failed ${currentTopic} → ${to} requestId=${requestId}: ${e?.message || "Unknown"}\n`,
|
|
489
|
+
);
|
|
490
|
+
return mcpError(`Error: "${to}" 세션에 메시지 전송 실패: ${e?.message || "Unknown"}`);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return mcpOk(
|
|
494
|
+
`"${to}" 세션에 참조 요청을 보냈습니다.\n\nrequest_id: ${requestId}\n\n"${to}"의 응답은 '[ask_session 응답 ← ${to} | request_id: ${requestId}]' 형식으로 이 세션에 자동으로 돌아옵니다. 응답이 도착할 때까지 같은 요청으로 ask_session을 재호출하지 마세요.`,
|
|
495
|
+
);
|
|
496
|
+
},
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
server.tool(
|
|
500
|
+
"abort_session",
|
|
501
|
+
"Abort the currently running query in another session. Use peek_session first to confirm it is busy.",
|
|
502
|
+
{
|
|
503
|
+
to: z.string().describe("Target session/topic name to abort"),
|
|
504
|
+
},
|
|
505
|
+
async ({ to }) => {
|
|
506
|
+
const remote = remotePeerTarget(to);
|
|
507
|
+
if (remote) {
|
|
508
|
+
const result = await forwardToPeer({
|
|
509
|
+
action: "abort",
|
|
510
|
+
toNode: remote.node,
|
|
511
|
+
toTopic: remote.topic,
|
|
512
|
+
userId,
|
|
513
|
+
...(peerHostQueryId ? { sourceQueryId: peerHostQueryId } : {}),
|
|
514
|
+
});
|
|
515
|
+
if (!result.ok) {
|
|
516
|
+
return mcpError(`Error: "${to}" 원격 abort 실패: ${result.error}`);
|
|
517
|
+
}
|
|
518
|
+
return mcpOk(`"${to}" 세션(노드 ${remote.node})에 abort 신호를 보냈습니다.`);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const validation = validateTarget(to);
|
|
522
|
+
if (!validation.ok) return validation.error;
|
|
523
|
+
|
|
524
|
+
if (to === currentTopic) {
|
|
525
|
+
return mcpError(`Error: 자기 자신은 abort할 수 없습니다.`);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
try {
|
|
529
|
+
const targetTopicId = validation.target.topicId;
|
|
530
|
+
if (!targetTopicId) {
|
|
531
|
+
return mcpError(`Error: "${to}" 세션의 토픽 ID를 찾을 수 없습니다.`);
|
|
532
|
+
}
|
|
533
|
+
const inboxFile = buildInboxPath(targetTopicId);
|
|
534
|
+
mkdirSync(dirname(inboxFile), { recursive: true });
|
|
535
|
+
// Send query abort signal via inbox
|
|
536
|
+
appendJsonlEntry(inboxFile, {
|
|
537
|
+
type: "abort",
|
|
538
|
+
timestamp: new Date().toISOString(),
|
|
539
|
+
});
|
|
540
|
+
process.stderr.write(`[session-comm] abort_session: ${currentTopic} → ${to}\n`);
|
|
541
|
+
} catch (err) {
|
|
542
|
+
const e = err as { message?: string };
|
|
543
|
+
process.stderr.write(
|
|
544
|
+
`[session-comm] abort_session: failed ${currentTopic} → ${to}: ${e?.message || "Unknown"}\n`,
|
|
545
|
+
);
|
|
546
|
+
return mcpError(`Error: abort 신호 전송 실패: ${e?.message || "Unknown"}`);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return mcpOk(`"${to}" 세션에 abort 신호를 보냈습니다. 실행 중인 쿼리가 있으면 중단됩니다.`);
|
|
550
|
+
},
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
server.tool(
|
|
554
|
+
"tell_session",
|
|
555
|
+
"TELL — Delegate work or push context TO another session (one-way, nothing returns to your context). Message joins target's history; target processes async with full tools and the result lives in the target topic. Use for: delegating long-running or self-contained work (experiments, benchmarks, monitoring runs, file generation), status updates, persistent context injection — anything whose output the user can simply read in the target topic without you needing it. Prefer tell over ask_session whenever YOUR context doesn't need the result, since ask injects the full reply back and burns context. Decision rule: 'Do I need this output in MY context to proceed?' No → tell. Yes → ask_session.",
|
|
556
|
+
{
|
|
557
|
+
to: z.string().describe("Target session/topic name (e.g. '회의록', '신건')"),
|
|
558
|
+
message: z.string().describe("Message to send to the target session"),
|
|
559
|
+
},
|
|
560
|
+
async ({ to, message }) => {
|
|
561
|
+
if (message.length > MAX_MESSAGE_LENGTH) {
|
|
562
|
+
return mcpError(
|
|
563
|
+
`Error: message too long (${message.length} chars, max ${MAX_MESSAGE_LENGTH})`,
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (currentDepth + 1 > MAX_TELL_DEPTH) {
|
|
568
|
+
return mcpError(
|
|
569
|
+
`Error: depth 한도 도달 (현재 ${currentDepth}, 최대 ${MAX_TELL_DEPTH}). 더 이상 tell_session 체인을 만들 수 없습니다.`,
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Remote target ("node/topic") — hand to the runtime's peer forwarder.
|
|
574
|
+
const remote = remotePeerTarget(to);
|
|
575
|
+
if (remote) {
|
|
576
|
+
const fromRef = currentTopicRef();
|
|
577
|
+
const requestId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
578
|
+
const result = await forwardToPeer({
|
|
579
|
+
action: "tell",
|
|
580
|
+
toNode: remote.node,
|
|
581
|
+
toTopic: remote.topic,
|
|
582
|
+
userId,
|
|
583
|
+
fromKey: fromRef.key,
|
|
584
|
+
fromTitle: fromRef.title,
|
|
585
|
+
...(fromRef.topicId ? { fromTopicId: fromRef.topicId } : {}),
|
|
586
|
+
message,
|
|
587
|
+
requestId,
|
|
588
|
+
depth: currentDepth + 1,
|
|
589
|
+
...(peerHostQueryId ? { sourceQueryId: peerHostQueryId } : {}),
|
|
590
|
+
});
|
|
591
|
+
if (!result.ok) {
|
|
592
|
+
return mcpError(`Error: "${to}" 원격 세션에 전송 실패: ${result.error}`);
|
|
593
|
+
}
|
|
594
|
+
return mcpOk(
|
|
595
|
+
`"${to}" 토픽(노드 ${remote.node})에 메시지를 전달했습니다.\n\nrequest_id: ${requestId}\n\n결과는 해당 노드의 "${remote.topic}" 대화 기록에 남습니다.`,
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const validation = validateTarget(to);
|
|
600
|
+
if (!validation.ok) return validation.error;
|
|
601
|
+
if (!validation.target.agent) {
|
|
602
|
+
return mcpError(
|
|
603
|
+
`Error: "${to}" 토픽에는 AI가 초대되어 있지 않아 tell_session으로 작업을 trigger할 수 없습니다.`,
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const targetTopicId = validation.target.topicId;
|
|
608
|
+
if (!targetTopicId) {
|
|
609
|
+
return mcpError(`Error: "${to}" 세션의 토픽 ID를 찾을 수 없습니다.`);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Write to inbox — the Otium consumer will persist the DB message
|
|
613
|
+
// and trigger the AI turn. This matches the ask_session / abort_session
|
|
614
|
+
// pattern where the MCP is a pure writer and the consumer is the consumer.
|
|
615
|
+
//
|
|
616
|
+
// NOTE: no direct DB write here; the consumer in the Otium server process
|
|
617
|
+
// handles `deliverMessageToTopic` + the AI trigger.
|
|
618
|
+
const fromRef = currentTopicRef();
|
|
619
|
+
const requestId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
620
|
+
try {
|
|
621
|
+
appendJsonlEntry(buildInboxPath(targetTopicId), {
|
|
622
|
+
type: "tell",
|
|
623
|
+
requestId,
|
|
624
|
+
from: fromRef.key,
|
|
625
|
+
fromTitle: fromRef.title,
|
|
626
|
+
...(fromRef.topicId ? { fromTopicId: fromRef.topicId } : {}),
|
|
627
|
+
message,
|
|
628
|
+
depth: currentDepth + 1,
|
|
629
|
+
timestamp: new Date().toISOString(),
|
|
630
|
+
});
|
|
631
|
+
} catch (err: unknown) {
|
|
632
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
633
|
+
process.stderr.write(
|
|
634
|
+
`[session-comm] tell_session: failed to write inbox ${currentTopic} → ${to}: ${e.message}\n`,
|
|
635
|
+
);
|
|
636
|
+
return mcpError(`Error: "${to}" 세션에 메시지 전송 실패: ${e.message}`);
|
|
637
|
+
}
|
|
638
|
+
process.stderr.write(
|
|
639
|
+
`[session-comm] tell_session: ${fromRef.key} → ${to} requestId=${requestId} (topic ${validation.target.topicId ?? validation.target.name})\n`,
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
return mcpOk(
|
|
643
|
+
`"${to}" 토픽에 메시지를 전달했습니다.\n\nrequest_id: ${requestId}\n\n"${to}"의 대화 기록에 남습니다(다시 열거나 새로고침하면 보입니다).`,
|
|
644
|
+
);
|
|
645
|
+
},
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
await connectStdio(server);
|